事情的起因是想研究一下,能不能把公司自研 RPC 框架和 Spring 完美整合一下。
|| 背景
我司使用的是自研的 RPC 框架名字叫 DSF,和 Spring 结合的不是很完美,项目中用到其他服务的 client 实例时,只能先通过框架提供的代理工厂类创建出所依赖的 client 的实例后才能使用。代码风格都是下面这样的,不太优雅。
public class DSFClient {
private static IOrderClient orderClient = DSFProxyFactory.create(IOrderClient.class, IOrderClient.URL);
public static IOrderClient getOrderClient() {
return orderClient;
}
}
@Component
public class UserService{
public Order getOrderByUid(Long uid){
Order order = DSFClient.orderClient.getOrderByUid(uid);
...
}
}
对这块一直就有疑问,为什么不和 Spring 深度结合一下,使用其他服务的 client 就像使用 Spring 本地 Component 一样简单,不做任何配置,直接使用 @Autowire 注入就可以。就像下面这样:
@Component
public class UserService{
@Autowire
IOrderClient orderClient;
public Order getOrderByUid(Long uid){
Order order = orderClient.getOrderByUid(uid);
...
}
}
听老同事说好像是各种历史原因,不太好实现了。
虽说一直有疑问,但如果让我来解决这个问题,我还是很胆怵的,觉得扩展 Spring 都是大牛搞的,我肯定搞不定。
正好最近组内共同学习的专题是 Spring ,就想着趁着这次学习,看看能不能把这个问题解决了,正好也是对这次学习成果的一个小小的检验。
想要解决这个问题,对 Spring 要有一定的了解,需要知道以下知识:
- 几类不同后置处理器的作用
- Spring 扫描加载 class 的时间节点
- 如何自定义一个扫描器
- BeanDefinition 的修改(下文简称 bd)
- Spring 管理的类实例化的顺序
- 属性注入时,遇到没有实例化的类型是如何先去实例化的
|| 实现方案
和 Spring 框架的整合,无非就是把无法通过 Spring 配置直接扫描管理的类,通过 Spring 提供的扩展功能,加入到 Spring 容器中。
| 思路
- 自定义一个扫描器,可以只扫描业务包规则路径下带有DSF注解的类
- 提供一个后置处理器,去触发扫描器,得到想要的 BeanDefinition
- 修改得到的BD,是其能够使用框架提供的代理工厂生产出对象
| 自定义扫描器
public class DsfServiceScanner extends ClassPathBeanDefinitionScanner {
public DsfServiceScanner(BeanDefinitionRegistry registry) { super(registry);}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
return super.doScan("com.daojia");
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface();
}
}
isCandidateComponent 方法作用是对扫描到的 class 做二次过滤(第一次是根据自定义的注解),我这里重写后只要接口类型的。源码中在下图红框位置会调用到:
| 定义一个后置处理器
我把 Spring 中的后置处理器分为 3 类:
BeanDefinitionRegistryPostProcessor | 可以干预生成 bd 的过程, 添加自定义 bd |
BeanFactoryPostProcessor | 可以干预 bd 里的属性值,进而影响最终生成 bean 的方式 |
BeanPostProcessor | 可以干预 bean 的实例化和初始化 |
根据我们这次的需求,很显然应该使用 BeanDefinitionRegistryPostProcessor。
根据Spring的源码也可以发现,会先执行 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry() 方法。
上代码:
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
DsfServiceScanner dsfServiceScanner = new DsfServiceScanner(registry);
// 添加扫描过滤的注解 @DSFServiceContract
dsfServiceScanner.addIncludeFilter(new AnnotationTypeFilter(DSFServiceContract.class));
// 启动扫描器,扫描 DSF client 包
Set<BeanDefinitionHolder> holders = dsfServiceScanner.doScan();
// 更新 bd
updateBeanDefinition(holders);
}
| 修改bd
private void updateBeanDefinition(Set<BeanDefinitionHolder> holders){
for (BeanDefinitionHolder holder : holders) {
ScannedGenericBeanDefinition bd = (ScannedGenericBeanDefinition)holder.getBeanDefinition();
bd.setConstructorArgumentValues(getConstructArgValues(bd));
bd.setFactoryMethodName("create");
bd.setBeanClass(DSFProxyFactory.class);
}
}
这里替换了 beanClass、factoryMethodName,后面 Spring 根据 bd 创建对象时,就会通过 DSFProxyFactory.create(Class cls, Strig url) 方法创建对象。
Spring 默认是通过它自己推测出的构造方法去创建对象的,推测过程极为复杂。
启动看一下效果
根据 name、type 都可以获取到,说明 Spring 已经可以自动扫描并创建带有 @DSFServiceContract
的类。
|| 注入失败了?
既然 Spring 已经可以帮我创建我所需要的对象了,那就赶紧注入到业务类中看看效果吧。
@Component
public class OrderService {
@Autowired
private PayPlatformAgent payPlatformAgent;
public PayPlatformAgent getPayPlatformAgent() {
return payPlatformAgent;
}
}
启动报错
具体报错原因
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'orderService':
Unsatisfied dependency expressed through field 'payPlatformAgent';
nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'com.daojia.jz.payplatform.contract.PayPlatformAgent' available:
expected at least 1 bean which qualifies as autowire candidate.
Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
原来是注入的时候,没找到 payPlatformAgent ,可是刚才上面明明已经可以从容器中获取到 bean 了。
猜测可能是对象创建顺序的问题,但就算注入的时候 payPlatformAgent 还没有创建,按 Spring 流程,也会先把依赖的对象创建出来,然后再注入。
所以问题的关键是为什么没找到之后,又没有去创建对象?
|| 排查解决
| 排查过程
带着上面的问题,debug 看一下具体原因。
可以看到注入 payPlatformAgent 时, 该对象还没创建。
正常流程应该是:
- 先从所有已创建对象的名字(org.springframework.beans.factory.support.DefaultListableBeanFactory#allBeanNamesByType)中找;
- 找不到,再从所有的 bd 中找;
- 找到可以创建出这个类型对象的 bd,返回这个 bd 的名字;
- 后面再用这个 bd 去创建对象。
继续看为什么没有去创建,核心在这个方法里,看看为什么有对应的 bd ,却没有匹配上。
这里会得到该 bd 能生成的 bean 的类型,但为什么是 Object ?不应该是 PayPlatformAgent 吗。
正是由于这里返回的 Object,所以下面这里才会匹配失败
进而导致后面没有可用的 bd 来创建对象。
查看 bd 和 DSFProxyFactory.create()
public static <T> T create(Class<?> type, String strUrl) {
return create(type, strUrl, false);
}
create() 方法返回的是泛型,而在前面我们对 bd 的 beanClass 由 PayPlatformAgent 替换成了 DSFProxyFactory,所以 Spring 根据 bd 的 beanClass 和 factoryMethodName 推断出此 bd 能创建出类型为 Object 的对象,也就是 RootBeanDefinition 里的 resolvedTargetType 。
| 解决过程
找到了原因,怎么解决这个问题呢?
Spring 匹配的规则是死的,这部分我们改不了,改 bd 里的 beanClass ? 当然也不行,我们拿到的业务类只是一个接口。
这条路走不通,就只能换个角度考虑,能不能把我自己扫描的类先创建出对象来?这样在注入时,在单例池中就能找到。
问题又来了,怎么能提前创建我自己扫描类的对象呢?
我们先看看创建对象的顺序
其实创建对象的顺序,就是遍历 beanDefinitionNames 的顺序,但是 beanDefinitionNames 的顺序又是怎么来的呢?
这就得追溯到扫描 class 生成 bd 时了,说白了,就是先扫描到的先加入这个 List 。
看着似乎有解决方案了,我可以先扫描我自己的类不就行了吗。
很遗憾,这条路还是走不通,因为 Spring 会先处理扫描 配置类 上配置的包路径,也就是先执行 ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry() 我们自定义的后置处理器,是在它后面执行的。
| 瞒天过海
真的无解了?当然不是。
我们可以采用一个讨巧的办法,我称之为【瞒天过海】。
扫描我自己的类之前,把 Spring 已经扫描出的 bd 先搞出来放到其他地方,等扫描我们自己的类之后,再把刚才挪走的 bd 再追加回去。这样就可以骗过 Spring,造成的假象就是先扫描了我自己的类。
看看代码的实现,修改我们自定义的后置处理器
移除的时候要注意,Spring 几个内置类的 bd 是不能移除的,它们是在刚启动时就添加进来的,要先被实例化的,所以我加了 "springframework" 关键字的过滤。
| 成功
经过上面的各种骚操作,终于可以成功注入了。
|| 更优方案
虽说功能已经实现,但方案还是不够优美,总不能每个项目里都要加上这一堆代码吧。
所以更好的解决方案是把这部分功能集成到我们自研的框架中,采用 MyBatis @MapperScan 的方案优先扫描自己的类。
但还有个现实的问题,就是我们发布接口时,没有统一 URL 的提供方式,起各种名字的都有,还有没有的。没有规律就没办法统一获取到 URL 当做创建对象的参数。如果可以的话 在@DSFServiceContract 中提供一个 URL 属性,发布接口时这个是必填的。
所以以上方案暂时还只能停留在纸面上。
不得不感叹一句,约定大于开发啊!