作者:vivo 互联网服务器团队- Wang Fei、LinYupan

Dubbo泛化调用特性可以在不依赖服务接口API包的场景中发起远程调用, 这种特性特别适合框架集成和网关类应用开发。

本文结合在实际开发过程中所遇到的需要远程调用多个三方系统的问题,阐述了如何利用Dubbo泛化调用来简化开发降低系统耦合性的项目实践,最后对Dubbo泛化调用的原理进行了深度解析。

一、背景

统一配置平台是一个提供终端设备各个模块进行文件配置和文件下发能力的平台,模块开发在后台服务器进行文件配置,然后终端设备可以按照特定规则获取对应的配置文件,文件下发可以按照多种设备维度进行下发,具体项目框架可以参加下图:

现有的下发策略,都是由模块开发在统一配置后台服务器进行下发维度配置,文件是否下发到对应终端设备,由用户在本平台所选择的维度所确定。

但是其他业务方也存在在公司内部的A/B实验平台配置下发规则,来借助统一配置平台每天轮询服务器请求新文件的能力,但是在统一配置平台配置的文件是否能够下发由A/B实验平台来确定,A/B实验平台内会配置对应的规则以及配置统一配置平台对应的文件id,然后统一配置平台就需要针对请求调用A/B实验平台接口来判断文件是否可以下发。

随着公司内部实验平台的增加,越来越多这种由三方平台来决定文件是否下发的对接需求,如何更好更快的应对这种类似的对接需求,是我们需要去深入思考的问题。

二、方案选型

原有统一配置的下发逻辑是先找到所有可以下发的文件,然后判断单个配置文件是否满足设备维度,如果满足则可以下发。现在在对接A/B实验平台以后,文件是否能下发还需要由外部系统来确定,当时设计时考虑过两种方案:

  • 方案一:

同样先找到所有可以下发的文件,然后针对单个文件按照①设备维度判断是否匹配,然后②调用A/B实验平台的接口获取这台设备可以下发的文件Id, 再调用③灰度实验平台获取这台设备可以下发的文件id, 最后将前三步获取到的配置文件id进行汇总得到可以下发的文件,如下图所示。

方案一打破了原来文件是否能够下发的判断逻辑,现在除了原有的判断逻辑,还需要额外步骤调用其他系统来追加另外可以下发的文件。并且后续不可避免对接其他三方系统,方案一需要不断增加调用三方接口的逻辑来追加可以下发的文件id。此外常规的dubbo调用在provider端需要引入其他实验系统的二方库以及模型类型,增加了统一配置系统和其他系统的强耦合性。

  • 方案二:

利用 Dubbo 泛化调用高级特性抽象一个下发维度(远程调用),专门用于其他想由三方实验系统来决定是否下发文件的场景,如下图所示:

方案二统一抽象一个远程调用下发维度,可以保持原有的判断逻辑,也就是先把系统中所有可以下发的文件先查找出来,然后根据设备维度进行匹配,如果某一个文件配置的是远程调用维度,那么查找这个远程调用维度所包含的函数名称、参数类型数组和参数值对象数组,然后调用三方接口,从而判断这个文件是否可以下发到设备,最终获取到可以下发的文件id列表。

此外,利用Dubbo泛化调用高级特性,调用方并不关心提供者的接口的详细定义,只需要关注调用哪个方法,传什么参数以及接收到什么返回结果即可,这样避免需要依赖服务提供者的二方库以及模型类元,这样可以大大降低consumer端和provider端的耦合性。

综合上面的分析,我们最终确定了方案二采取利用Dubbo泛化调用来抽象一个统一维度的方式,下面来看一下具体的实现。

三、具体实现

