Spring 扩展实战,自动创建 RPC 框架代理对象

 关注公众号【1024个为什么】,及时接收最新推送文章! 

本文内容:

1、背景

2、实现方案

3、注入失败了?

4、排查解决

5、更优方案

事情的起因是想研究一下,能不能把公司自研 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 做二次过滤(第一次是根据自定义的注解),我这里重写后只要接口类型的。源码中在下图红框位置会调用到:

03a395652003191572b66320844e3d90.png

| 定义一个后置处理器

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

BeanDefinitionRegistryPostProcessor可以干预生成 bd 的过程, 添加自定义 bd
BeanFactoryPostProcessor可以干预 bd 里的属性值,进而影响最终生成 bean 的方式
BeanPostProcessor可以干预 bean 的实例化和初始化

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

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

fce6f4faf8e033702bf804ad2cbad707.png

上代码:

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

启动看一下效果

448068326013e14fb78c9c8def747f13.png

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

||  注入失败了?

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

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

启动报错

d5a55d8697bb919a4776c2d7d2806041.png

具体报错原因

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

57082c58496f83d7b0a7f4cc61cf122f.png

正常流程应该是:

  1. 先从所有已创建对象的名字(org.springframework.beans.factory.support.DefaultListableBeanFactory#allBeanNamesByType)中找;

  2. 找不到,再从所有的 bd 中找;

  3. 找到可以创建出这个类型对象的 bd,返回这个 bd 的名字;

  4. 后面再用这个 bd 去创建对象。

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

f91ac7a87b86f2d50469cd7625a0d07f.png

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

abd714cca6ba10d82313447c20b11896.png

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

1f9bae7fb1381d427ee8003e8fe0eb27.png

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

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

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

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

1eedc32ae336896e629b6203a2bd6138.png

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

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

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

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

| 瞒天过海

真的无解了?当然不是。

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

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

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

64d06ea5787ea6499924e5b7bacb1175.png

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

| 成功

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

d1aed22dcf23cb6c8087110a4fd221ad.png

||  更优方案

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

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

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

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

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

扯两句

目的性很强才容易做成一件事

解决问题,要学会变通思路

留个思考,这算不算 Spring 的 bug 呢?如果是你,会怎么改造 Spring 呢?

我的思路:

如果 bd 创建出的对象类型是泛型(Object,细想一下,谁会在项目中让 Spring 创建一个 Object 对象呢),能不能先试着创建一次对象,用创建出来的和要注入的对象做一下类型比较,如果相同就使用,不同再报错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值