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泛化调用来抽象一个统一维度的方式,下面来看一下具体的实现。
三、具体实现
-
GenericService是Dubbo提供的泛化接口,用来进行泛化调用。只提供了一个$invoke方法,三个入口参数分别为函数名称、参数类型数组和参数值对象数组。
package com.alibaba.dubbo.rpc.service;
/**
* Generic service interface
*
* @export
*/
public interface GenericService {
/**
* Generic invocation
*
* @param method Method name, e.g. findPerson. If there are overridden methods, parameter info is
* required, e.g. findPerson(java.lang.String)
* @param parameterTypes Parameter types
* @param args Arguments
* @return invocation return value
* @throws Throwable potential exception thrown from the invocation
*/
Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
2. 创建服务引用配置对象ReferenceConfig。
private ReferenceConfig<GenericService> buildReferenceConfig(RemoteDubboRestrictionConfig config) {
ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setRegistry(registryConfig);
referenceConfig.setInterface(config.getInterfaceName());
referenceConfig.setVersion(config.getVersion());
referenceConfig.setGeneric(Boolean.TRUE.toString());
referenceConfig.setCheck(false);
referenceConfig.setTimeout(DUBBO_INVOKE_TIMEOUT);
referenceConfig.setRetries(DUBBO_INVOKE_RETRIES);
return referenceConfig;
}
3.设置请求参数及服务调用, 这里利用在后台所配置的完整方法名、参数类型数组和参数值数组就可以进行服务调用。
public List<Integer> invoke(RemoteDubboRestrictionConfig config, ConfigFileListQuery listQuery) {
//由于ReferenceConfig很重量,里面封装了所有与注册中心及服务提供方连接,所以这里做了缓存
GenericService genericService = prepareGenericService(config);
//构建参数
Map<String, Object> params = buildParams(listQuery);
String method = config.getMethod();
String[] parameterTypeArray = new String[]{Map.class.getName()};
Object[] parameterValueArray = new Object[]{params};
long begin = System.currentTimeMillis();
Assert.notNull(genericService, "cannot find genericService");
//具体调用
Object result = genericService.$invoke(method, parameterTypeArray, parameterValueArray);
if (logger.isDebugEnabled()) {
long duration = System.currentTimeMillis() - begin;
logger.debug("Dubbo调用结果:{}, 耗时: {}", result, duration);
}
return result == null ? Collections.emptyList() : (List<Integer>) result;
}
那么为什么Dubbo泛化调用所涉及的调用方并不关心提供者的接口的详细定义,只需要关注调用哪个方法,传什么参数以及接收到什么返回结果即可呢?
在讲解泛化调用的实现原理之前,先简单讲述一下直接调用的原理。
四、 Dubbo 直接调用相关原理
Dubbo的直接调用相关原理涉及到两个方面:Dubbo服务暴露原理和Dubbo服务消费原理
4.1 Dubbo 服务暴露原理
4.1.1 服务远程暴露的整体流程
在整体上看,Dubbo框架做服务暴露分为两大部分,第一步将持有的服务实例通过代理转换成Invoker,第二步会把Invoker通过具体的协议(比如Dubbo)转换成Exporter,框架做了这层抽象也大大方便了功能扩展。
这里的Invoker可以简单理解成一个真实的服务对象实例,是 Dubbo框架实体域,所有模型都会向它靠拢,可向它发起invoke调用。它可能是一个本地的实 现,也可能是一个远程的实现,还可能是一个集群实现。
源代码如下:
if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
// export to local if the config is not remote (export to remote only when config is remote)
if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
exportLocal(url);
}
// export to remote if the config is not local (export to local only when config is local)
if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
if (logger.isInfoEnabled()) {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
if (registryURLs != null && !registryURLs.isEmpty()) {
for (URL registryURL : registryURLs) {
url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
URL monitorUrl = loadMonitor(registryURL);
if (monitorUrl != null) {
url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
}
if (logger.isInfoEnabled()) {
logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
}
// For providers, this is used to enable custom proxy to generate invoker
String proxy = url.getParameter(Constants.PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
}<