1.GenericService是Dubbo提供的泛化接口,用来进行泛化调用。只提供了一个$invoke方法,三个入口参数分别为函数名称、参数类型数组和参数值对象数组。

  1. package com.alibaba.dubbo.rpc.service;
  2.  
  3. /**
  4. * Generic service interface
  5. *
  6. * @export
  7. */
  8. public interface GenericService {
  9.  
  10. /**
  11. * Generic invocation
  12. *
  13. * @param method Method name, e.g. findPerson. If there are overridden methods, parameter info is
  14. * required, e.g. findPerson(java.lang.String)
  15. * @param parameterTypes Parameter types
  16. * @param args Arguments
  17. * @return invocation return value
  18. * @throws Throwable potential exception thrown from the invocation
  19. */
  20. Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;

2.创建服务引用配置对象ReferenceConfig。

  1. private ReferenceConfig<GenericService> buildReferenceConfig(RemoteDubboRestrictionConfig config) {
  2. ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
  3. referenceConfig.setApplication(applicationConfig);
  4. referenceConfig.setRegistry(registryConfig);
  5. referenceConfig.setInterface(config.getInterfaceName());
  6. referenceConfig.setVersion(config.getVersion());
  7. referenceConfig.setGeneric(Boolean.TRUE.toString());
  8. referenceConfig.setCheck(false);
  9. referenceConfig.setTimeout(DUBBO_INVOKE_TIMEOUT);
  10. referenceConfig.setRetries(DUBBO_INVOKE_RETRIES);
  11. return referenceConfig;
  12. }

3.设置请求参数及服务调用, 这里利用在后台所配置的完整方法名、参数类型数组和参数值数组就可以进行服务调用。

  1. public List<Integer> invoke(RemoteDubboRestrictionConfig config, ConfigFileListQuery listQuery) {
  2. //由于ReferenceConfig很重量,里面封装了所有与注册中心及服务提供方连接,所以这里做了缓存
  3. GenericService genericService = prepareGenericService(config);
  4.  
  5. //构建参数
  6. Map<String, Object> params = buildParams(listQuery);
  7. String method = config.getMethod();
  8. String[] parameterTypeArray = new String[]{Map.class.getName()};
  9. Object[] parameterValueArray = new Object[]{params};
  10.  
  11. long begin = System.currentTimeMillis();
  12. Assert.notNull(genericService, "cannot find genericService");
  13. //具体调用
  14. Object result = genericService.$invoke(method, parameterTypeArray, parameterValueArray);
  15.  
  16. if (logger.isDebugEnabled()) {
  17. long duration = System.currentTimeMillis() - begin;
  18. logger.debug("Dubbo调用结果:{}, 耗时: {}", result, duration);
  19. }
  20.  
  21. return result == null ? Collections.emptyList() : (List<Integer>) result;
  22. }

那么为什么Dubbo泛化调用所涉及的调用方并不关心提供者的接口的详细定义,只需要关注调用哪个方法,传什么参数以及接收到什么返回结果即可呢?

在讲解泛化调用的实现原理之前,先简单讲述一下直接调用的原理。

四、 Dubbo 直接调用相关原理

Dubbo的直接调用相关原理涉及到两个方面:Dubbo服务暴露原理和Dubbo服务消费原理

4.1 Dubbo 服务暴露原理

4.1.1 服务远程暴露的整体流程

在整体上看,Dubbo框架做服务暴露分为两大部分,第一步将持有的服务实例通过代理转 换成Invoker,第二步会把Invoker通过具体的协议(比如Dubbo)转换成Exporter,框架做了 这层抽象也大大方便了功能扩展。

这里的Invoker可以简单理解成一个真实的服务对象实例,是 Dubbo框架实体域,所有模型都会向它靠拢,可向它发起invoke调用。它可能是一个本地的实 现,也可能是一个远程的实现,还可能是一个集群实现。

源代码如下:

  1. if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
  2.  
  3. // export to local if the config is not remote (export to remote only when config is remote)
  4. if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
  5. exportLocal(url);
  6. }
  7. // export to remote if the config is not local (export to local only when config is local)
  8. if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
  9. if (logger.isInfoEnabled()) {
  10. logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
  11. }
  12. if (registryURLs != null && !registryURLs.isEmpty()) {
  13. for (URL registryURL : registryURLs) {
  14. url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
  15. URL monitorUrl = loadMonitor(registryURL);
  16. if (monitorUrl != null) {
  17. url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
  18. }
  19. if (logger.isInfoEnabled()) {
  20. logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
  21. }
  22.  
  23. // For providers, this is used to enable custom proxy to generate invoker
  24. String proxy = url.getParameter(Constants.PROXY_KEY);
  25. if (StringUtils.isNotEmpty(proxy)) {
  26. registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
  27. }
  28.  
  29. Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
  30. DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
  31. //向注册中心注册服务信息
  32. Exporter<?> exporter = protocol.export(wrapperInvoker);
  33. exporters.add(exporter);
  34. }
  35. } else {
  36. Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
  37. DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
  38.  
  39. Exporter<?> exporter = protocol.export(wrapperInvoker);
  40. exporters.add(exporter);
  41. }
  42. }

首先将实现类ref封装为Invoker,之后将invoker转换为exporter,最后将exporter放入缓存Listexporters中。

4.1.2 服务暴露的细节

4.1.2.1 将实现类ref封装为Invoker

  1. Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));

① dubbo远程暴露的入口在ServiceBean的export()方法,由于servicebean'继承了serviceconfig类,于是真正执行暴露的逻辑是serviceconfig的doExport()方法。

