自动构造 DubboReference 和 DubboService 的 Spring Boot Starter 怎么实现?

前言

要写一个 Spring Boot Starter 很简单,只需要 META-INF 新建 spring.factories,并定义要初始化的 Configuration 类或者 Bean 类就可以了。难就难在组件不是只是创建一个 Bean 就完了,可能得根据规则创建一堆。我们这次就针对 DubboReference 和 DubboService 的使用来设计一个组件。他们都需要根据规则生成,而非固定几个对象。

目标

最原始使用 Reference 和 Service 的方式是手动把接口变成 DubboReference 对象,把 Bean 导出成 DubboService 对象。这个组件希望通过某种方式自动完成上面的步骤,并提供方便使用上面对象的方式。

下面会分别针对 DubboReference 和 DubboService 来分析

DubboReference 如何初始化和被引用

DubboReference 的特点是需要批量把接口变成对象,并提供引用方式。有点类似于 Mybatis 里面的 Mapper,是一种非常常见的需求。

针对引用方式有2种使用方式,这也决定了 DubboReference 如何初始化

  1. DubboReference 初始化成 Bean,然后利用 Spring 里面引用 Bean 的方式引用就可以了。例如 @Autowire
  2. 不转成Bean,通过自定义注解方式去引用,参考官方插件
java 复制代码
    @DubboReference(version = "1.0.0", url = "dubbo://127.0.0.1:12345")
    private DemoService demoService;

方式1: 转化成Bean

优点是使用比较简单,而且能用到 Bean 很多好用的特性,例如多种构造方式,AOP,通过各种处理器对 Bean 进行修改。

缺点是

  1. 每个 DubboReference 的个性化配置(例如timeout,retry等)需要配置在配置文件中,无法直接配置在注解上。
  2. 因为在启动的时候就需要初始化成 Bean,因此只能通过常见的指定 package 或者 Dubbo接口 选择要初始化成 Bean 的 Reference。

从 BeanDefinition 到 Bean

普通的 Bean 会根据 BeanDefinition 的 BeanClass 去构造对象,再执行拓展点完善 Bean,例如注入依赖,执行 @PostConstruct 的方法等等。

有2个阶段可以对 Bean 进行处理

  1. 修改 BeanDefinition 的属性,Bean 工厂会根据属性来构造 Bean,例如根据 BeanClass 构造对象,根据 InitMethod 执行初始化方法等等
  2. 自定义拓展点修改 Bean 例如 BeanPostProcessor,InitializingBean 等等

如何转换成 Bean

因为 DubboReference 是接口,因此有2种方式构造 Bean,一种是通过 CGLib 或者 javassist 等字节码修改工具去构造普通的类,才能给 Bean 去初始化。这种方式用的不多,毕竟通过字节码去构建 Class 比较复杂。因此 Spring 提供了一种更好的方式,那就是使用 FactoryBean。

FactoryBean 和 BeanFactory 是不同的。BeanFactory 是真正的 Bean 工厂,会创建很多Bean,Spring 里面最核心的就是 DefaultListableBeanFactory,大部分 Bean 都是由它创建,而 FactoryBean 也是工厂,但它只生成一个 Bean。一个 FactoryBean 对应 2 个Bean,一个是它自己,一个是通过 FactoryBean 的 getObject 方法创建的 Bean。

FactoryBean 的好处在于可以自己构造代理对象,而非交由 Spring 基于 Class 来构建。而且还能在构造过程中注入其他 Bean,方便构造代理对象。

如何构造 FactoryBean

前面提到要成为一个 Bean,必须先有 BeanDefinition,因此需要先构造 BeanDefinition。FactoryBean 也是一种 Bean,只是这个 Bean 的 Class 必须要继承 org.springframework.beans.factory.FactoryBean,泛型 T 就是由这个工厂创建出来的 Bean 的类型。

