Подтвердить что ты не робот

Spring - программно сгенерировать набор beans

У меня есть приложение Dropwizard, которое должно генерировать дюжину или около того beans для каждой из конфигураций в списке конфигурации. Такие вещи, как проверки работоспособности, планировщики кварца и т.д.

Что-то вроде этого:

@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, для которых все нужны beans следующим образом. Сейчас я должен скопировать и вставить эти определения и переименовать их для каждой новой конфигурации.

Можно ли каким-то образом перебирать классы конфигурации и генерировать набор определений bean для каждого из них?

Мне было бы хорошо с подклассифицирующим решением или любым безопасным типом, не заставляя меня копировать и вставлять один и тот же код и переименовывать методы когда-нибудь, когда я должен добавить новую услугу.

EDIT: Я должен добавить, что у меня есть другие компоненты, которые зависят от этих beans (например, они вводят Collection<HealthCheck>).

4b9b3361

Ответ 1

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

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

Spring просто не имеет возможности динамически добавлять beans в виде файла.

Ответ 2

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

См. BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry() method javadocs. Это именно то, что вам нужно, потому что оно позволяет вам изменить <контекст приложения 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);
        }
    }

    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 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 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 для каждой конфигурации. Вы должны полагаться на какой-то шаблон именования, а затем набирать beans по имени по мере необходимости:

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

    ...

}

Примечания:

  • Если вы читаете имена конфигурации вручную из файла, используйте Spring ClassPathResource.getInputStream().

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

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

  • Проверьте BeanDefinitionBuilder javadocs и GenericBeanDefinition javadocs для более подробной информации.

Ответ 3

Вы должны сделать что-то вроде этого:

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

}

Ответ 4

Просто расширяя ответ на 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();
    }

}

Следует отметить, что я создаю пользовательские beans как атрибуты класса конфигурации и инициализируя их в методе @PostConstruct. Таким образом, у меня есть объект, зарегистрированный как bean (поэтому @Autowire и @Inject работают так, как ожидалось), и я могу позже использовать тот же экземпляр в инжекции конструктора для beans, который его требует. Видимость атрибута настроена на защиту, поэтому подклассы могут использовать созданные объекты.

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

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

Ответ 5

Я просто заберу здесь. Другие отметили, что вам нужно создать bean, в который вводится ваша конфигурация. Затем bean будет использовать вашу конфигурацию для создания других beans и вставить их в контекст (который вам также нужно будет вводить в той или иной форме).

То, что я не думаю, что кто-то еще поднял, - это то, что вы сказали, что другие beans будут зависеть от этих динамически созданных beans. Это означает, что ваш динамический bean factory должен быть создан до зависимого beans. Вы можете сделать это (в мире аннотаций) с помощью

@DependsOn("myCleverBeanFactory")

Что касается типа вашего умного bean factory, другие рекомендуют более эффективные способы сделать это. Но если я правильно помню, вы можете сделать что-то подобное в старом мире 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
   } 

..

Ответ 6

Вам необходимо создать базовый класс конфигурации, который будет расширен всеми вашими классами 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.
}