② Dubbo支持相同服务暴露多个协议,比如同时暴露Dubbo和REST协议,也支持多个注册中心,比如zookeeper和nacos,框架内部会依次 对使用的协议都做一次服务暴露,每个协议注册元数据都会写入多个注册中心,具体是执行doExportUrlsFor1Protocol。

③ 然后通过动态代理的方式创建Invoker对象,在服务端生成AbstractProxylnvoker实例,所有真实的方法调用都会委托给代理,然后代理转发给服务实现者 ref 调用;动态代理一般有:JavassistProxyFactory 和 JdkProxyFactory两种方式,这里所选用的JavassistProxyFactory 。

4.1.2.2 将invoker转换为exporter

Exporter exporter= protocol.export(wrapperInvoker);

  1. Exporter<?> exporter = protocol.export(wrapperInvoker);

在将服务实例ref转换成Invoker之后,开始执行服务暴露过程。

这里会经过一系列的过滤器链路,最终会通过RegistryProtocol#export 进行更细粒度的控制,比如先进行服务暴露再注册服务元数据。注册中心在做服务暴露时依次 做了以下几件事情:

  1. 委托具体协议(Dubbo)进行服务暴露,创建NettyServer监听端口和保存服务实例。

  2. 创建注册中心对象,与注册中心创建TCP连接。

  3. 注册服务元数据到注册中心。

  4. 订阅configurators节点,监听服务动态属性变更事件。

  5. 服务销毁收尾工作,比如关闭端口、反注册服务信息等。

  1. @Override
  2. public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
  3. //export invoker
  4. final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
  5.  
  6. URL registryUrl = getRegistryUrl(originInvoker);
  7.  
  8. //registry provider
  9. final Registry registry = getRegistry(originInvoker);
  10. final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);
  11.  
  12. //to judge to delay publish whether or not
  13. boolean register = registeredProviderUrl.getParameter("register", true);
  14.  
  15. ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);
  16.  
  17. if (register) {
  18. //TODO 注册服务元数据
  19. register(registryUrl, registeredProviderUrl);
  20. ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
  21. }
  22.  
  23. // Subscribe the override data
  24. // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call the same service. Because the subscribed is cached key with the name of the service, it causes the subscription information to cover.
  25. final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);
  26. final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
  27. overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
  28. registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
  29. //Ensure that a new exporter instance is returned every time export
  30. return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
  31. }

这里我们重点讲解委托具体协议进行服务暴露的过程doLocalExport(final InvokeroriginInvoker)。

  1. private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) {
  2. String key = getCacheKey(originInvoker);
  3. ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
  4. if (exporter == null) {
  5. synchronized (bounds) {
  6. exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
  7. if (exporter == null) {
  8. final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
  9. exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
  10. bounds.put(key, exporter);
  11. }
  12. }
  13. }
  14. return exporter;
  15. }

(Exporter) protocol.export(invokerDelegete)方法又会经过一系列的拦截器进行处理,最终调用DubboProtocol的export方法。

  1. @Override
  2. public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
  3. URL url = invoker.getUrl();
  4.  
  5. // export service.
  6. String key = serviceKey(url);
  7. DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
  8. exporterMap.put(key, exporter);
  9.  
  10. //export an stub service for dispatching event
  11. Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
  12. Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
  13. if (isStubSupportEvent && !isCallbackservice) {
  14. String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
  15. if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
  16. if (logger.isWarnEnabled()) {
  17. logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
  18. "], has set stubproxy support event ,but no stub methods founded."));
  19. }
  20. } else {
  21. stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
  22. }
  23. }
  24.  
  25. openServer(url);
  26. optimizeSerialization(url);
  27. return exporter;
  28. }

这里很重要的一点就是将exporter放到了缓存,这里的key是serviceGroup/serviceName:serviceVersion:port这样形式,这里最后获取到的是com.alibaba.dubbo.demo.DemoService:20880,之后创建DubboExporter。这里的内存缓存exporterMap是很重要的一个属性,在后续消费者调用服务提供者时会被被再次使用到。

至此,服务器提供者的远程暴露流程就基本介绍完毕。

4.2 Dubbo服务消费的实现原理

4.2.1 服务消费的整体流程

在整体上看,Dubbo框架做服务消费也分为两大部分,第一步通过持有远程服务实例生成 Invoker,这个Invoker在客户端是核心的远程代理对象。第二步会把Invoker通过动态代理转换 成实现用户接口的动态代理引用。这里的Invoker承载了网络连接、服务调用和重试等功能,在 客户端,它可能是一个远程的实现,也可能是一个集群实现。