java 复制代码
public class DubboReferenceFactoryBean implements FactoryBean<Object> {
    // objectType 和 ReferenceManager 需要注入
    private ReferenceManager referenceManager;
  
    private Class<?> objectType;
    @Override
    public Object getObject() throws Exception {
        return referenceManager.refer(getObjectType());
    }
    @Override
    public Class<?> getObjectType() {
        return objectType;
    }
    @Override
    public boolean isSingleton() {
        return true;
    }
    
    public void setReferenceManager(ReferenceManager referenceManager) {
        this.referenceManager = referenceManager;
    }

    public void setObjectType(Class<?> objectType) {
        this.objectType = objectType;
    }
}
   
@Service
public class ReferenceManager {
    private ConcurrentMap<Class, Object> referenceConfigs = new ConcurrentHashMap<>();

    @Autowired
    private ApplicationContext applicationContext;

    public <T> T refer(Class<T> clazz) {
        if (referenceConfigs.containsKey(clazz)) {
            return (T) referenceConfigs.get(clazz);
        } else {
            T referenceBean = referenceBean(clazz);
            referenceConfigs.put(clazz, referenceBean);
            return referenceBean;
        }
    }


    private <T> T referenceBean(Class<T> clazz) {
        ReferenceConfig<T> referenceConfig = new ReferenceConfig<>();
        referenceConfig.setInterface(clazz);
        referenceConfig.setApplication(applicationContext.getBean(ApplicationConfig.class));
        // 省略根据配置对 referenceConfig 的属性进行修改
        try {
            referenceConfig.get();
        } catch (Exception e) {
            throw new RuntimeException("初始化 reference 失败", e);
        }
        return referenceConfig.get();
    }

}

要得到 FactoryBean 需要先构造出 FactoryBean 的 BeanDefinition

构造 BeanDefinition 有几种

  1. ClassPathBeanDefinitionScanner 自动扫描 BeanDefinition
  2. BeanDefinitionRegistryPostProcessor 动态注册 BeanDefinition

使用 ClassPathBeanDefinitionScanner 最方便,因为他提供了很多模板方法,例如 Class 搜索,Class 过滤,BeanDefinition 初始化的方法,所以用起来最方便。

只要看看 ClassPathBeanDefinitionScanner 里面的 doScan 方法就知道这个类的作用,和怎么修改里面的模板方法了

java 复制代码
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
    for (String basePackage : basePackages) {
       Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
       for (BeanDefinition candidate : candidates) {
          ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
          candidate.setScope(scopeMetadata.getScopeName());
           // 生成新的 BeanName
          String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
          if (candidate instanceof AbstractBeanDefinition) {
            // 可以对 BeanDefinition 进行处理
             postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
          }
          if (candidate instanceof AnnotatedBeanDefinition) {
             AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
          }
           // 判断是否要注册 BeanDefinition
          if (checkCandidate(beanName, candidate)) {
             BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
             definitionHolder =
                   AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
             beanDefinitions.add(definitionHolder);
             registerBeanDefinition(definitionHolder, this.registry);
          }
       }
    }
    return beanDefinitions;
}

最理想的情况就是 package 里面只有 Reference 接口,就不需要过滤,只要重写 postProcessBeanDefinition 就可以了。

在 DubboReferenceFactoryBean 里面需要注入 referenceManager,objectType。因为 DubboReferenceFactoryBean 是通过 BeanDefinition 构造的,这时候无法自动根据类型注入,因为 referenceManager 可能都还没构造出来。现在还处于收集 BeanDefinition 的阶段。因此只能通过 BeanDefinition 里面的属性来注入。

