如何在运行时实例化Spring管理的Bean?

29

我卡在了一个从纯Java到Spring的简单重构上。这个应用程序有一个“容器”对象,它在运行时实例化其部分。让我用代码来解释一下:

public class Container {
    private List<RuntimeBean> runtimeBeans = new ArrayList<RuntimeBean>();

    public void load() {
        // repeated several times depending on external data/environment
        RuntimeBean beanRuntime = createRuntimeBean();
        runtimeBeans.add(beanRuntime);
    }

    public RuntimeBean createRuntimeBean() {
         // should create bean which internally can have some 
         // spring annotations or in other words
         // should be managed by spring
    }
}

基本上,在加载容器时,容器会请求一些外部系统提供关于每个RuntimeBean的数量和配置信息,并根据给定的规范创建这些bean。

问题是:通常在Spring中进行操作时。

ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfiguration.class);
Container container = (Container) context.getBean("container");

我们的对象已经完全配置并注入了所有依赖。但在我的情况下,我必须在执行load()方法后实例化一些需要依赖注入的对象。
我该如何实现呢?

我正在使用基于Java的配置,我已尝试为RuntimeBeans创建一个工厂:

public class BeanRuntimeFactory {

    @Bean
    public RuntimeBean createRuntimeBean() {
        return new RuntimeBean();
    }
}
期望使用@Bean在所谓的“轻量级”模式下工作。http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/context/annotation/Bean.html不幸的是,我发现与仅仅使用new RuntimeBean()没有什么区别。这里有一个类似问题的帖子:如何获取由FactoryBean创建且由Spring管理的bean? 还有http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/annotation/Configurable.html,但在我的情况下看起来很笨重。
我还尝试使用ApplicationContext.getBean(“runtimeBean”,args),其中runtimeBean具有“Prototype”范围,但getBean是一个糟糕的解决方案。

更新1

为了更具体,我正在尝试重构这个类: https://github.com/apache/lucene-solr/blob/trunk/solr/core/src/java/org/apache/solr/core/CoreContainer.java 参见#load()方法并查找“return create(cd,false);”

更新2

我在Spring文档中发现了一件相当有趣的事情,叫做“查找方法注入”: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-factory-lookup-method-injection

还有一个有趣的jira票据https://jira.spring.io/browse/SPR-5192,Phil Webb在其中说https://jira.spring.io/browse/SPR-5192?focusedCommentId=86051&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-86051应该在这里使用javax.inject.Provider(它让我想起了Guice)。

更新3

还有http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.html

更新4

所有这些“查找”方法的问题在于它们不支持传递任何参数。我也需要像使用applicationContext.getBean(“runtimeBean”,arg1,arg2)一样传递参数。好像在某个时候用https://jira.spring.io/browse/SPR-7431修复了这个问题。

更新5

Google Guice有一个很棒的功能叫做AssistedInject。https://github.com/google/guice/wiki/AssistedInject


1
如果您使用new运算符和构造函数实例化对象,则它不是Spring Bean,因此不符合DI的条件。 - Kevin Bowersox
1
你能详细解释一下你想做什么吗? - Kevin Bowersox
1
@KevinBowersox 如果对象是通过正确拦截的@Bean方法返回的,例如在配置类上,则不是这样。虽然这听起来很像一个XY问题,但Spring Cloud Connectors之类的东西可能是更好的选择。 - chrylis -cautiouslyoptimistic-
@chrylis 同意,Java配置需要使用new运算符。我更多地是指Container类及其对new的使用。这在Spring中行不通。 - Kevin Bowersox
你们都是对的,我的问题是如何重新设计代码使其正常工作。我想做什么?我想要通过某种方式重新设计代码,在配置中定义运行时bean(但如果我甚至不知道bean的数量和属性,该怎么办呢?)或者拥有某种工厂可以在运行时创建bean并完成完整的Spring编织...实际上,我不知道,我需要有人解释如何正确地做事。 - Vadim Kirilchuk
顺便问一下,什么是 XY 问题?谢谢。 - Vadim Kirilchuk
5个回答

17

看来我找到了一个解决方案。由于我使用的是基于Java的配置,所以比你想象的还要简单。另一种在xml中的选择是lookup-method,但仅限于从Spring版本4.1.X开始,因为它支持将参数传递给方法。

下面是一个完整可用的示例:

public class Container {
    private List<RuntimeBean> runtimeBeans = new ArrayList<RuntimeBean>();
    private RuntimeBeanFactory runtimeBeanFactory;