源代码如下:

  1. public Object getObject() throws Exception {
  2. return get();
  3. }
  4.  
  5. public synchronized T get() {
  6. if (destroyed) {
  7. throw new IllegalStateException("Already destroyed!");
  8. }
  9. if (ref == null) {
  10. init();
  11. }
  12. return ref;
  13. }
  14.  
  15. private void init() {
  16. ...
  17. ref = createProxy(map);
  18. }
  19.  
  20. private T createProxy(Map<String, String> map) {
  21. ...
  22. if (urls.size() == 1) {
  23. invoker = refprotocol.refer(interfaceClass, urls.get(0));
  24. }
  25. ...
  26. // 创建服务代理
  27. return (T) proxyFactory.getProxy(invoker);
  28. }

4.2.2 服务消费的细节

4.2.2.1 使用Protocol将interfaceClass转化为Invoker

invoker = refprotocol.refer(interfaceClass, url);

① 服务引用的入口点在 ReferenceBean#getObject,由于Referencebean'继承了serviceconfig类,接着会调用Reference的get方法。

② 然后根据引用的接口类型将持有远程服务实例生成 Invoker。

③ 通过一系列的过滤器链,最后调用RegistryProtocol的doRefer方法。

  1. private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
  2. RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
  3. directory.setRegistry(registry);
  4. directory.setProtocol(protocol);
  5. // all attributes of REFER_KEY
  6. Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
  7. URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);
  8. if (!Constants.ANY_VALUE.equals(url.getServiceInterface())
  9. && url.getParameter(Constants.REGISTER_KEY, true)) {
  10. URL registeredConsumerUrl = getRegisteredConsumerUrl(subscribeUrl, url);
  11. registry.register(registeredConsumerUrl);
  12. directory.setRegisteredConsumerUrl(registeredConsumerUrl);
  13. }
  14. directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
  15. Constants.PROVIDERS_CATEGORY
  16. + "," + Constants.CONFIGURATORS_CATEGORY
  17. + "," + Constants.ROUTERS_CATEGORY));
  18.  
  19. Invoker invoker = cluster.join(directory);
  20. ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
  21. return invoker;
  22. }

这段逻辑主要完成了注册中心实例的创建,元数据注册到注册中心及订阅的功能。

具体远程Invoker是在哪里创建的呢?客户端调用拦截器又是在哪里构造的呢?

当在directory.subscrib()中 第一次发起订阅时会进行一次数据拉取操作,同时触发RegistryDirectory#notify方法,这里 的通知数据是某一个类别的全量数据,比如providers和routers类别数据。当通知providers数 据时,在RegistryDirectory#toInvokers方法内完成Invoker转换。

  1. private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
  2. Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>();
  3. if (urls == null || urls.isEmpty()) {
  4. return newUrlInvokerMap;
  5. }
  6. Set<String> keys = new HashSet<String>();
  7. String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
  8. for (URL providerUrl : urls) {
  9. // If protocol is configured at the reference side, only the matching protocol is selected
  10. ......
  11. URL url = mergeUrl(providerUrl);
  12.  
  13. String key = url.toFullString(); // The parameter urls are sorted
  14. if (keys.contains(key)) { // Repeated url
  15. continue;
  16. }
  17. keys.add(key);
  18. // Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again
  19. Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference
  20. Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
  21. if (invoker == null) { // Not in the cache, refer again
  22. try {
  23. boolean enabled = true;
  24. .........
  25. if (enabled) {
  26. invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
  27. }
  28. } catch (Throwable t) {
  29. logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
  30. }
  31. if (invoker != null) { // Put new invoker in cache
  32. newUrlInvokerMap.put(key, invoker);
  33. }
  34. } else {
  35. newUrlInvokerMap.put(key, invoker);
  36. }
  37. }
  38. keys.clear();
  39. return newUrlInvokerMap;
  40. }

核心代码

  1. invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);

这里会经过一系列的过滤器链,然后最终调用DubboProtocol的refer方法,来创建具体的invoker。

  1. @Override
  2. public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
  3. optimizeSerialization(url);
  4. // create rpc invoker.
  5. DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
  6. invokers.add(invoker);
  7. return invoker;
  8. }

