运行时动态创建 Spring Bean

通常我们注册 Spring Bean 是通过像 @Named, @Bean, @Component, @Service 这样的注解来注册的,或者用更为古老的 XML 配置文件的方式。难免有时候要根据实际业务需求在 Spring  运行期间动态注册 Spring Bean, 比如基本某种形式的配置文件或系统属性来注册相应的 Bean, 这好像又回到了 XML 文件注册方式,也不尽然。

那为什么在运行期还要去注册 Spring Bean 呢,直接 new 对象不行吗?当然行得通,不过这样的话就不能更好的使用到  Spring IOC 的好处了。像待注册的 Bean 构造函数可以直接用到其他的 Spring  对象,或 @Value 引入环境变量,还有 @PostContruct 这样的行为。

最初思考如何注册 Spring Bean 时还是费了不少周折,如今清晰了许多。了当的说,不管是 Spring  初始时还是运行时,注册 Bean 的关键(应是唯一) 入口就是 BeanDefinitionRegistry 接口的方法

纵观该接口在 SpringBoot(Spring) 中的实现有以下

再翻一翻,实现了 registerBeanDefinition(String beanName, BeanDefinition beanDefinition) 的也就两处

其一为 GenericApplicationContext 中的

还有就是 DefaultListableBeanFactory 中的该方法的真正实现代码。因为总共两处,所以 GenericApplicationContext 中的

this.beanFactory.registerBeanDefinition(beanName, beanDefinition)

无疑也是委派给了 DefaultListableBeanFactory.registerBeanDefinition(...) 了。因此,终级之道,不管何时注册 Spring Bean 都得靠它。

那么现在的问题是 DefaultListableBeanFactory 在哪里,其实它是我们最为熟悉的对象,试着启动一个最简单的 SpringBoot 应用

输出类似如下

org.springframework.context.annotation.AnnotationConfigApplicationContext@2928854b: startup date [Wed Mar 18 17:40:29 CDT 2020]; root of context hierarchy

这个 ApplicationContext 是一个 AnnotationConfigApplicationContext, 这对于没什么惊奇,但只要沿着最前边的那个类继承关系图就能发现

  1. AnnotationConfigApplicationContext 是 GenericApplicationContext 的子类
  2. GenericApplicationContext 实现了 registerBeanDefinition() 方法
  3. GenericApplicationContext.registerBeanDefinition() 实际是调用了 DefaultListableBeanFactory.registerBeanDefinition() 方法
  4. 所以把这里的 ApplicationContext 转型为 GenericApplicationContext 后就能用 registerBeanDefinition() 方法来注册 Spring Bean 了

马上我们就来上面的那个 ApplicationContext 来开刷,测试下面的代码,此处备注一下,所用的测试环境为 Spring 1。

创建一个 XmlParser 类

由于没有任何像 @Name 这样的注解,所以不会被 Spring  自动注册为 Spring bean, 构造函数将要使用系统属性中的 file.encoding 值。

执行后输出如下

#2 {}
#3 runtimeXmlParser
#4 {}
#5 XmlParser{charset='UTF-8'}
18:02:35.785 [Thread-3] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@2928854b: startup date [Wed Mar 18 18:02:35 CDT 2020]; root of context hierarchy
18:02:35.787 [Thread-3] INFO o.s.j.e.a.AnnotationMBeanExporter - Unregistering JMX-exposed beans on shutdown
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'yanbin.blog.XmlParser' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:352)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:339)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1092)
at yanbin.blog.TestBeanRegister.main(TestBeanRegister.java:36)

由输出内容可推断出以下信息

  1. 确实可以通过 ApplicationContext 来注册 Spring Bean
  2. 注册后只能用 context.getBean(beanName) 来获得新注册的 Bean, 而 context.getBean(class) 和 context.getBeansOfType(class) 都看不见
  3. 该方式注册也能触发 @PostContruct 行为,但与 ApplicationListener<ContextRefreshedEvent> 无缘

也就是说,这种方式注册的 Spring Bean 不是我们想要的,原因是注册的太迟了,Spring 上下文都初始化完成再注册的 Bean 意义不大。

以上只是一种尝试,真真想要有效的注册 Spring Bean 的方式是让一个自动注册的 Spring Bean 实现接口 BeanDefinitionRegistryPostProcessor,然后在其方法中注册 Spring Bean

