Dubbo与Spring集成
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
Dubbo框架常被当作第三方框架集成到应用中当Spring集成Dubbo框架后为什么在编写代码的时候只用了@DubboReference注解就可以调用提供方的服务了呢这篇笔记就是分析Dubbo框架是怎么与Spring结合的。
现状integration层代码编写形式
public interface SamplesFacade {
QueryOrderRes queryOrder(QueryOrderReq req);
}
public interface SamplesFacadeClient {
QueryOrderResponse queryRemoteOrder(QueryOrderRequest req);
}
public class SamplesFacadeClientImpl implements SamplesFacadeClient {
@DubboReference
private SamplesFacade samplesFacade;
@Override
public QueryOrderResponse queryRemoteOrder(QueryOrderRequest req){
// 构建下游系统需要的请求入参对象
QueryOrderReq integrationReq = buildIntegrationReq(req);
// 调用 Dubbo 接口访问下游提供方系统
QueryOrderRes resp = samplesFacade.queryOrder(integrationReq);
// 判断返回的错误码是否成功
if(!"000000".equals(resp.getRespCode())){
throw new RuntimeException("下游系统 XXX 错误信息");
}
// 将下游的对象转换为当前系统的对象
return convert2Response(resp);
}
}
思路
- SamplesFacade是下游提供方系统定义的一个接口改接口中有一个queryOrder的方法。
- SamplesFacadeClient是integration层中定义的一个接口并且实现了一个queryRemoteOrder方法专门负责与SamplesFacade的queryOrder打交道。
- SamplesFacadeClientImpl也是定义在integration层中并且实现了SamplesFacadeClient接口中重写了queryRemoteOrder方法。
queryRemoteOrder方法封装了调用下游接口的逻辑先构建调用下游系统的对象然后把对象传入下游系统的接口中再接收返回值并针对错误码判断最后转成自己的Bean对象。
这个地方就可以做优化封装一下屏蔽下游提供方各种接口的差异性减少重复性的代码编写。
封装
顺着调用链路分析有哪写变量因素在封装的时候需要考虑
- 因素1怎么知道调用下游提供方的哪个接口呢这说明下游的接口类名、方法名、方法入参类名是变量因素。
- 因素2怎么区分各个调用方的timeout、retries、cache、loadbalance等参数属性呢这说明消费方接口级别的Dubbo参数属性也是变量因素。
- 因素3调用下游提供方接口后拿到返回有些接口需要判断错误码有些接口不需要判断而且不同接口的错误码字段、错误码字段的值也可能不一样。这说明返参错误码的判断形式也是一个变量因素。
- 拿到下游接口的返回数据后怎么转成各个调用方想要的对象呢这说明将数据转成各个调用方期望的对象形式也是一个变量因素。
抽象
抽象是把相似流程的骨架抽象出来简单来说就是去掉表象保留相对不变的。
一段代码的流程可以是业务流程也可以是代码流程还可以是调用流程当然本质都是一小块相对聚集的业务逻辑的核心主干流程把不变的流程固化下来变成模板然后把变化的因素交给各个调用方意在求同存异追求不变的稳定放任变化的自由。
结合上班的例子不变的是重复写的那段调用逻辑先构建调用下游系统的请求对象并将请求对象传入下游系统的接口中然后接收返参并针对错误码进行判断最后转成自己的Bean对象。把变化的因素分发给各个具体业务的实现类。
根据源码的一些设计思想我们可以把变化的因素由注解来实现根据这个思考放行我们来再次分析前边是四大变化因素
- 因素1是下游的接口类名、方法名、方法入参类名涉及的类可以放在类注解上方法名、方法入参可以放在方法注解上。
- 因素2中消费方接口级别的timeout、retries、loadbalance等属性也可以放在方法注解上。
- 因素3中的错误码理论上下游提供方一个类中多个方法返回的格式应该是一样的所以如何判断错误码的变量因素可以放在类注解上。
- 因素4中如何将下游数据类型转换为本系统的Bean类型其实最终还是接口级别的事还是可以放在方法注解上。
根据我们的分析修改代码
@DubboFeignClient(
remoteClass = SamplesFacade.class,
needResultJudge = true,
resultJudge = (remoteCodeNode = "respCode", remoteCodeSuccValueList = "000000", remoteMsgNode = "respMsg")
)
public interface SamplesFacadeClient {
@DubboMethod(
timeout = "5000",
retries = "3",
loadbalance = "random",
remoteMethodName = "queryRemoteOrder",
remoteMethodParamsTypeName = {"com.hmily.QueryOrderReq"}
)
QueryOrderResponse queryRemoteOrderInfo(QueryOrderRequest req);
}
我们针对SamplesFacadeClient定义了两个注解@DubboFeignClient是类注解@DubboMethod是方法注解。
- 类注解中参数体现了调用系统方接口归属的类以及怎么处理这个类中所有方法的返参错误码情况。
- 方法注解中参数体现了下游提供方的方法名和方法入参、返参对象类型转换、接口的timeout、retries、loadbalance等属性情况。
仿照Spring类扫描
把SamplesFacadeClient设计好后之前调用下游提供方的代码现在只需要自己定义一个接口并添加上两种注解就好了接下来是使用这个接口。
按照上边的思路顺下来在integration层由一堆像SamplesFacadeClient这样的接口每个接口上还有两个注解在使用的时候可能这样写
@Autowired
private SamplesFacadeClient samplesClient;
然后可以直接使用samplesClient.queryRemoteInfo这样的方式调用方法。这个时候就有问题了samplesClient要想在运行时调用方法首先samplesClient必须得有一个实例化的对象可是我们根本没有SamplesFacadeClient接口的任何实现类那怎么把一个接口变成运行时的实例对象呢在这个具体例子里就是任何使变量samplesClient被@Autowired注解修饰后变成实例对象
@Autowired是Spring框架定义的在Spring框架中被注解修饰变量可能是原型实例对象也可能是代理对象所以该怎么把这个接口变成实例对象现在就有了答案可以想办法把接口变成运行时的代理对象。
了解Spring源码中的一个类org.springframework.context.annotation.ClassPathBeanDefinitionScanner这个类是Spring为了扫描一堆BeanDefinition而设计的目的就是要从@SpringBootApplication注解中设置过的包路径及其子包路径中的所有类文件中扫描出含有@Component、@Configuration等注解的类并构建BeanDefinition对象。
我们可以利用Spring这套扫描机制自定义扫描器类然后自定义扫描器类中自己手动构建BeanDefinition对象并且后续创建代理对象。
public class DubboFeignScanner extends ClassPathBeanDefinitionScanner {
// 定义一个 FactoryBean 类型的对象方便将来实例化接口使用
private DubboClientFactoryBean<?> factoryBean = new DubboClientFactoryBean<>();
// 重写父类 ClassPathBeanDefinitionScanner 的构造方法
public DubboFeignScanner(BeanDefinitionRegistry registry) {
super(registry);
}
// 扫描各个接口时可以做一些拦截处理
// 但是这里不需要做任何扫描拦截因此内置消化掉返回true不需要拦截
public void registerFilters() {
addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
}
// 重写父类的 doScan 方法并将 protected 修饰范围放大为 public 属性修饰
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 利用父类的doScan方法扫描指定的包路径
// 在此DubboFeignScanner自定义扫描器就是利用Spring自身的扫描特性
// 来达到扫描指定包下的所有类文件省去了自己写代码去扫描这个庞大的体力活了
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if(beanDefinitions == null || beanDefinitions.isEmpty()){
return beanDefinitions;
}
processBeanDefinitions(beanDefinitions);
return beanDefinitions;
}
// 自己手动构建 BeanDefinition 对象
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition = null;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition)holder.getBeanDefinition();
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
// 特意针对 BeanDefinition 设置 DubboClientFactoryBean.class
// 目的就是在实例化时能够在 DubboClientFactoryBean 中创建代理对象
definition.setBeanClass(factoryBean.getClass());
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
自定义DubboFeignScanner对象并且继承ClassPathBeanDefinitionScanner重写doScan方法接收包路径利用super.doScan让Spring帮助扫描指定包路径下的所有类文件。还可以手动在processBeanDefinitons方法中创建BeanDefinition对象。
这里扩展一个点以上是理想中的实现逻辑但是实际开发中可能经常发现指定的包路径下有一些其他类文件导致DubboFeignScanner.doScan方法扫描后不准确或者出现各种报错。可以借鉴Spring框架的处理思路Spring 源码在添加 BeanDefinition 时需要借助一个 org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#isCandidateComponent 方法来判断是不是候选组件也就是是不是需要拾取指定注解。
我们也重写isCandidateComponent方法判断一下如果扫描出来的类包含有@DubboFeignClient注解就添加BeanDefinition对象否则就不处理。
这样包含@DubboFeignClient注解的类的BeanDefiniton对象都被扫描收集起来接着Spring本身refresh方法中的org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons 方法进行实例化了而实例化的时候如果发现 BeanDefinition 对象是 org.springframework.beans.factory.FactoryBean 类型会调用 FactoryBean 的 getObject 方法创建代理对象。
针对接口进行代理对象的创建可以使用JDK中的java.lang.reflect,Proxy类可以这样创建代理对象
public class DubboClientFactoryBean<T> implements FactoryBean<T>, ApplicationContextAware {
private Class<T> dubboClientInterface;
private ApplicationContext appCtx;
public DubboClientFactoryBean() {
}
// 该方法是在 DubboFeignScanner 自定义扫描器的 processBeanDefinitions 方法中
// 通过 definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()) 代码设置进来的
// 这里的 dubboClientInterface 就等价于 SamplesFacadeClient 接口
public DubboClientFactoryBean(Class<T> dubboClientInterface) {
this.dubboClientInterface = dubboClientInterface;
}
// Spring框架实例化FactoryBean类型的对象时的必经之路
@Override
public T getObject() throws Exception {
// 为 dubboClientInterface 创建一个 JDK 代理对象
// 同时代理对象中的所有业务逻辑交给了 DubboClientProxy 核心代理类处理
return (T) Proxy.newProxyInstance(dubboClientInterface.getClassLoader(),
new Class[]{dubboClientInterface}, new DubboClientProxy<>(appCtx));
}
// 标识该实例化对象的接口类型
@Override
public Class<?> getObjectType() {
return dubboClientInterface;
}
// 标识 SamplesFacadeClient 最后创建出来的代理对象是单例对象
@Override
public boolean isSingleton() {
return true;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.appCtx = applicationContext;
}
}
代码中getObject是我们创建代理对象的核心过程我们还创建了一个DuboClientProxy对象这个对象放在java.lang,reflect.Proxy#newProxyInstance(java.lang.ClassLoader,java.lang.Class<?>[],**java.lang.reflect.InvocationHandler**)
方法中的第三个参数。
这意味着将来含有@DubboFeignClient注解的类的方法被调用的时候一定会出发调用DubboClientProxy类也就说我们可以在DubboClientProxy类拦截方法。
public class DubboClientProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 省略前面的一些代码
// 读取接口例SamplesFacadeClient上对应的注解信息
DubboFeignClient dubboClientAnno = declaringClass.getAnnotation(DubboFeignClient.class);
// 读取方法例queryRemoteOrderInfo上对应的注解信息
DubboMethod methodAnno = method.getDeclaredAnnotation(DubboMethod.class);
// 获取需要调用下游系统的类、方法、方法参数类型
Class<?> remoteClass = dubboClientAnno.remoteClass();
String mtdName = getMethodName(method.getName(), methodAnno);
Method remoteMethod = MethodCache.cachedMethod(remoteClass, mtdName, methodAnno);
Class<?> returnType = method.getReturnType();
// 发起真正远程调用
Object resultObject = doInvoke(remoteClass, remoteMethod, args, methodAnno);
// 判断返回码并解析返回结果
return doParse(dubboClientAnno, returnType, resultObject);
}
}
DubboClientProxy.invoke方法按照不变的代码流程从类注解、方法注解分别将变化的因素读取出来然后构建调用下游系统的请求对象并将请求对象传入下游系统的接口中然后接收返参并针对错误码进行判断最后转成自己的Bean对象。
这样就实现了一套代码解决了所有的integration层接口的远超调用简化了重复代码的开发简化代码。
Dubbo扫描原理机制
Dubbo源码中的DubboClassPathBeanDefinitionScanner这个类继承了ClassPathBeanDefinitionScanner充分利用了Spring的扩展性来实现自己的三个注解类org.apache.dubbo.config.annotation.DubboService、org.apache.dubbo.config.annotation.Service、com.alibaba.dubbo.config.annotation.Service然后完成对BeanDefinition对象的创建在完成Proxy代理对象的创建最后在运行时可以直接拿来使用。
不管是在系统中定义接口也好还是在自研框架中定义接口也好如果这些接口是同类性质的而且 Spring 还无法通过注解修饰接口直接使用的话都可以采取扫描机制统一处理共性逻辑将不变的流程逻辑下沉将变化的因素释放给各个接口。
学习来源极客时间 《Dubbo源码剖析与实战》 学习笔记 Day01