转载

运行时动态创建 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 接口的方法

void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
        throws BeanDefinitionStoreException;

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

运行时动态创建 Spring Bean

再翻一翻,实现了 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 , 这对于没什么惊奇,但只要沿着最前边的那个类继承关系图就能发现

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

马上我们就来上面的那个 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) 

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

  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 并做同样的事情。

@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

再一次强调前面的规则:

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

下面换一种方式, 改为 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 

若需深入一些

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

由前面可知,其实在 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 的注册。

最后还是总结一下:

  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/
正文到此结束
Loading...