这里返回的invoker会用来更新RegistryDirectory的methodInvokerMap 属性,最终在实际调用消费端方法时,会根据method找到对应的invoker列表。

  1. private void refreshInvoker(List<URL> invokerUrls) {
  2. if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null
  3. && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
  4. this.forbidden = true; // Forbid to access
  5. this.methodInvokerMap = null; // Set the method invoker map to null
  6. destroyAllInvokers(); // Close all invokers
  7. } else {
  8. this.forbidden = false; // Allow to access
  9. Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
  10. if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
  11. invokerUrls.addAll(this.cachedInvokerUrls);
  12. } else {
  13. this.cachedInvokerUrls = new HashSet<URL>();
  14. this.cachedInvokerUrls.addAll(invokerUrls);//Cached invoker urls, convenient for comparison
  15. }
  16. if (invokerUrls.isEmpty()) {
  17. return;
  18. }
  19. Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map
  20. Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // Change method name to map Invoker Map
  21. // state change
  22. // If the calculation is wrong, it is not processed.
  23. if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
  24. logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :" + invokerUrls.size() + ", invoker.size :0. urls :" + invokerUrls.toString()));
  25. return;
  26. }
  27. this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
  28. this.urlInvokerMap = newUrlInvokerMap;
  29. try {
  30. destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
  31. } catch (Exception e) {
  32. logger.warn("destroyUnusedInvokers error. ", e);
  33. }
  34. }
  35. }

4.2.2.2 使用ProxyFactory创建代理

  1. (T) proxyFactory.getProxy(invoker)

上述的proxyFactory是ProxyFactory$Adaptive实例,其getProxy内部最终得到是一个被StubProxyFactoryWrapper包装后的JavassistProxyFactory。直接来看JavassistProxyFactory.getProxy方法。

  1. public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
  2. return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
  3. }

【invoker】:MockClusterInvoker实例

【interfaces】:[interface com.alibaba.dubbo.demo.DemoService, interface com.alibaba.dubbo.rpc.service.EchoService]

我们最终返回的代理对象其实是一个proxy0对象,当我们调用其sayHello方法时,其调用内部的handler.invoke方法。

  1. package com.alibaba.dubbo.common.bytecode;
  2.  
  3. import com.alibaba.dubbo.demo.DemoService;
  4. import java.lang.reflect.InvocationHandler;
  5. import java.lang.reflect.Method;
  6.  
  7. public class proxy0 implements DemoService {
  8. public static Method[] methods;
  9. private InvocationHandler handler;
  10.  
  11. public String sayHello(String paramString) {
  12. Object[] arrayOfObject = new Object[1];
  13. arrayOfObject[0] = paramString;
  14. Object localObject = this.handler.invoke(this, methods[0], arrayOfObject);
  15. return (String) localObject;
  16. }
  17.  
  18. public proxy0() {
  19. }
  20.  
  21. public proxy0(InvocationHandler paramInvocationHandler) {
  22. this.handler = paramInvocationHandler;
  23. }
  24. }

Dubbo泛化调用一般是服务器提供者都采用直接暴露的形式,消费者端采用服务泛化调用的形式,所以这里重点讨论Dubbo泛化调用与直接调用在消费者端的服务引用和发起消费的区别与联系。

五、Dubbo泛化调用与直接调用的区别与联系

5.1 通过持有远程服务实例生成 Invoker

  1. private T createProxy(Map<String, String> map) {
  2. ...
  3. if (urls.size() == 1) {
  4. invoker = refprotocol.refer(interfaceClass, urls.get(0));
  5. }
  6. ...
  7. // 创建服务代理
  8. return (T) proxyFactory.getProxy(invoker);
  9. }

这里的interfaceClass的来源不一样,createProxy(Mapmap) 是在ReferenceConfig的init()方法中调用的,具体的interfaceClass根据是否是返回调用会有所区别,具体看如下代码:

  1. private void init() {
  2. ...
  3. if (ProtocolUtils.isGeneric(getGeneric())) {
  4. interfaceClass = GenericService.class;
  5. } else {
  6. try {
  7. interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
  8. .getContextClassLoader());
  9. } catch (ClassNotFoundException e) {
  10. throw new IllegalStateException(e.getMessage(), e);
  11. }
  12. checkInterfaceAndMethods(interfaceClass, methods);
  13. }
  14. ...
  15. ref = createProxy(map);
  16. }

直接调用:interfaceClass→com.alibaba.dubbo.demo.DemoService

泛化调用:interfaceClass→com.alibaba.dubbo.rpc.service.GenericService

最终获取的invoker也不一样

直接调用:

interface com.alibaba.dubbo.demo.DemoService -> dubbo://xx.xx.xx.xx:20881/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-consumer&bean.name=com.alibaba.dubbo.demo.DemoService&check=false&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=24932&qos.port=33333ister.ip=xx.xx.xx.xx&remote.timestamp=1640744945905&side=consumer×tamp=1640745033688

泛化调用:

