Spring - 以编程方式生成一组 bean

Spring - Programmatically generate a set of beans

我有一个 Dropwizard 应用程序需要为配置列表中的每个配置生成十几个 bean。健康检查、石英调度程序等

像这样:

@Component
class MyModule {
    @Inject
    private MyConfiguration configuration;

    @Bean
    @Lazy
    public QuartzModule quartzModule() {
        return new QuartzModule(quartzConfiguration());
    }


    @Bean
    @Lazy
    public QuartzConfiguration quartzConfiguration() {
        return this.configuration.getQuartzConfiguration();
    }

    @Bean
    @Lazy
    public HealthCheck healthCheck() throws SchedulerException {
        return this.quartzModule().quartzHealthCheck();
    }
}

我有多个 MyConfiguration 实例,它们都需要这样的 bean。 现在我必须复制并粘贴这些定义并为每个新配置重命名它们。

我能否以某种方式迭代我的配置 类 并为每个配置生成一组 bean 定义?

我可以使用子类化解决方案或任何类型安全的解决方案,而无需在每次必须添加新服务时复制和粘贴相同的代码并重命名方法。

编辑:我应该补充一点,我还有其他依赖于这些 bean 的组件(例如,它们注入 Collection<HealthCheck>。)

您应该可以这样做:

@Configuration
public class MyConfiguration implements BeanFactoryAware {

    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    @PostConstruct
    public void onPostConstruct() {
        ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) beanFactory;
        for (..) {
            // setup beans programmatically
            String beanName= ..
            Object bean = ..
            configurableBeanFactory.registerSingleton(beanName, bean);
        }
     }

}

您需要创建一个基础配置 class,它由您的所有 Configuration class 扩展。然后,您可以迭代所有配置 classes,如下所示:

// Key - name of the configuration class
// value - the configuration object
Map<String, Object> configurations = applicationContext.getBeansWithAnnotation(Configuration.class);
Set<String> keys = configurations.keySet();
for(String key: keys) {
    MyConfiguration conf = (MyConfiguration) configurations.get(key);

    // Implement the logic to use this configuration to create other beans.
}

只是扩展 Michas 的回答 - 如果我这样设置他的解决方案就可以工作:

public class ToBeInjected {

}

public class PropertyInjected {

    private ToBeInjected toBeInjected;

    public ToBeInjected getToBeInjected() {
        return toBeInjected;
    }

    @Autowired
    public void setToBeInjected(ToBeInjected toBeInjected) {
        this.toBeInjected = toBeInjected;
    }

}

public class ConstructorInjected {
    private final ToBeInjected toBeInjected;

    public ConstructorInjected(ToBeInjected toBeInjected) {
        this.toBeInjected = toBeInjected;
    }

    public ToBeInjected getToBeInjected() {
        return toBeInjected;
    }

}

@Configuration
public class BaseConfig implements BeanFactoryAware{

    private ConfigurableBeanFactory beanFactory;

    protected ToBeInjected toBeInjected;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (ConfigurableBeanFactory) beanFactory;
    }

    @PostConstruct
    public void addCustomBeans() {
        toBeInjected = new ToBeInjected();
        beanFactory.registerSingleton(this.getClass().getSimpleName() + "_quartzConfiguration", toBeInjected);
    }

    @Bean
    public ConstructorInjected test() {
        return new ConstructorInjected(toBeInjected);
    }

    @Bean
    public PropertyInjected test2() {
        return new PropertyInjected();
    }

}

需要注意的一件事是我正在创建自定义 bean 作为配置的属性 class 并在 @PostConstruct 方法中初始化它们。通过这种方式,我将对象注册为一个 bean(因此 @Autowire 和 @Inject 按预期工作),以后我可以在需要它的 bean 的构造函数注入中使用相同的实例。属性visibility设置为protected,这样subclasses就可以使用创建的objects.

由于我们持有的实例实际上不是 Spring 代理,因此可能会出现一些问题(方面未触发等)。在注册之后检索 bean 实际上可能是一个好主意,如:

toBeInjected = new ToBeInjected();
String beanName = this.getClass().getSimpleName() + "_quartzConfiguration";
beanFactory.registerSingleton(beanName, toBeInjected);
toBeInjected = beanFactory.getBean(beanName, ToBeInjected.class);

因此您需要即时声明新 bean 并将它们注入到 Spring 的应用程序上下文中,就好像它们只是普通 bean 一样,这意味着它们必须受到代理,post-processing 等,即它们必须服从 Spring beans 生命周期。

请参阅 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry() method javadoc。这正是您所需要的,因为它允许您修改Spring的应用程序上下文在正常的 bean 定义被加载后 在实例化任何单个 bean 之前