    public void load() {
        // repeated several times depending on external data/environment
        runtimeBeans.add(createRuntimeBean("Some external info1"));
        runtimeBeans.add(createRuntimeBean("Some external info2"));
    }

    public RuntimeBean createRuntimeBean(String info) {
         // should create bean which internally can have some 
         // spring annotations or in other words
         // should be managed by spring
         return runtimeBeanFactory.createRuntimeBean(info);
    }

    public void setRuntimeBeanFactory(RuntimeBeanFactory runtimeBeanFactory) {
        this.runtimeBeanFactory = runtimeBeanFactory;
    }
}

public interface RuntimeBeanFactory {
    RuntimeBean createRuntimeBean(String info);
}

//and finally
@Configuration
public class ApplicationConfiguration {
    
    @Bean
    Container container() {
        Container container = new Container(beanToInject());
        container.setBeanRuntimeFactory(runtimeBeanFactory());
        return container;
    }
        
    // LOOK HOW IT IS SIMPLE IN THE JAVA CONFIGURATION
    @Bean 
    public BeanRuntimeFactory runtimeBeanFactory() {
        return new BeanRuntimeFactory() {
            public RuntimeBean createRuntimeBean(String beanName) {
                return runtimeBean(beanName);
            }
        };
    }
    
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    RuntimeBean runtimeBean(String beanName) {
        return new RuntimeBean(beanName);
    }
}

class RuntimeBean {
    @Autowired
    Container container;
}

就这样了。

谢谢大家。


是的,虽然您的方法仅适用于4.1.4.RELEASE版本之后,在此之前,您必须在上下文中使用简单的getBean(name,...args)或在ConfigurationClassEnhancer上覆盖拦截器以将args传递给构造函数。 - mariubog
基于Java的方法应该可以在早期版本上运行,我有什么遗漏吗? - Vadim Kirilchuk
1
自4.1.4版本以后,它已经修复了构造函数不接受参数的问题。 - mariubog
2
自4.1.4版本以后,构造函数已经可以接受参数了。请记住,通过调用“return runtimeBean(beanName);”来调用您的方法runtimeBean并不是直接调用,而是在Spring上下文中负责创建此bean的bean工厂上调用实例化方法,并且在将参数传递给实际bean之前由工厂解析参数。在使用'@Bean'和'@Configuration'注释的情况下,整个过程都被ConfigurationClassEnhancer.BeanFactoryAwareMethodInterceptor拦截,并且它决定如何实例化您的bean。 - mariubog
上面的代码看起来不完整,很难理解...beanToInjectMethod()在哪里?我没有看到Container中有任何以此为输入的构造函数等等...你的解决方案是基于查找方法吗?谢谢澄清 :) - greg
也许工厂的名称应该是 RuntimeBeanFactory?我在样例代码中看到了 BeanRuntimeFactoryRuntimeBeanFactory 两者都有,这让我感到困惑。 - liushuaikobe

8
我认为你的概念是错误的,因为使用以下代码: RuntimeBean beanRuntime = createRuntimeBean(); 你绕过了Spring容器并且使用了普通的Java构造函数,因此任何工厂方法上的注释都会被忽略,并且该bean不会由Spring管理。
以下是在一个方法中创建多个原型bean的解决方案,看起来不太好看,但应该能够正常工作。我在RuntimeBean中自动装配了容器,证明了自动装配已经在日志中显示,当你运行这个方法时,你可以在日志中看到每个bean都是原型的新实例。
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);

        ApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
        Container container = (Container) context.getBean("container");
        container.load();
    }
}

@Component
class Container {
    private List<RuntimeBean> runtimeBeans = new ArrayList<RuntimeBean>();
    @Autowired
    ApplicationContext context;

    @Autowired
    private ObjectFactory<RuntimeBean> myBeanFactory;

    public void load() {

        // repeated several times depending on external data/environment
        for (int i = 0; i < 10; i++) {
            // **************************************
            // COMENTED OUT THE WRONG STUFFF 
            // RuntimeBean beanRuntime = context.getBean(RuntimeBean.class);
            // createRuntimeBean();
            // 
            // **************************************

            RuntimeBean beanRuntime = myBeanFactory.getObject();
            runtimeBeans.add(beanRuntime);
            System.out.println(beanRuntime + "  " + beanRuntime.container);
        }
    }

    @Bean
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public RuntimeBean createRuntimeBean() {
        return new RuntimeBean();
    }
}

// @Component