interface com.alibaba.dubbo.rpc.service.GenericService -> dubbo://xx.xx.xx.xx:20881/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=test&bean.name=com.alibaba.dubbo.demo.DemoService&check=false&dubbo=2.0.2&generic=true&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=27952&qos.port=33333ister.ip=xx.xx.xx.xx&remote.timestamp=1640748337173&side=consumer×tamp=1640748368427

5.2 服务发起消费流程

在4.2.2 服务消费者发起请求细节第①步是将请求参数(方法名,方法参数类型,方法参数值,服务名,附加参数)封装成一个Invocation。

直接调用的RpcInvoaction如下:

RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[world], attachments={}]

泛化调用的RpcInvoaction如下:

RpcInvocation [methodName=$invoke, parameterTypes=[class java.lang.String, class [Ljava.lang.String;, class [Ljava.lang.Object;], arguments=[sayHello, [Ljava.lang.String;@22c1bff0, [Ljava.lang.Object;@30ae230f], attachments={path=com.alibaba.dubbo.demo.DemoService, input=296, dubbo=2.0.2, interface=com.alibaba.dubbo.demo.DemoService, version=0.0.0, generic=true}]

我们可以发现这里生成的RpcInvocation对象是有区别的,但是服务提供者暴露的服务是不会发生变化的,所以这里必然有一个转换过程,这里的参数转换关键就在于服务提供者端的GenericImplFilter类。

  1. @Activate(group = Constants.PROVIDER, order = -20000)
  2. public class GenericFilter implements Filter {
  3.  
  4. protected final Logger logger = LoggerFactory.getLogger(getClass());
  5.  
  6. @Override
  7. public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
  8. logger.info("----------------GenericFilter-------------------------");
  9. if (inv.getMethodName().equals(Constants.$INVOKE)
  10. && inv.getArguments() != null
  11. && inv.getArguments().length == 3
  12. && !invoker.getInterface().equals(GenericService.class)) {
  13. String name = ((String) inv.getArguments()[0]).trim();
  14. String[] types = (String[]) inv.getArguments()[1];
  15. Object[] args = (Object[]) inv.getArguments()[2];
  16. try {
  17. Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
  18. Class<?>[] params = method.getParameterTypes();
  19. if (args == null) {
  20. args = new Object[params.length];
  21. }
  22. String generic = inv.getAttachment(Constants.GENERIC_KEY);
  23.  
  24. if (StringUtils.isBlank(generic)) {
  25. generic = RpcContext.getContext().getAttachment(Constants.GENERIC_KEY);
  26. }
  27.  
  28. if (StringUtils.isEmpty(generic)
  29. || ProtocolUtils.isDefaultGenericSerialization(generic)) {
  30. args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
  31. } else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
  32. ...
  33. } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
  34. ...
  35. }
  36. Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));
  37. if (result.hasException()
  38. && !(result.getException() instanceof GenericException)) {
  39. return new RpcResult(new GenericException(result.getException()));
  40. }
  41. RpcResult rpcResult;
  42. if (ProtocolUtils.isJavaGenericSerialization(generic)) {
  43. ...
  44. } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
  45. ...
  46. } else {
  47. rpcResult = new RpcResult(PojoUtils.generalize(result.getValue()));
  48. }
  49. rpcResult.setAttachments(result.getAttachments());
  50. return rpcResult;
  51. } catch (NoSuchMethodException e) {
  52. throw new RpcException(e.getMessage(), e);
  53. } catch (ClassNotFoundException e) {
  54. throw new RpcException(e.getMessage(), e);
  55. }
  56. }
  57. return invoker.invoke(inv);
  58. }
  59.  
  60. }

核心流程:

① 是否是泛化调用判断

  1. if (inv.getMethodName().equals(Constants.$INVOKE)
  2. && inv.getArguments() != null
  3. && inv.getArguments().length == 3
  4. && !invoker.getInterface().equals(GenericService.class)) {

② 参数的提取

  1. String name = ((String) inv.getArguments()[0]).trim();
  2. String[] types = (String[]) inv.getArguments()[1];
  3. Object[] args = (Object[]) inv.getArguments()[2];

③ 参数的序列化,再构造新的RpcInvocation对象

  1. Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
  2. Class<?>[] params = method.getParameterTypes();
  3. if (StringUtils.isEmpty(generic)
  4. || ProtocolUtils.isDefaultGenericSerialization(generic)) {
  5. args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
  6. }
  7. ...
  8.  
  9. Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));

序列化前RpcInvocation对象:

