写点什么

源码解析:Dubbo3 的 Spring 适配原理与初始化流程

作者:Apache Dubbo
  • 2022-12-05
    浙江
  • 本文字数:5038 字

    阅读完需:约 17 分钟

Dubbo 国内影响力最大的开源框架之一,非常适合构建大规模微服务集群的,提供开发框架、高性能通信、丰富服务治理等能力。同时 Dubbo 无缝支持 Spring、Spring Boot 模式的开发,这篇文章帮助大家理解 Dubbo 是怎么和 Spring 做集成的,非常适合关心原理是先的开发者。

感兴趣的朋友可以直接访问官网体验 Spring+Dubbo 开发微服务 或搜索关注官方微信公众号:Apache Dubbo

Spring Context Initialization

首先,我们先来看一下 Spring context 初始化主要流程,如下图所示:



相关代码:org.springframework.context.support.AbstractApplicationContext#refresh()


简单描述一下每个步骤包含的内容:


  1. 创建 BeanFactory:读取加载 XML/注解定义的 BeanDefinition。

  2. prepareBeanFactory: 注册提前加载的各种内置 post-processor 以及环境变量等。

  3. invokeBeanFactoryPostProcessors: 加载 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor。注意这里的加载顺序比较复杂,还涉及到多次加载,详细请查看代码。 常用于加载较早初始化的组件,如属性配置器 PropertyPlaceholderConfigurer 和 PropertySourcesPlaceholderConfigurer。 还有一个比较重要的 ConfigurationClassPostProcessor 实现了 BeanDefinitionRegistryPostProcessor 接口,用于加载 @Configuration 类,解析 @Bean 定义并注册 BeanDefinition。 这个阶段常用于注册自定义 BeanDefinition。

  4. registerBeanPostProcessors: 加载并注册各种 BeanPostProcessor,常用于修改或包装(代理)bean 实例,如 Seata 的 GlobalTransactionScanner。

  5. registerListeners: 加载并注册 ApplicationListener,处理 earlyApplicationEvents

  6. finishBeanFactoryInitialization: 注册 EmbeddedValueResolver,冻结配置

  7. preInstantiateSingletons: 遍历加载单例 bean,也就是加载普通的 bean,包括 @Controller, @Service, DAO 等。

FactoryBean

Spring 容器支持两种 bean:普通 bean 和工厂 bean(FactoryBean)。我们经常写的 @Controller/@Service 这种被 Spring 直接初始化的 bean 就是普通 bean, 而 FactoryBean 则是先由 Spring 先创建 FactoryBean 实例,然后由其再创建最终的 bean 实例。


[Spring BeanFactory] --create---> [XxxFactoryBean instance] --create--> [Final Bean Instance]FactoryBean接口如下:
复制代码


public interface FactoryBean<T> {  /**   * Return an instance (possibly shared or independent) of the object managed by this factory.   */  T getObject() throws Exception;
/** * Return the type of object that this FactoryBean creates, or null if not known in advance. * This allows one to check for specific types of beans without instantiating objects, for example on autowiring. */ Class<?> getObjectType();
}
复制代码

BeanDefinition

Spring bean 分为注册和创建实例两大阶段。将从 Spring XML/注解解析到的 bean 信息放到 BeanDefinition,然后将其注册到 BeanFactory,后面会根据 BeanDefinition 来初始化 bean 实例。 不管是普通 bean 还是工厂 bean,都是先注册 bean definition,然后按照依赖顺序进行初始化。


两者 BeanDefinition 的差异是:


  • 普遍 bean 的 BeanDefinition 的 beanClassName 为最终 bean 的 class

  • 工厂 bean 的 BeanDefinition 的 beanClassName 为工厂 bean 的 class


注册 Bean 主要有几种方式:


  1. 在 Spring XML 中定义< bean />,由 Spring 解析生成并 BeanDefinition

  2. 在 Spring java config 中声明 @Bean 方法,由 Spring 解析生成并 BeanDefinition

  3. 调用 BeanDefinitionRegistry.registerBeanDefinition()方法手工注册 BeanDefinition

  4. 通过 SingletonBeanRegistry.registerSingleton()方法注册 bean 实例。


注意:注册 bean 实例与前面三种注册 BeanDefinition 有本质的区别。 打个比方,注册 BeanDefinition 是新儿子,Spring 会管理 bean 的初始化及依赖注入及解决属性占位符,调用 BeanPostProcessor 进行处理等。 而注册 bean 实例就是别人的儿子,Spring 将其视为已经完成初始化的 bean,不会解决其依赖和属性占位符。后面会讲到 Dubbo 2.7/3 两个版本 Reference 注解注册 bean 的差异。

初始化 bean

