赞
踩
Dubbo框架常被当作第三方框架集成到应用中,当Spring集成Dubbo框架后,为什么在编写代码的时候,只用了@DubboReference注解就可以调用提供方的服务了呢?这篇笔记就是分析Dubbo框架是怎么与Spring结合的。
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); } }
思路:
queryRemoteOrder方法封装了调用下游接口的逻辑,先构建调用下游系统的对象,然后把对象传入下游系统的接口中,再接收返回值并针对错误码判断,最后转成自己的Bean对象。
这个地方就可以做优化,封装一下,屏蔽下游提供方各种接口的差异性,减少重复性的代码编写。
顺着调用链路分析有哪写变量因素在封装的时候需要考虑:
抽象是把相似流程的骨架抽象出来,简单来说就是去掉表象,保留相对不变的。
一段代码的流程,可以是业务流程,也可以是代码流程,还可以是调用流程,当然本质都是一小块相对聚集的业务逻辑的核心主干流程,把不变的流程固化下来变成模板,然后把变化的因素交给各个调用方,意在求同存异,追求不变的稳定,放任变化的自由。
结合上班的例子,不变的是重复写的那段调用逻辑,先构建调用下游系统的请求对象,并将请求对象传入下游系统的接口中,然后接收返参并针对错误码进行判断,最后转成自己的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是方法注解。
把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源码中的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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。