RpcInvocation [methodName=$invoke, parameterTypes=[class java.lang.String, class [Ljava.lang.String;, class [Ljava.lang.Object;], arguments=[sayHello, [Ljava.lang.String;@22c1bff0, [Ljava.lang.Object;@30ae230f], attachments={path=com.alibaba.dubbo.demo.DemoService, input=296, dubbo=2.0.2, interface=com.alibaba.dubbo.demo.DemoService, version=0.0.0, generic=true}]

序列化后RpcInvocation对象:

RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[world], attachments={path=com.alibaba.dubbo.demo.DemoService, input=296, dubbo=2.0.2, interface=com.alibaba.dubbo.demo.DemoService, version=0.0.0, generic=true}]

后面的调用逻辑就和直接调用的是一致的了,比如从本地缓存从本地缓存中Map<string, list<invoker>> methodInvokerMap中获取key为sayHello(指定方法名)的List<invoker>,接着进行后续的调用。

那么什么时候触发GenericFilter的invoke方法呢,这里其实就和过滤器的调用链建立有关系了,从GenericFilter类上的注解,我们可以看到@Activate(group = Constants.PROVIDER, order = -20000),说明是在服务提供者端生效的。

另外,服务提供者端是如何知道调用是直接调用还是泛化调用的,这里就涉及到与服务提供者端GenericFilter对应的消费者端的GenericImplFilter类,代码如下:

  1. /**
  2. * GenericImplInvokerFilter
  3. */
  4. @Activate(group = Constants.CONSUMER, value = Constants.GENERIC_KEY, order = 20000)
  5. public class GenericImplFilter implements Filter {
  6.  
  7. private static final Logger logger = LoggerFactory.getLogger(GenericImplFilter.class);
  8.  
  9. private static final Class<?>[] GENERIC_PARAMETER_TYPES = new Class<?>[]{String.class, String[].class, Object[].class};
  10.  
  11. @Override
  12. public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  13. String generic = invoker.getUrl().getParameter(Constants.GENERIC_KEY);
  14. if (ProtocolUtils.isGeneric(generic)
  15. && !Constants.$INVOKE.equals(invocation.getMethodName())
  16. && invocation instanceof RpcInvocation) {
  17. ...
  18. }
  19.  
  20. if (invocation.getMethodName().equals(Constants.$INVOKE)
  21. && invocation.getArguments() != null
  22. && invocation.getArguments().length == 3
  23. && ProtocolUtils.isGeneric(generic)) {
  24.  
  25. Object[] args = (Object[]) invocation.getArguments()[2];
  26. if (ProtocolUtils.isJavaGenericSerialization(generic)) {
  27.  
  28. for (Object arg : args) {
  29. if (!(byte[].class == arg.getClass())) {
  30. error(generic, byte[].class.getName(), arg.getClass().getName());
  31. }
  32. }
  33. } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
  34. for (Object arg : args) {
  35. if (!(arg instanceof JavaBeanDescriptor)) {
  36. error(generic, JavaBeanDescriptor.class.getName(), arg.getClass().getName());
  37. }
  38. }
  39. }
  40.  
  41. ((RpcInvocation) invocation).setAttachment(
  42. Constants.GENERIC_KEY, invoker.getUrl().getParameter(Constants.GENERIC_KEY));
  43. }
  44. return invoker.invoke(invocation);
  45. }

5.3 泛化调用的整体流程图

六、总结

高内聚低耦合是我们架构设计的一个重要目标,而Dubbo的泛化调用特性仅仅只需知道服务的完整接口路径、请求参数类型和请求参数值就可以直接进行调用获取请求结果,能够避免依赖特定三方jar包,从而降低了系统的耦合性。在日常学习和开发的过程中,我们除了需要关注一门技术的常规使用方法以外,还需要关注一些高级特性,从而做出更合适的架构设计。

参考资料:

  1. 《深入理解Apache Dubbo与实战》 诣极,林琳

  2. Dubbo源码分析-泛化调用

  3. Dubbo泛化调用使用及原理解析

Dubbo 泛化调用在vivo统一配置系统的应用的更多相关文章

  1. dubbo泛化调用 小demo

    前两天刚好有个同事来问是否用过 dubbo泛化 调用,不需要通过指定配置.第一次听到的时候,还是有点懵,但觉得有意思,可以学点东西. 立马百度了,找了demo,这篇比较容易上手(http://www. ...

  2. 两种方案实现Dubbo泛化调用

    Dubbo的泛化调用是一个服务A在没有服务B的依赖,包的情况下,只知道服务B的服务名:服务的接口的全限定类名和方法名,以及参数,实现服务A调用服务B. 原文链接:http://blog.qiyuan. ...

  3. Dubbo 泛化调用的参数解析问题及一个强大的参数解析工具 PojoUtils

    排查了3个多小时,因为一个简单的错误,发现一个强大的参数解析工具,记录一下. 背景 Nodejs 通过 tether 调用 Java Dubbo 服务.请求类的某个参数对象 EsCondition 有 ...

  4. vue3 vite 系统标题 系统名称统一配置

    想要统一配置系统名称 或者其他的,需要在vue3中使用 vite 的环境变量 vite 的环境变量 需要创建两个文件(和 vite.config.js 文件同一目录) .env.development ...

  5. dubbo接口泛化调用例子

    @ApiOperation(value = "dubbo泛化调用工具接口") public Result dubboApiTool( @ApiParam(value = " ...

  6. Dubbo高级特性实践-泛化调用

    引言 当后端Java服务用Dubbo协议作为RPC方案的基础,但部分消费方是前端Restful的PHP服务,不能直接调用,于是在中间架设了Router服务提供统一的基于HTTP的后端调用入口. 而Ro ...

  7. Dubbo 高级特性实践-泛化调用

    引言 当后端Java服务用Dubbo协议作为RPC方案的基础,但部分消费方是前端Restful的PHP服务,不能直接调用,于是在中间架设了Router服务提供统一的基于HTTP的后端调用入口. 而Ro ...

  8. Dubbo基本特性之泛化调用

    Dubbo 是支持泛化调用的,什么是泛化调用呢?泛化调用的好处是什么呢,泛化调用说白一点就是服务消费者并没有服务的接口. 在<Dubbo入门-搭建一个最简单的Demo框架>一文中,我们已完 ...

  9. Dubbo学习笔记4:服务消费端泛化调用与异步调用

    本文借用dubbo.learn的Dubbo API方式来解释原理. 服务消费端泛化调用 前面我们讲解到,基于Spring和基于Dubbo API方式搭建简单的分布式系统时,服务消费端引入了一个SDK二 ...

  10. Spring Boot微服务电商项目开发实战 --- 多环境部署配置、端口号统一配置及Dubbo提供者消费者实现

    昨天已经搭建好了SpringBoot基于Maven的基础父子级项目,今天开始进入项目分模块及分布式实现.首先我们基于昨天的项目,在父级工程下建lyn-sys,lyn-customer,lyn-good ...

随机推荐

  1. 你真的了解HashSet 和HashMap的区别、优缺点、使用场景吗?

    HashSet 和 HashMap 是 Java 集合框架中的两个常用类,它们都用于存储和管理数据,但在使用方式.功能和性能上有很大的区别. HashSet 和 HashMap 的区别 区别一:用途不 ...

  2. mac电脑升级后wifi报感叹号连不上WiFi的问题

    我的mac电脑是2015款的makebook pro,13英寸,之前一直用的是10.14系统,后来看到系统更新一直在推10.15系统,我就升级了10.15系统,但是升级后就坑爹了,wifi标志直接就不 ...

  3. isAlive

    线程存活 当线程执行时显示线程存活 执行完毕为false

  4. ubuntu20.04安装goaccess实时对nginx日志进行分析

    ubuntu20.04安装goaccess实时对nginx日志进行分析 goaccess可以对nginx日志进行分析,生成实时动态页面,同时通过nginx反向代理来解决WebSocket数据传输问题. ...

  5. 安装了华企盾DSC防泄密,所有进程的加密文件都无法打开

    用pchunter等工具查看系统回调中是否有文件厂商不存在的(system目录的除外),在恢复模式删除掉,或者用360系统急救箱查杀一下

  6. APP性能测试-客户端性能测试

    https://blog.csdn.net/xiaomaoxiao336368/article/details/83547318

  7. Python——第一章:注释、变量、常量

    python中的注释有2种: 1.单行注释 单行注释用# #这是一个单行注释 快捷键用Ctrl+/全选多个内容可以多行快速注释,也可以快速去掉注释符# 比如快速将全选的所有行注释掉--加# 2.多行注 ...

  8. 国产Geoscene Server 4.0编译SOE总结

    背景:公司一直使用Arcgis Server 10.1.Visual Studio 2010开发SOE功能,随着国产化软件发展大趋势,SOE中的各种分析功能,需要升级到Geoscene Server环 ...

  9. MySQL篇:第四章_详解DML语言

    DML语言 插入 一.方式一 语法: insert into 表名(字段名,...) values(值1,...); 特点: 1.字段类型和值类型一致或兼容,而且一一对应 2.可以为空的字段,可以不用 ...

  10. 8种桌面IDE CodeArts智能代码补全类型

    摘要:代码补全可以有效的提升开发效率.减少拼写错误和输入代码量.CodeArts 依赖于 codearts.smartassist-java-ls 插件实现代码补全功能. 本文分享自华为云社区< ...