创建 bean 大概有下面几个步骤:


  • 创建实例 createBeanInstance

  • 解决依赖 resolveDependency

  • 解决属性占位符 applyPropertyValues


其中多次调用 BeanPostProcessor 进行处理,如果某些 BeanPostProcessor 此时还没注册,则可能导致遗漏处理了当前的 bean。 后面会讲到 dubbo 2.7 中提前加载 config bean 导致的一系列问题。


其中关键逻辑请参考代码:AbstractAutowireCapableBeanFactory#doCreateBean()

解决依赖

在 Spring 注解流行起来之后,通常是使用 @Autowire 注解来注入依赖的 bean。此种注入方式大概的流程如下:


  • 查找匹配属性类型的 beanName 列表

  • 根据 @Qualifier/@Primary/propertyName 等选择合适的 bean 关键逻辑请参考代码:DefaultListableBeanFactory#doResolveDependency()。


其中第一步,查找匹配类型的 beanName 列表时会调用 ListableBeanFactory#getBeanNamesForType()来枚举检查所有的 beanDefinition。 检查 bean type 的逻辑请查看 AbstractBeanFactory#isTypeMatch()。 涉及的逻辑比较复杂,这里只简单讲一下重要的分支:


  • 如果是普通 bean,则检查 BeanDefinition 的 beanClass 是否匹配

  • 如果是 FactoryBean,则通过多种方式来预测 bean type


FactoryBean 的类型预测主要包括下面几种:


  1. 如果有 DecoratedDefinition,则覆盖 BeanDefinition,检查合并后的 beanClass 是否匹配

  2. 通过 FactoryBean.OBJECT_TYPE_ATTRIBUTE 属性获取 beanType (since 5.2)

  3. 实例化这个 FactoryBean,调用 getObjectType()方法来获取 beanType 上面提到的第三种情况可能会出现实例化失败(如解决属性占位符失败)而被多次创建的问题,即每次预测 bean type 都会尝试实例化,而每次都失败,直到它所依赖的组件都就绪才成功。


Dubbo ReferenceBean 本身也是一个 FactoryBean,在 2.7 中经常因为预测 bean type 导致被自动初始化,后面会详细讲这个问题。

解决属性

在 Spring 中一般是通过 PropertyPlaceholderConfigurer/PropertySourcesPlaceholderConfigurer 来解决 XML/@Value 中的属性占位符 ${...}。 二者都实现了 BeanFactoryPostProcessor 接口,会在 invokeBeanFactoryPostProcessors 阶段被加载,然后遍历处理所有 BeanDefinition 中的属性占位符。


[解析注册BeanDefinition] => [PropertyResourceConfigurer 解决属性占位符] => [加载BeanPostProcessor] => [初始化单例bean]由此可知,如果在PropertyPlaceholderConfigurer/PropertySourcesPlaceholderConfigurer加载前去初始化某个bean,则这个bean的属性占位符是不会被解决的。 这个就是Dubbo config bean 被过早加载导致无法解决占位符的根因。
复制代码

Dubbo Spring 的一些问题及解决办法

Dubbo spring 2.7 初始化过程