java 复制代码
public class DubboReferenceClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
    @Override
    protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) {
       
        try {
             //扫描后的 beanClass 是接口, FactoryBean 在 getObject 的时候需要用接口来获取 DubboReference
            //所以 FactoryBean 的构造方法需要传入接口类型
            ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
            constructorArgumentValues.addGenericArgumentValue(Class.forName(beanDefinition.getBeanClassName()));
            beanDefinition.setConstructorArgumentValues(constructorArgumentValues);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("获取bean class 失败", e);
        }


        //因为扫描的时候 referenceManager 都还没创建, 所以只能用 RuntimeBeanReference 来引用
        beanDefinition.getPropertyValues().add("referenceManager",new RuntimeBeanReference(("referenceManager")));

        //修改 beanClass 为 DubboReferenceFactoryBean 才能被 Spring 初始化
        beanDefinition.setBeanClass(DubboReferenceFactoryBean.class);
    
        super.postProcessBeanDefinition(beanDefinition, beanName);
    }
    
    ```
@Override
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    return true;
}

@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    return true;
}

}

最后通过 ImportBeanDefinitionRegistrar 来触发扫描,这时也可以根据自定义注解来设置扫描类和一些参数。

java 复制代码
    
public class DubboReferenceScannerRegistrar implements ImportBeanDefinitionRegistrar  {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(DubboReferenceScan.class.getName()));
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        scanner.scan(annoAttrs.getStringArray("basePackages"));
    }
}

  
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 注解和注册器绑定,只需要在 Spring Boot 启动类加上这个注解即可,类似 Mybatis 的 @MapperScan
@Import(DubboReferenceScannerRegistrar.class)
public @interface  DubboReferenceScan {
    
    // 扫描的包,也可以自定义自己的属性,例如 timeout 等
    String[] basePackages() default {};
}

如何通过 FactoryBean 生成 DubboReferenceBean

spring 会自动处理调用 FactoryBean 的 getObject 的方法,并注入成 Bean。后续就可以直接和普通的 Bean 一样使用和被处理了。

方式2: 自定义注解方式去引用 (官方组件使用)

优点是 DubboReference 的配置可以直接配置在注解里面,改配置非常方便。且只有在注解配置的 Dubbo 接口才会生成 DubboReference,不需要再像上面的方法一样需要指定 package 或者 Dubbo 接口。

缺点是需要引入新的注解,引入三方组件依赖,且需要自己处理注入问题。

通过 BeanPostProcessor 手动注入

自定义一个 BeanPostProcessor,遍历所有的 bean 的所有属性和 setter 方法,发现属性或者 setter 方法配置了指定注解,把 Reference 设置进去。

dubbo 官方组件为什么选择方式2

主要有几个原因 1. 因为项目一般会依赖外部的 Api,导致 Api 的包路径和项目路径不同, 需要单独配置package 2. 每个 Reference 都有很多个性化配置 3. 一个 package 的 Api 太多, 但实际上就小部分用上

而 Mybatis 的 Starter 则不会有上面三个问题, 因此选择的是方式1

java 复制代码
@Service
public class ReferenceBeanFactoryPostProcessor : BeanPostProcessor, DisposableBean {

    override fun postProcessBeforeInitialization(bean: Any, beanName: String?): Any {
        var clazz: Class<*> = bean.javaClass
        if (AopUtils.isAopProxy(bean)) {
            // 解决被 AOP 之后类获取异常的问题
            clazz = AopUtils.getTargetClass(bean)
        }

        val fields = clazz.getDeclaredFields()
            // 这里忽略了 setter 方法的初始化
        for (field in fields) {
            try {
                if (!field.isAccessible) {
                    field.setAccessible(true)
                }
                val reference = field.getAnnotation(Reference::class.java)
                if (reference != null) {
                    val value = refer(reference, field.type)
                    field[bean] = value
                }
            } catch (e: Exception) {
                throw BeanInitializationException(
                    "Failed to init remote service reference at filed " + field.name + " in class " + bean.javaClass.getName(),
                    e
                )
            }
        }
        return bean
    }

实现比较暴力,性能估计会有一定影响(这个没测),但实现逻辑其实还是挺简单的。

官方组件 dubbo-spring-boot-project 实现方式和方式 2 类似,但会有些差异,代码会优雅一些但也复杂一些。有兴趣可以自己看源码 dubbo-config-spring

简单来说就是使用一种特殊的 BeanPostProcessor 也就是InstantiationAwareBeanPostProcessorAdapter。 通过 postProcessPropertyValues 方法去每个 Bean 收集 InjectionMetadata,最后再统一注入。

和方法2 一样通过 BeanPostProcessor 来周到自定义注解对应的属性或者setter,但这时候不自动构造 DubboReference,而是通过 beanFactory.registerBeanDefinition(fieldBeanName,beanDefinition) 来注册 FactoryBean,走一遍 Bean 的流程。优点是配置方便,也能依赖 Bean 的特性,缺点是需要自己手动注入。

DubboService 如何初始化和被引用

DubboService 本来就是一个 Bean,只是这个 Bean 还需要被 export。而 export 需要等到 Bean 初始化完才能提供服务。要标识这个 Bean 需要 export 需要一个特定的注解。

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
//@Component 注解非常重要,因为没有这个默认 ClassPathBeanDefinitionScanner 不认这个注解
//需要自定义 ClassPathBeanDefinitionScanner 来添加,会非常麻烦
@Component 
public @interface DubboService {
    int timeout() default 0;
    // 下面省略一堆 DubboService 的配置
}

这里也有2种方式

  1. 自定义 BeanPostProcessor
  2. 自定义 ApplicationListener

方式1: 自定义 BeanPostProcessor

优点: 可以及时发现 export 失败,结束进程 缺点: 可能会过早 export,导致处理请求失败

java 复制代码
public class DubboServiceFactoryProcessor implements BeanPostProcessor, DisposableBean {

    private ConcurrentHashSet<ServiceConfig<Object>> serviceConfigs = new ConcurrentHashSet<>();

    @Override
    public void destroy() {
        for (ServiceConfig<Object> serviceConfig : serviceConfigs) {
            serviceConfig.unexport();
        }
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        DubboService dubboService = getActualClass(bean).getAnnotation(DubboService.class);

        if (dubboService == null) {
            return bean;
        }

        ServiceConfig serviceConfig = new ServiceConfig<>();
        serviceConfig.setRef(bean);
        // 省略一堆根据 dubboService 来生成 ServiceConfig 的设置
        serviceConfig.export();

        serviceConfigs.add(serviceConfig);
        return bean;
    }

    private Class<?> getActualClass(Object bean) {
        Class<?> clazz = bean.getClass();
        if (AopUtils.isAopProxy(bean)) {
            clazz = AopUtils.getTargetClass(bean);
        }
        return clazz;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

方式2: 自定义 ApplicationListener (更稳一些)

优点: 等所有 Bean 都准备就绪之后再初始化,避免在未启动完就接收请求。 缺点: 如果 export 异常需要等到项目启动才能发现。

就不再提供代码了,只是通过 ApplicationContext 根据注解获取所有的 Bean,然后统一 export。

总结

Spring 其实提供了非常多的拓展点来给我们去写组件,但中间的坑也有很多,包括 Bean 的初始化顺序导致拓展点的执行顺序,Bean 的状态,Bean 的依赖等等。只有对 Bean 的生命周期比较熟,能看源码,且知道不同拓展点的特点和常见的组件编写方式,才能写好组件。

相关推荐
希冀12321 分钟前
【操作系统】1.2操作系统的发展与分类
后端
GoppViper1 小时前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文2 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people2 小时前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
罗政7 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师9 小时前
spring获取当前request
java·后端·spring
Java小白笔记10 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
JOJO___12 小时前
Spring IoC 配置类 总结
java·后端·spring·java-ee
白总Server13 小时前
MySQL在大数据场景应用
大数据·开发语言·数据库·后端·mysql·golang·php
Lingbug14 小时前
.Net日志组件之NLog的使用和配置
后端·c#·.net·.netcore