通常我们注册 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 接口的方法
1 2 |
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException; |
纵观该接口在 SpringBoot(Spring) 中的实现有以下
再翻一翻,实现了 registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
的也就两处
其一为 GenericApplicationContext 中的
1 2 3 4 5 6 |
@Override public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException { this.beanFactory.registerBeanDefinition(beanName, beanDefinition); } |
还有就是 DefaultListableBeanFactory 中的该方法的真正实现代码。因为总共两处,所以 GenericApplicationContext 中的
this.beanFactory.registerBeanDefinition(beanName, beanDefinition)
无疑也是委派给了 DefaultListableBeanFactory.registerBeanDefinition(...) 了。因此,终级之道,不管何时注册 Spring Bean 都得靠它。
那么现在的问题是 DefaultListableBeanFactory 在哪里,其实它是我们最为熟悉的对象,试着启动一个最简单的 SpringBoot 应用
1 2 3 4 |
public static void main(String[] args) { ApplicationContext context = SpringApplication.run(TestBeanRegister.class, args); System.out.println(context); } |
输出类似如下
org.springframework.context.annotation.AnnotationConfigApplicationContext@2928854b: startup date [Wed Mar 18 17:40:29 CDT 2020]; root of context hierarchy
这个 ApplicationContext 是一个 AnnotationConfigApplicationContext
, 这对于没什么惊奇,但只要沿着最前边的那个类继承关系图就能发现
- AnnotationConfigApplicationContext 是 GenericApplicationContext 的子类
- GenericApplicationContext 实现了 registerBeanDefinition() 方法
- GenericApplicationContext.registerBeanDefinition() 实际是调用了 DefaultListableBeanFactory.registerBeanDefinition() 方法
- 所以把这里的 ApplicationContext 转型为 GenericApplicationContext 后就能用 registerBeanDefinition() 方法来注册 Spring Bean 了
马上我们就来上面的那个 ApplicationContext 来开刷,测试下面的代码,此处备注一下,所用的测试环境为 Spring 1。
创建一个 XmlParser 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package yanbin.blog; import org.springframework.beans.factory.annotation.Value; public class XmlParser { private String charset; public XmlParser(@Value("${file.encoding}") String charset) { this.charset = charset; } @Override public String toString() { return "XmlParser{" + "charset='" + charset + '\'' + '}'; } } |
由于没有任何像 @Name 这样的注解,所以不会被 Spring 自动注册为 Spring bean, 构造函数将要使用系统属性中的 file.encoding 值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@SpringBootApplication public class TestBeanRegister { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(TestBeanRegister.class, args); for (String name : context.getBeanDefinitionNames()) { if (context.getBean(name) instanceof XmlParser) { System.out.println("#1 " + name); } } System.out.println("#2 " + context.getBeansOfType(XmlParser.class)); BeanDefinitionRegistry beanRegistry = (GenericApplicationContext) context; beanRegistry.registerBeanDefinition("runtimeXmlParser", BeanDefinitionBuilder.genericBeanDefinition(XmlParser.class).getBeanDefinition()); for (String name : context.getBeanDefinitionNames()) { if (context.getBean(name) instanceof XmlParser) { System.out.println("#3 " + name); } } System.out.println("#4 " + context.getBeansOfType(XmlParser.class)); System.out.println("#5 " + context.getBean("runtimeXmlParser")); System.out.println("#6 " + context.getBean(XmlParser.class)); } |
执行后输出如下
#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)
由输出内容可推断出以下信息
- 确实可以通过 ApplicationContext 来注册 Spring Bean
- 注册后只能用 context.getBean(beanName) 来获得新注册的 Bean, 而 context.getBean(class) 和 context.getBeansOfType(class) 都看不见
- 该方式注册也能触发 @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 并做同样的事情。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Configuration public class AppConfig implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanRegistry) throws BeansException { System.out.println("AppConfig method: postProcessBeanDefinitionRegistry, " + beanRegistry.getClass()); beanRegistry.registerBeanDefinition("runtimeXmlParser", BeanDefinitionBuilder.genericBeanDefinition(XmlParser.class).getBeanDefinition()); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { System.out.println("AppConfig method: postProcessBeanFactory, " + beanFactory.getClass()); } } |
然后 Main 方法为
1 2 3 4 5 6 7 8 |
@SpringBootApplication public class TestBeanRegister { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(TestBeanRegister.class, args); System.out.println(context.getBean("runtimeXmlParser")); System.out.println(context.getBean(XmlParser.class)); } |
执行输出为
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
再一次强调前面的规则:
- 只要让通过正常方式(@Name, @Component, 或 @Configuration)注册的 Spring Bean 类实现了 BeanDefinitionRegistryPostProcessor, 就可以在相应的实现方法 postProcessBeanDefinitionRegistry(beanRegistry) 中注册自己的 Spring Bean 了
- 有多个实现了 BeanDefinitionRegistryPostProcessor 的 Spring Bean 都不是问题,每个 postProcessBeanDefinitionRegistry(beanRegistry) 都能得到执行
下面换一种方式, 改为 AppConfig 类为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@Configuration public class AppConfig { @Bean BeanDefinitionRegistryPostProcessor beanPostProcessor1() { return new BeanDefinitionRegistryPostProcessor() { public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanRegistry) throws BeansException { beanRegistry.registerBeanDefinition("runtimeXmlParser", BeanDefinitionBuilder.genericBeanDefinition(XmlParser.class).getBeanDefinition()); } public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } }; } @Bean BeanDefinitionRegistryPostProcessor beanPostProcessor2(ConfigurableEnvironment env) { return new BeanDefinitionRegistryPostProcessor() { public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanRegistry) throws BeansException { AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(String.class) .addConstructorArgValue(env.getProperty("HOME")).getBeanDefinition(); beanRegistry.registerBeanDefinition("homeString", beanDefinition); } public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } }; } } |
测试类 TestBeanRegister
1 2 3 4 5 6 7 8 |
@SpringBootApplication public class TestBeanRegister { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(TestBeanRegister.class, args); System.out.println(context.getBean("runtimeXmlParser")); System.out.println(context.getBean("homeString")); } |
执行后输出如下
XmlParser{charset='UTF-8'}
/Users/yanbin
若需深入一些
- 可以用各种 @ConditionalOnXxx 加上像 beanPostProcessor1() 之上来控制有条件的来决定要不要注册 Spring Bean,比如说完成 AutoConfiguration 之类的行为
- BeanDefinitionBuilder 可用来设置构造参数,设置 Bean 的字段值,甚至替换掉 Bean 的方法实现
- 在 postProcessBeanFactory() 方法中或许还能做些事情
由前面可知,其实在 postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 中的 beanFactory 也是一个 DefaultListableBeanFactory, 所以从实现效果上,下面的 AppConfig 代码也差不多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Configuration public class AppConfig { @Bean BeanFactoryPostProcessor beanPostProcessor1() { return new BeanFactoryPostProcessor() { public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { ((BeanDefinitionRegistry)beanFactory).registerBeanDefinition("runtimeXmlParser", BeanDefinitionBuilder.genericBeanDefinition(XmlParser.class). getBeanDefinition()); } }; } } |
BeanDefinitionRegistryPostProcessor 继承自 BeanFactoryPostProcessor 接口,虽然以上代码也能成功注册 Spring Bean, 但语义上与该接口的设计相违背。因为一个强制转型,从而让该 BeanFactoryPostProcessor 做了不该做的事。我们还是应该遵从设计者的原意在 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry(beanRegistry) 中进行 Spring Bean 的注册。
最后还是总结一下:
- 在 SpringBoot 中我们可以直接拿启动后 ApplicationContext(确定它是 BeanDefinitionRegistry 的实例) 来注册 Spring Bean, 但这时注册的 Bean 没多大意义
- 可通过 BeanFactoryPostProcessor.postProcessBeanFactory(beanFactory) 来注册 Spring Bean
- 更好的方法应该由 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry(beanRegistry) 来注册 Spring Bean。BeanDefinitionRegistryPostProcessor 继承自 BeanFactoryPostProcessor 接口
- 无论是通过 BeanFactoryPostProcessor 还是 BeanDefinitionRegistryPostProcessor 来注册 Spring Bean, 实现这两接口的 Bean 必须是在 Spring 上下文初始化之前注册的 Spring Bean。具体点说比如是由正常方式(@Named, @Component 等) 注册的,因为像 XxxPostProcessor 这样的类是依赖于 Spring 上下文事件来处理的,如果 Spring 完成了启动就太迟了
本文链接 https://yanbin.blog/dynamic-creating-spring-bean-runtime/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
Google因为使用了Java的API接口,还不是具体的代码实现,就被ORACLE告得鸡飞狗跳的。日本这边,现在很多公司已经直接不再考虑使用任何与Oracle&Java&Mysql相关的东西作项目了。你们那边使用Java不受影响吗?