void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanRegistry)

进一步验证,此处让 AppConfig 实现 BeanDefinitionRegistryPostProcessor, 因为 AppConfig 有注解 @Configuration, 所以 AppConfig 会被注册为一个 Spring Bean。实际上任意的 Spring Bean 都可以去实现 BeanDefinitionRegistryPostProcessor 并做同样的事情。

然后 Main 方法为

执行输出为

AppConfig method: postProcessBeanDefinitionRegistry, class org.springframework.beans.factory.support.DefaultListableBeanFactory
AppConfig method: postProcessBeanFactory, class org.springframework.beans.factory.support.DefaultListableBeanFactory
XmlParser{charset='UTF-8'}
XmlParser{charset='UTF-8'}

没问题,Bean 成功注册,系统属性成功注入,从 Spring 上下文获得 Bean 也没问题。 要是在 XmlParser 中加上一个 @PostContruct 方法也会在 Bean 初始化后成功执行。而且看到接口中两方法的参数类型都是 DefaultListableBeanFactory

再一次强调前面的规则:

  1. 只要让通过正常方式(@Name, @Component, 或 @Configuration)注册的 Spring Bean 类实现了 BeanDefinitionRegistryPostProcessor, 就可以在相应的实现方法 postProcessBeanDefinitionRegistry(beanRegistry) 中注册自己的 Spring Bean 了
  2. 有多个实现了 BeanDefinitionRegistryPostProcessor 的 Spring Bean 都不是问题,每个 postProcessBeanDefinitionRegistry(beanRegistry) 都能得到执行

下面换一种方式, 改为 AppConfig 类为

测试类 TestBeanRegister

执行后输出如下

XmlParser{charset='UTF-8'}
/Users/yanbin

若需深入一些

  1. 可以用各种 @ConditionalOnXxx 加上像 beanPostProcessor1() 之上来控制有条件的来决定要不要注册 Spring Bean,比如说完成 AutoConfiguration 之类的行为
  2. BeanDefinitionBuilder 可用来设置构造参数,设置 Bean 的字段值,甚至替换掉 Bean 的方法实现
  3. 在 postProcessBeanFactory() 方法中或许还能做些事情

由前面可知,其实在 postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 中的 beanFactory 也是一个 DefaultListableBeanFactory, 所以从实现效果上,下面的 AppConfig 代码也差不多

BeanDefinitionRegistryPostProcessor 继承自 BeanFactoryPostProcessor 接口,虽然以上代码也能成功注册 Spring Bean, 但语义上与该接口的设计相违背。因为一个强制转型,从而让该 BeanFactoryPostProcessor 做了不该做的事。我们还是应该遵从设计者的原意在 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry(beanRegistry) 中进行 Spring Bean 的注册。

最后还是总结一下:

  1. 在 SpringBoot 中我们可以直接拿启动后 ApplicationContext(确定它是  BeanDefinitionRegistry 的实例) 来注册 Spring Bean, 但这时注册的 Bean 没多大意义
  2. 可通过 BeanFactoryPostProcessor.postProcessBeanFactory(beanFactory) 来注册 Spring Bean
  3. 更好的方法应该由 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry(beanRegistry) 来注册 Spring Bean。BeanDefinitionRegistryPostProcessor 继承自 BeanFactoryPostProcessor 接口
  4. 无论是通过 BeanFactoryPostProcessor 还是 BeanDefinitionRegistryPostProcessor 来注册 Spring Bean, 实现这两接口的 Bean 必须是在 Spring 上下文初始化之前注册的 Spring Bean。具体点说比如是由正常方式(@Named, @Component 等) 注册的,因为像 XxxPostProcessor 这样的类是依赖于 Spring 上下文事件来处理的,如果 Spring 完成了启动就太迟了

本文链接 https://yanbin.blog/dynamic-creating-spring-bean-runtime/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments
JackZhou
3 years ago

Google因为使用了Java的API接口,还不是具体的代码实现,就被ORACLE告得鸡飞狗跳的。日本这边,现在很多公司已经直接不再考虑使用任何与Oracle&Java&Mysql相关的东西作项目了。你们那边使用Java不受影响吗?