初始化入口是 ReferenceBean#prepareDubboConfigBeans(),即当第一个 ReferenceBean 初始化完成时,尝试加载其他 dubbo config bean。


    @Override    public void afterPropertiesSet() throws Exception {
// Initializes Dubbo's Config Beans before @Reference bean autowiring prepareDubboConfigBeans();
// lazy init by default. if (init == null) { init = false; }
// eager init if necessary. if (shouldInit()) { getObject(); } } private void prepareDubboConfigBeans() { beansOfTypeIncludingAncestors(applicationContext, ApplicationConfig.class); beansOfTypeIncludingAncestors(applicationContext, ModuleConfig.class); beansOfTypeIncludingAncestors(applicationContext, RegistryConfig.class); beansOfTypeIncludingAncestors(applicationContext, ProtocolConfig.class); beansOfTypeIncludingAncestors(applicationContext, MonitorConfig.class); beansOfTypeIncludingAncestors(applicationContext, ProviderConfig.class); beansOfTypeIncludingAncestors(applicationContext, ConsumerConfig.class); beansOfTypeIncludingAncestors(applicationContext, ConfigCenterBean.class); beansOfTypeIncludingAncestors(applicationContext, MetadataReportConfig.class); beansOfTypeIncludingAncestors(applicationContext, MetricsConfig.class); beansOfTypeIncludingAncestors(applicationContext, SslConfig.class); }
复制代码


存在的问题:


  1. 没有一个固定的初始化时机,而是与 ReferenceBean 初始化相关。 如果 ReferenceBean 被过早初始化,经常出现 dubbo 配置丢失、属性占位符未解决等错误。

  2. 可能在 BeanPostProcessor 加载完成前初始化 ReferenceBean,将导致类似 Seata 这种通过 BeanPostProcessor 机制的组件拦截失败。

Dubbo spring 3 的初始化过程

Dubbo 3 中进行大量重构,上面的痛点问题已经被解决,初始化主要流程如下:


[Spring解析XML/@Configuration class注册BeanDefinition] => [加载BeanFactoryPostProcessor(包含PropertyResourceConfigurer)]  => [1.解析@DubboReference/@DubboService注解并注册BeanDefinition] => [加载并注册BeanPostProcessor]  => [加载ApplicationListener] => [2.加载DubboConfigBeanInitializer初始化config bean] => [初始化单例bean] => [依赖注入ReferenceBean] => [3.监听ContextRefreshedEvent事件,启动dubbo框架]
复制代码


主要包含 3 个阶段:


  1. 在 BeanFactoryPostProcessor 阶段解析 @DubboReference/@DubboService 注解并注册 BeanDefinition。因为此时还是 BeanDefinition 处理阶段, 故注册的 ReferenceBean 可以被后续加载的业务 bean 使用 @Autowire 依赖注入。同时,也扩展支持在 @Configuration bean 方法使用 @DubboReference/@DubboService 注解。

  2. 在加载完所有 PropertyResourceConfigurer 和 BeanPostProcessor 之后才会执行 DubboConfigBeanInitializer 初始化 config bean,解决了属性 占位符未解决和 BeanPostProcessor 拦截失败的问题。

  3. 监听在 Spring context 事件,在其加载完毕时启动 dubbo 框架。

支持在 @Configuration bean 方法使用 @DubboReference/@DubboService 注解

参考 Dubbo spring 3 的初始化过程的第 1 阶段。

属性占位符解决失败

参考 Dubbo spring 3 的初始化过程的第 2 阶段。

ReferenceBean 被过早初始化问题

预测 ReferenceBean beanType 导致 Dubbo ReferenceBean 本身也是一个 FactoryBean,在 2.7 中经常因为预测 bean type 导致被自动初始化。 例如用户自定义的某个 BeanFactoryPostProcessor bean 使用了 @Autowire 注解依赖注入某个业务 bean, 而且这个自定义的 BeanFactoryPostProcessor bean 优先级比解决属性占位符的 PropertyResourceConfigurer 高,则此时出现解决属性占位符失败。


Dubbo 3 中 ReferenceBean 通过下面两种方式解决预测 type 的问题:


FactoryBean 的类型预测主要包括下面几种:


如果有 DecoratedDefinition,则覆盖 BeanDefinition,检查合并后的 beanClass 是否匹配


通过 FactoryBean.OBJECT_TYPE_ATTRIBUTE 属性获取 beanType (since 5.2)


ReferenceBean 被直接依赖导致过早初始如果在 Dubbo config bean 初始化前被依赖自动创建 ReferenceBean 实例,并创建一个 Lazy proxy 类注入到依赖的类中,不需要解决属性占位符,不会拉起 Dubbo 框架。 其他的 config bean 则固定在 PropertyResourceConfigurer 和 BeanPostProcessor 加载完成后才会执行初始化,避免了上述问题。

Reference 注解可能出现 @Autowire 注入失败的问题

在 Dubbo 2.7 中,在 BeanPostProcessor 中解析 @DubboReference/@Reference 注解,创建并注入 ReferenceBean 实例到 Spring 容器。这种方式有几个问题:


@DubboReference/@Reference 注解与 XML 定义的< dubbo:reference />初始化方式不一致,前者是由 dubbo 初始化,后者是由 Spring 容器负责初始化。


执行时机导致的依赖注入失败问题。按照正常的在 invokeBeanFactoryPostProcessors 阶段注册完毕所有 BeanDefinition,而 dubbo 2.7 的 ReferenceAnnotationBeanPostProcessor 是在 BeanPostProcessor 执行时才创建 ReferenceBean,可能出现某些比它早初始化的 bean 使用 @Autowire 注入失败的情况。


在 Dubbo 3 中,改成在 BeanFactoryPostProcessor 解析 @DubboReference/@Reference 注解并注册 ReferenceBean 的 BeanDefinition,记录字段将要注入的 referenceBeanName。 在 BeanPostProcessor 执行时通过 BeanFactory().getBean(referenceBeanName)获取到 ReferenceBean 实例。

发布于: 刚刚阅读数: 5
用户头像

Apache Dubbo

关注

还未添加个人签名 2020-07-31 加入

还未添加个人简介

评论

发布
暂无评论
源码解析:Dubbo3 的 Spring 适配原理与初始化流程_Java_Apache Dubbo_InfoQ写作社区