Spring - программное создание набора bean-компонентов.

У меня есть приложение 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>.)


person noah    schedule 06.02.2015    source источник
comment
Либо вам нужно зарегистрировать определения bean-компонентов с помощью BeanDefinitionRegistryPostProcessor, либо выполнить некоторую магию контекстной иерархии (отдельные контексты для ваших модулей, в которых зависимости могут регистрироваться в родительском контексте), либо просто выполнить поиск службы вместо того, чтобы позволить Spring внедрить ваши зависимости (т.е. ApplicationContext#getBeansOfType) .   -  person Pavel Horal    schedule 13.02.2015


Ответы (6)


Таким образом, вам нужно объявлять новые bean-компоненты на лету и внедрять их в контекст приложения Spring, как если бы они были просто обычными bean-компонентами, то есть они должны подвергаться проксированию, постобработке и т. Д., Т.е. они должны подчиняться жизненному циклу bean-компонентов Spring. .

См. _ 1_ метод javadocs. Это именно то, что вам нужно, потому что оно позволяет вам изменять контекст приложения Spring после загрузки обычных определений 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);
        }
    }

    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...
}

Это эффективно объявляет необходимые bean-компоненты и внедряет их в контекст приложения Spring, по одному набору 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 _ 4_.

  2. Если вы собираетесь сканировать путь к классам самостоятельно, я настоятельно рекомендую вам использовать замечательную библиотеку отражений.

  3. Вы должны вручную установить все свойства и зависимости для каждого определения bean-компонента. Каждое определение bean-компонента не зависит от других определений bean-компонентов, т.е. вы не можете повторно использовать их, устанавливать одно внутри другого и т. Д. Думайте о них, как если бы вы объявляли bean-компоненты старым способом XML.

  4. Проверьте BeanDefinitionBuilder и GenericBeanDefinition javadocsinition для получения дополнительной информации.

person fps    schedule 16.02.2015
comment
Есть ли способ повторно использовать один компонент (динамически созданный) внутри другого компонента при динамическом создании? - person Ashish Kumar; 17.07.2019

У вас должно получиться что-то вроде этого:

@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);
        }
     }

}
person micha    schedule 06.02.2015
comment
Это не сработает, потому что мне нужно создать bean-компоненты, внедрить их и использовать для создания других bean-компонентов. - person noah; 13.02.2015
comment
Очень хорошо работает с SpringBoot 2.x - person Marcello de Sales; 28.12.2019
comment
Большое спасибо за то, что вы сэкономили мне много времени! Я разместил аналогичный вопрос в разделе свойств загрузки в классе, который реализует ImportBeanDefinitionRegistrar, но ваше решение сработало для меня. Я только что реализовал BeanFactoryAware, и это позаботилось об этом. - person F. K.; 20.08.2020

Просто расширяю ответ Микаса - его решение работает, если я настроил его следующим образом:

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-компоненты как атрибуты класса конфигурации и инициализирую их в методе @PostConstruct. Таким образом, у меня есть объект, зарегистрированный как bean-компонент (так что @Autowire и @Inject работают, как ожидалось), и позже я могу использовать тот же экземпляр при внедрении конструктора для bean-компонентов, которые в нем нуждаются. Для видимости атрибута установлено значение protected, чтобы подклассы могли использовать созданные объекты.

Поскольку экземпляр, который мы храним, на самом деле не является прокси-сервером Spring, могут возникнуть некоторые проблемы (аспекты не запускаются и т. Д.). На самом деле может быть хорошей идеей получить компонент после его регистрации, например:

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

Я просто скинусь здесь. Другие упоминали, что вам нужно создать 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
   } 

..

person Richard    schedule 16.02.2015

Вам необходимо создать базовый класс конфигурации, который будет расширен всеми вашими Configuration классами. Затем вы можете перебрать все классы конфигурации следующим образом:

// 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.
}
person Mithun    schedule 13.02.2015

«Лучший» подход, который я мог придумать, заключался в том, чтобы обернуть всю мою конфигурацию Quartz и планировщики в 1 uber bean и подключить все это вручную, а затем провести рефакторинг кода для работы с интерфейсом uber bean.

Компонент uber создает все объекты, которые мне нужны, в своем PostConstruct и реализует ApplicationContextAware, чтобы он мог их автоматически связывать. Это не идеально, но это лучшее, что я мог придумать.

В Spring просто нет хорошего способа динамически добавлять компоненты безопасным способом.

person Community    schedule 17.02.2015