事情的起因是想研究一下,能不能把公司自研 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 要有一定的了解,需要知道以下知识:

  1. 几类不同后置处理器的作用
  2. Spring 扫描加载 class 的时间节点
  3. 如何自定义一个扫描器
  4. BeanDefinition 的修改(下文简称 bd)
  5. Spring 管理的类实例化的顺序
  6. 属性注入时,遇到没有实例化的类型是如何先去实例化的

||  实现方案

和 Spring 框架的整合,无非就是把无法通过 Spring 配置直接扫描管理的类,通过 Spring 提供的扩展功能,加入到 Spring 容器中。

| 思路

  1. 自定义一个扫描器,可以只扫描业务包规则路径下带有DSF注解的类
  2. 提供一个后置处理器,去触发扫描器,得到想要的 BeanDefinition 
  3. 修改得到的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 做二次过滤(第一次是根据自定义的注解),我这里重写后只要接口类型的。源码中在下图红框位置会调用到:

Java 常用RPC框架 spring rpc框架_java

| 定义一个后置处理器

我把 Spring 中的后置处理器分为 3 类:

BeanDefinitionRegistryPostProcessor

可以干预生成 bd 的过程, 添加自定义 bd

BeanFactoryPostProcessor

可以干预 bd 里的属性值,进而影响最终生成 bean 的方式

BeanPostProcessor

可以干预 bean 的实例化和初始化

根据我们这次的需求,很显然应该使用 BeanDefinitionRegistryPostProcessor。

根据Spring的源码也可以发现,会先执行  BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry() 方法。

Java 常用RPC框架 spring rpc框架_java_02

上代码:

@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 默认是通过它自己推测出的构造方法去创建对象的,推测过程极为复杂。

启动看一下效果

Java 常用RPC框架 spring rpc框架_vue_03

根据 name、type 都可以获取到,说明 Spring 已经可以自动扫描并创建带有 @DSFServiceContract
的类。

||  注入失败了?

既然 Spring 已经可以帮我创建我所需要的对象了,那就赶紧注入到业务类中看看效果吧。

@Component
public class OrderService {
  @Autowired
  private PayPlatformAgent payPlatformAgent;
  public PayPlatformAgent getPayPlatformAgent() {
    return payPlatformAgent;
  }
}

启动报错

Java 常用RPC框架 spring rpc框架_Java 常用RPC框架_04

具体报错原因

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  时, 该对象还没创建。

Java 常用RPC框架 spring rpc框架_spring boot_05

正常流程应该是:

  1. 先从所有已创建对象的名字(org.springframework.beans.factory.support.DefaultListableBeanFactory#allBeanNamesByType)中找;
  2. 找不到,再从所有的 bd 中找;
  3. 找到可以创建出这个类型对象的 bd,返回这个 bd 的名字;
  4. 后面再用这个 bd 去创建对象。

继续看为什么没有去创建,核心在这个方法里,看看为什么有对应的 bd ,却没有匹配上。

Java 常用RPC框架 spring rpc框架_spring_06

这里会得到该 bd 能生成的 bean 的类型,但为什么是 Object ?不应该是 PayPlatformAgent 吗。

Java 常用RPC框架 spring rpc框架_java_07

正是由于这里返回的 Object,所以下面这里才会匹配失败

Java 常用RPC框架 spring rpc框架_java_08

进而导致后面没有可用的 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 ? 当然也不行,我们拿到的业务类只是一个接口。

这条路走不通,就只能换个角度考虑,能不能把我自己扫描的类先创建出对象来?这样在注入时,在单例池中就能找到。

问题又来了,怎么能提前创建我自己扫描类的对象呢?

我们先看看创建对象的顺序

Java 常用RPC框架 spring rpc框架_Java 常用RPC框架_09

其实创建对象的顺序,就是遍历 beanDefinitionNames 的顺序,但是 beanDefinitionNames  的顺序又是怎么来的呢?

这就得追溯到扫描 class 生成 bd 时了,说白了,就是先扫描到的先加入这个 List 。

看着似乎有解决方案了,我可以先扫描我自己的类不就行了吗。

很遗憾,这条路还是走不通,因为 Spring 会先处理扫描 配置类 上配置的包路径,也就是先执行 ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry() 我们自定义的后置处理器,是在它后面执行的。

| 瞒天过海

真的无解了?当然不是。

我们可以采用一个讨巧的办法,我称之为【瞒天过海】。

扫描我自己的类之前,把 Spring 已经扫描出的 bd 先搞出来放到其他地方,等扫描我们自己的类之后,再把刚才挪走的 bd 再追加回去。这样就可以骗过 Spring,造成的假象就是先扫描了我自己的类。

看看代码的实现,修改我们自定义的后置处理器

Java 常用RPC框架 spring rpc框架_spring boot_10

移除的时候要注意,Spring 几个内置类的 bd 是不能移除的,它们是在刚启动时就添加进来的,要先被实例化的,所以我加了 "springframework" 关键字的过滤。

| 成功

经过上面的各种骚操作,终于可以成功注入了。

Java 常用RPC框架 spring rpc框架_vue_11

||  更优方案

虽说功能已经实现,但方案还是不够优美,总不能每个项目里都要加上这一堆代码吧。

所以更好的解决方案是把这部分功能集成到我们自研的框架中,采用 MyBatis @MapperScan 的方案优先扫描自己的类。

但还有个现实的问题,就是我们发布接口时,没有统一 URL 的提供方式,起各种名字的都有,还有没有的。没有规律就没办法统一获取到 URL 当做创建对象的参数。如果可以的话 在@DSFServiceContract 中提供一个 URL 属性,发布接口时这个是必填的。

所以以上方案暂时还只能停留在纸面上。

不得不感叹一句,约定大于开发啊!