通常我们注册 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 接口的方法
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException;
丛观该接口在 SpringBoot(Spring) 中的实现有以下
再翻一翻,实现了 registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
的也就两处
其一为 GenericApplicationContext 中的
@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 应用
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
, 这对于没什么惊奇,但只要沿着最前边的那个类继承关系图就能发现
马上我们就来上面的那个 ApplicationContext 来开刷,测试下面的代码,此处备注一下,所用的测试环境为 Spring 1。
创建一个 XmlParser 类
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 值。
@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)
由输出内容可推断出以下信息
也就是说,这种方式注册的 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 并做同样的事情。
@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 方法为
@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
再一次强调前面的规则:
下面换一种方式, 改为 AppConfig 类为
@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
@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
若需深入一些
由前面可知,其实在 postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 中的 beanFactory 也是一个 DefaultListableBeanFactory, 所以从实现效果上,下面的 AppConfig 代码也差不多
@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 的注册。
最后还是总结一下: