web-dev-qa-db-fra.com

Spring - Génération par programme d'un ensemble de haricots

J'ai une application Dropwizard qui doit générer une douzaine de beans pour chacune des configurations de la liste de configuration. Des choses comme les bilans de santé, les ordonnanceurs à quartz, etc.

Quelque chose comme ça:

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

J'ai plusieurs instances de MyConfiguration qui ont toutes besoin de beans comme celui-ci. Pour le moment, je dois copier et coller ces définitions et les renommer à chaque nouvelle configuration.

Puis-je en quelque sorte parcourir mes classes de configuration et générer un ensemble de définitions de beans pour chacune?

Je serais bien avec une solution de sous-classement ou tout ce qui est de type sûr sans me faire copier et coller le même code et renommer les méthodes chaque fois que je dois ajouter un nouveau service.

EDIT: Je devrais ajouter que j’ai d’autres composants qui dépendent de ces beans (ils injectent Collection<HealthCheck> par exemple).

22
noah

La «meilleure» approche que je pouvais proposer consistait à envelopper toutes mes configurations et ordonnanceurs Quartz dans 1 uber bean et à les relier manuellement, puis à refactoriser le code pour qu'il fonctionne avec l'interface uber bean.

Le bean uber crée tous les objets dont j'ai besoin dans PostConstruct et implémente ApplicationContextAware afin de pouvoir les connecter automatiquement. Ce n'est pas idéal, mais c'était le meilleur que je pouvais trouver.

Spring n'a tout simplement pas le bon moyen d'ajouter dynamiquement des haricots de manière sécurisée. 

0
noah

Vous devez donc déclarer les nouveaux haricots à la volée et les injecter dans le contexte d'application de Spring comme s'il s'agissait de haricots ordinaires, ce qui signifie qu'ils doivent être soumis à un proxy, un post-traitement, etc., c'est-à-dire qu'ils doivent être soumis au cycle de vie des haricots Spring. .

Veuillez consulter BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry(), méthode javadocs. Ceci est exactement ce dont vous avez besoin, car il vous permet de modifier le contexte d'application de Spring après le chargement des définitions de beans normales _ ​​mais/ avant qu'un bean unique ait été chargé. instancié.

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

Cela déclare efficacement les beans dont vous avez besoin et les injecte dans le contexte d'application de Spring, un ensemble de beans pour chaque configuration. Vous devez vous appuyer sur un certain {modèle de nommage} puis sur autowire vos beans par nom chaque fois que vous en avez besoin:

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

    ...

}

Remarques:

  1. Si vous lisez manuellement les noms de configuration à partir d'un fichier, utilisez la fonction ClassPathResource.getInputStream() de Spring.

  2. Si vous parcourez vous-même le classpath, je vous recommande fortement d'utiliser l'étonnante bibliothèque de réflexions .

  3. Vous devez définir manuellement toutes les propriétés et dépendances pour chaque définition de bean. Chaque définition de bean est indépendante des autres définitions de beans, c’est-à-dire que vous ne pouvez pas les réutiliser, les placer les unes dans les autres, etc. Pensez-y comme si vous déclariez des beans de l’ancienne méthode XML.

  4. Consultez javadocs BeanDefinitionBuilder et javadocs GenericBeanDefinition pour plus de détails.

32

Vous devriez pouvoir faire quelque chose comme ça:

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

}
6
micha

Je développe juste sur la réponse de Michas - sa solution fonctionne si je l’installe de la manière suivante:

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

}

Une chose à noter est que je crée les beans personnalisés en tant qu'attributs de la classe de configuration et les initialise dans la méthode @PostConstruct. De cette façon, l’objet est enregistré comme un haricot (donc, @Autowire et @Inject fonctionnent comme prévu) et je peux utiliser plus tard la même instance dans l’injection de constructeur pour les haricots qui le nécessitent. La visibilité de l'attribut est définie sur protégée afin que les sous-classes puissent utiliser les objets créés.

Comme l'instance que nous détenons n'est pas réellement le proxy Spring, certains problèmes peuvent survenir (aspects ne se déclenchant pas, etc.). Il peut être intéressant de récupérer le haricot après l’avoir enregistré, comme dans:

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

Je vais juste apporter ici. D’autres ont mentionné la nécessité de créer un haricot dans lequel votre configuration est injectée . Ce haricot utilisera ensuite votre configuration pour créer d’autres haricots et les insérer dans le contexte (vous devrez également procéder à une injection sous une forme ou une autre). un autre).

Ce que je ne pense pas que quiconque ait compris, c’est que vous avez dit que d’autres haricots dépendront de ces haricots créés dynamiquement . Cela signifie que votre usine de haricots dynamique doit être instanciée avant les haricots dépendants. Vous pouvez le faire (dans le monde des annotations) en utilisant 

@DependsOn("myCleverBeanFactory")

En ce qui concerne le type d’objet de votre fabrique de haricots intelligents, d’autres ont recommandé de meilleures méthodes pour le faire . Mais si je me souviens bien, vous pouvez le faire dans le vieux monde 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
   } 

..

2
Richard

Vous devez créer une classe de configuration de base qui est étendue à toutes vos classes Configuration. Ensuite, vous pouvez parcourir toutes les classes de configuration comme suit:

// 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.
}
0
Mithun