@Configuration
public class ConfigLoader implements BeanDefinitionRegistryPostProcessor {

    private final List<String> configurations;

    public ConfigLoader() {
        this.configurations = new LinkedList<>();
        // TODO Get names of different configurations, just the names!
        // i.e. You could manually read from some config file
        // or scan classpath by yourself to find classes 
        // that implement MyConfiguration interface.
        // (You can even hardcode config names to start seeing how this works)
        // Important: you can't autowire anything yet, 
        // because Spring has not instantiated any bean so far!
        for (String readConfigurationName : readConfigurationNames) {
            this.configurations.add(readConfigurationName);
        }
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // iterate over your configurations and create the beans definitions it needs
        for (String configName : this.configurations) {
            this.quartzConfiguration(configName, registry);
            this.quartzModule(configName, registry);
            this.healthCheck(configName, registry);
            // etc.
        }
    }

    private void quartzConfiguration(String configName, BeanDefinitionRegistry registry) throws BeansException {
        String beanName = configName + "_QuartzConfiguration";
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(QuartzConfiguration.class).setLazyInit(true); 
        // TODO Add what the bean needs to be properly initialized
        // i.e. constructor arguments, properties, shutdown methods, etc
        // BeanDefinitionBuilder let's you add whatever you need
        // Now add the bean definition with given bean name
        registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
    }

    private void quartzModule(String configName, BeanDefinitionRegistry registry) throws BeansException {
        String beanName = configName + "_QuartzModule";
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(QuartzModule.class).setLazyInit(true); 
        builder.addConstructorArgReference(configName + "_QuartzConfiguration"); // quartz configuration bean as constructor argument
        // Now add the bean definition with given bean name
        registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
    }

    private void healthCheck(String configName, BeanDefinitionRegistry registry) throws BeansException {
        String beanName = configName + "_HealthCheck";
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(HealthCheck.class).setLazyInit(true); 
        // TODO Add what the bean needs to be properly initialized
        // i.e. constructor arguments, properties, shutdown methods, etc
        // BeanDefinitionBuilder let's you add whatever you need
        // Now add the bean definition with given bean name
        registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
    }

    // And so on for other beans...
}

这有效地声明了您需要的 beans 并将它们注入到 Spring 的应用程序上下文中,每个配置一组 beans。您必须依赖一些 命名模式 然后 在需要的地方按名称 自动装配您的 bean:

@Service
public class MyService {

    @Resource(name="config1_QuartzConfiguration")
    private QuartzConfiguration config1_QuartzConfiguration;

    @Resource(name="config1_QuartzModule")
    private QuartzModule config1_QuartzModule;

    @Resource(name="config1_HealthCheck")
    private HealthCheck config1_HealthCheck;

    ...

}

备注:

  1. 如果您通过手动从文件中读取配置名称,请使用 Spring 的 ClassPathResource.getInputStream()

  2. 如果你自己扫描类路径,我强烈建议你使用神奇的Reflections library

  3. 您必须为每个 bean 定义手动设置所有属性和依赖项。每个 bean 定义都独立于其他 bean 定义,即你不能重用它们,不能将它们一个一个地放在另一个里面,等等。把它们想象成你用旧的 XML 方式声明 beans。

  4. 查看 BeanDefinitionBuilder javadocs and GenericBeanDefinition javadocs 了解更多详情。

我就在这里凑钱吧。其他人提到您需要创建一个 bean,将您的配置注入其中。 然后该 bean 将使用您的配置创建其他 bean 并将它们插入到上下文中(您还需要以一种或另一种形式注入)。

我认为其他人没有注意到的是,您说过其他 bean 将依赖于这些动态创建的 bean。 这意味着您的动态 bean 工厂必须在依赖 bean 之前实例化。您可以使用

执行此操作(在注释世界中)
@DependsOn("myCleverBeanFactory")

至于你的智能豆工厂是什么类型的对象,其他人已经推荐了更好的方法来做到这一点。 但如果我没记错的话,你实际上可以在旧的 spring 2 世界中这样做:

public class MyCleverFactoryBean implements ApplicationContextAware, InitializingBean {
  @Override
  public void afterPropertiesSet() {
    //get bean factory from getApplicationContext()
    //cast bean factory as necessary
    //examine your config
    //create beans
    //insert beans into context
   } 

..

我想出的 "best" 方法是将我所有的 Quartz 配置和调度程序包装在 1 个超级 bean 中,然后手动将其全部连接起来,然后重构代码以使用超级 bean 接口。

uber bean 在它的 PostConstruct 中创建我需要的所有对象,并实现 ApplicationContextAware 以便它可以自动连接它们。这并不理想,但这是我能想到的最好的。

Spring 根本没有以类型安全的方式动态添加 bean 的好方法。