class RuntimeBean {
    @Autowired
    Container container;

} '

这篇文章很好的解释了如何以“让它工作”的方式使其正常运行,这也是我尝试通过RuntimeBeanFactory实现的目标。然而,我感觉应该有更好的解决方案。谢谢。 - Vadim Kirilchuk
1
这里的另一个问题是我的原型作用域bean带有一些参数。ObjectFactory#getObject()不允许传递任何参数。 - Vadim Kirilchuk
这种方法是可行的,不像基于BeanFactoryPostProcessor的解决方案。谢谢!! - Michael Böckling

6
一个简单的方法:
@Component
public class RuntimeBeanBuilder {

    @Autowired
    private ApplicationContext applicationContext;

    public MyObject load(String beanName, MyObject myObject) {
        ConfigurableApplicationContext configContext = (ConfigurableApplicationContext) applicationContext;
        SingletonBeanRegistry beanRegistry = configContext.getBeanFactory();

        if (beanRegistry.containsSingleton(beanName)) {
            return beanRegistry.getSingleton(beanName);
        } else {
            beanRegistry.registerSingleton(beanName, myObject);

            return beanRegistry.getSingleton(beanName);
        }
    }
}


@Service
public MyService{

   //inject your builder and create or load beans
   @Autowired
   private RuntimeBeanBuilder builder;

   //do something
}

您可以使用以下方法代替SingletonBeanRegistry:

BeanFactory beanFactory = configContext.getBeanFactory();

无论如何,SingletonBeanBuilder扩展了HierarchicalBeanFactory,而HierarchicalBeanFactory扩展了BeanFactory。

4
你不需要使用Container,因为所有的运行时对象都应该由ApplicationContext创建、持有和管理。想象一下一个Web应用程序,它们非常相似。每个请求都包含如你上面提到的外部数据/环境信息。你需要的是一个原型/请求范围的bean,比如ExternalDataEnvironmentInfo,它可以通过静态方式读取和保存运行时数据,比如一个静态工厂方法。
<bean id="externalData" class="ExternalData"
    factory-method="read" scope="prototype"></bean>

<bean id="environmentInfo" class="EnvironmentInfo"
    factory-method="read" scope="prototype/singleton"></bean>

<bean class="RuntimeBean" scope="prototype">
    <property name="externalData" ref="externalData">
    <property name="environmentInfo" ref="environmentInfo">
</bean> 

如果你确实需要一个容器来保存运行时对象,代码应该是这样的:
class Container {

    List list;
    ApplicationContext context;//injected by spring if Container is not a prototype bean

    public void load() {// no loop inside, each time call load() will load a runtime object
        RuntimeBean bean = context.getBean(RuntimeBean.class); // see official doc
        list.add(bean);// do whatever
    }
}

官方文档:具有原型bean依赖关系的单例bean

感谢您的评论,我并没有看到我的工厂有太大的区别。它仍然是一个运行时bean,由容器在加载/重新加载等操作时创建,其作用域为原型。由于它是我尝试使用Spring重构的框架的核心类,所以我确实需要一个容器。 - Vadim Kirilchuk

3

通过使用BeanFactoryPostProcesor,可以动态注册bean。在应用程序启动时(Spring应用程序上下文已初始化),您可以这样做。您不能最新地注册bean,但另一方面,您可以利用依赖注入为您的bean,因为它们成为“真正”的Spring bean。

public class DynamicBeansRegistar implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        if (! (beanFactory instanceof BeanDefinitionRegistry))  {
            throw new RuntimeException("BeanFactory is not instance of BeanDefinitionRegistry");
        }   
        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;

        // here you can fire your logic to get definition for your beans at runtime and 
        // then register all beans you need (possibly inside a loop)

        BeanDefinition dynamicBean = BeanDefinitionBuilder.    
             .rootBeanDefinition(TheClassOfYourDynamicBean.class) // here you define the class
             .setScope(BeanDefinition.SCOPE_SINGLETON)
             .addDependsOn("someOtherBean") // make sure all other needed beans are initialized

             // you can set factory method, constructor args using other methods of this builder
            
             .getBeanDefinition();

        registry.registerBeanDefinition("your.bean.name", dynamicBean);           

}

@Component
class SomeOtherClass {

    // NOTE: it is possible to autowire the bean
    @Autowired
    private TheClassOfYourDynamicBean myDynamicBean;

}

如上所述,您仍然可以利用Spring的依赖注入,因为后处理器作用于实际的应用程序上下文。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接