Spring Java Config: comment créer un @Bean prototype avec des arguments d’exécution?

En utilisant Java Config de Spring, je dois acquérir / instancier un bean prototype dont les arguments de constructeur ne sont disponibles qu’au moment de l’exécution. Prenons l’exemple de code suivant (simplifié pour simplifier):

@Autowired private ApplicationContext appCtx; public void onRequest(Request request) { //request is already validated Ssortingng name = request.getParameter("name"); Thing thing = appCtx.getBean(Thing.class, name); //System.out.println(thing.getName()); //prints name } 

où la classe Thing est définie comme suit:

 public class Thing { private final Ssortingng name; @Autowired private SomeComponent someComponent; @Autowired private AnotherComponent anotherComponent; public Thing(Ssortingng name) { this.name = name; } public Ssortingng getName() { return this.name; } } 

Le name avis est final : il ne peut être fourni que via un constructeur et garantit l’immuabilité. Les autres dépendances sont des dépendances spécifiques à l’implémentation de la classe Thing et ne devraient pas être connues de (étroitement liées à) l’implémentation du gestionnaire de requêtes.

Ce code fonctionne parfaitement avec la configuration XML de Spring, par exemple:

    

Comment puis-je obtenir la même chose avec la configuration Java? Ce qui suit ne fonctionne pas:

 @Bean @Scope("prototype") public Thing thing(Ssortingng name) { return new Thing(name); } 

Maintenant, je pourrais créer une usine, par exemple:

 public interface ThingFactory { public Thing createThing(Ssortingng name); } 

Mais cela va à l’encontre de l’utilisation de Spring pour remplacer le modèle de conception ServiceLocator et Factory , ce qui serait idéal pour ce cas d’utilisation.

Si Spring Java Config pouvait le faire, je pourrais éviter:

  • définir une interface d’usine
  • définir une implémentation Factory
  • rédaction de tests pour l’implémentation Factory

C’est une tonne de travail (relativement parlant) pour quelque chose de si sortingvial que Spring prend déjà en charge via XML Config.

    Dans une classe @Configuration , une méthode @Bean comme ça

     @Bean @Scope("prototype") public Thing thing(Ssortingng name) { return new Thing(name); } 

    est utilisé pour enregistrer une définition de haricot et fournir l’usine pour créer le haricot . Le bean qu’il définit n’est instancié que sur requête à l’aide d’arguments déterminés directement ou via l’parsing de ce ApplicationContext .

    Dans le cas d’un bean prototype , un nouvel object est créé à chaque fois et la méthode @Bean correspondante est donc également exécutée.

    Vous pouvez récupérer un bean depuis ApplicationContext via sa BeanFactory#getBean(Ssortingng name, Object... args) qui indique

    Permet de spécifier des arguments de constructeur explicites / des arguments de méthode de fabrique, en remplaçant les arguments par défaut spécifiés (le cas échéant) dans la définition du bean.

    Paramètres:

    arguments args à utiliser si vous créez un prototype en utilisant des arguments explicites pour une méthode de fabrique statique. Il est invalide d’utiliser une valeur args non nulle dans les autres cas.

    En d’autres termes, pour ce prototype bean de scope, vous fournissez les arguments qui seront utilisés, pas dans le constructeur de la classe de bean, mais dans l’invocation de la méthode @Bean .

    Ceci est au moins vrai pour les versions Spring 4+.

    Avec Spring> 4.0 et Java 8, vous pouvez le faire plus en toute sécurité:

     @Configuration public class ServiceConfig { @Bean public Function thingFactory() { return name -> thing(name); // or this::thing } @Bean @Scope(value = "prototype") public Thing thing(Ssortingng name) { return new Thing(name); } } 

    Usage:

     @Autowired private Function thingFactory; public void onRequest(Request request) { //request is already validated Ssortingng name = request.getParameter("name"); Thing thing = thingFactory.apply(name); // ... } 

    Alors maintenant, vous pouvez obtenir votre bean à l’exécution. Ceci est un motif d’usine bien sûr, mais vous pouvez gagner du temps sur l’écriture d’une classe spécifique comme ThingFactory (cependant, vous devrez écrire une @FunctionalInterface personnalisée @FunctionalInterface pour transmettre plus de deux parameters).

    MISE À JOUR par commentaire

    D’abord, je ne sais pas pourquoi vous dites “cela ne fonctionne pas” pour quelque chose qui fonctionne bien dans Spring 3.x. Je pense que quelque chose ne va pas dans votre configuration.

    Cela marche:

    – Fichier de configuration:

     @Configuration public class ServiceConfig { // only here to demo execution order private int count = 1; @Bean @Scope(value = "prototype") public TransferService myFirstService(Ssortingng param) { System.out.println("value of count:" + count++); return new TransferServiceImpl(aSingletonBean(), param); } @Bean public AccountRepository aSingletonBean() { System.out.println("value of count:" + count++); return new InMemoryAccountRepository(); } } 

    – Fichier de test à exécuter:

     @Test public void prototypeTest() { // create the spring container using the ServiceConfig @Configuration class ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfig.class); Object singleton = ctx.getBean("aSingletonBean"); System.out.println(singleton.toSsortingng()); singleton = ctx.getBean("aSingletonBean"); System.out.println(singleton.toSsortingng()); TransferService transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter One"); System.out.println(transferService.toSsortingng()); transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter Two"); System.out.println(transferService.toSsortingng()); } 

    En utilisant Spring 3.2.8 et Java 7, donne cette sortie:

     value of count:1 com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d value of count:2 Using name value of: simulated Dynamic Parameter One com.spring3demo.account.service.TransferServiceImpl@634d6f2c value of count:3 Using name value of: simulated Dynamic Parameter Two com.spring3demo.account.service.TransferServiceImpl@70bde4a2 

    Le bean ‘Singleton’ est donc demandé deux fois. Cependant, comme on peut s’y attendre, Spring ne le crée qu’une seule fois. La deuxième fois, il voit qu’il a ce bean et renvoie simplement l’object existant. Le constructeur (méthode @Bean) n’est pas appelé une seconde fois. Par respect pour cela, lorsque le bean ‘Prototype’ est demandé deux fois au même object de contexte, nous voyons que la référence change dans la sortie ET que le constructeur (méthode @Bean) est appelé deux fois.

    Alors, la question est de savoir comment injecter un singleton dans un prototype. La classe de configuration ci-dessus montre comment faire cela aussi! Vous devez passer toutes ces références dans le constructeur. Cela permettra à la classe créée d’être un POJO pur et de rendre les objects de référence contenus immuables comme ils le devraient. Ainsi, le service de transfert pourrait ressembler à:

     public class TransferServiceImpl implements TransferService { private final Ssortingng name; private final AccountRepository accountRepository; public TransferServiceImpl(AccountRepository accountRepository, Ssortingng name) { this.name = name; // system out here is only because this is a dumb test usage System.out.println("Using name value of: " + this.name); this.accountRepository = accountRepository; } .... } 

    Si vous écrivez des tests unitaires, vous serez tellement heureux que vous ayez créé ces classes sans tous les @Autowired. Si vous avez besoin de composants automatiques, conservez ceux-ci dans les fichiers de configuration java.

    Cela appellera la méthode ci-dessous dans la BeanFactory. Notez dans la description comment cela est prévu pour votre cas d’utilisation exact.

     /** * Return an instance, which may be shared or independent, of the specified bean. * 

    Allows for specifying explicit constructor arguments / factory method arguments, * overriding the specified default arguments (if any) in the bean definition. * @param name the name of the bean to resortingeve * @param args arguments to use if creating a prototype using explicit arguments to a * static factory method. It is invalid to use a non-null args value in any other case. * @return an instance of the bean * @throws NoSuchBeanDefinitionException if there is no such bean definition * @throws BeanDefinitionStoreException if arguments have been given but * the affected bean isn't a prototype * @throws BeansException if the bean could not be created * @since 2.5 */ Object getBean(Ssortingng name, Object... args) throws BeansException;

    Depuis le spring 4.3, il existe une nouvelle façon de procéder, qui a été cousue pour ce problème.

    ObjectProvider – Il vous permet juste de l’append en tant que dépendance à votre haricot Prototype “argumenté” et de l’instancier en utilisant l’argument

    Voici un exemple simple d’utilisation:

     @Configuration public class MyConf { @Bean @Scope(BeanDefinition.SCOPE_PROTOTYPE) public MyPrototype createPrototype(Ssortingng arg) { return new MyPrototype(arg); } } public class MyPrototype { private Ssortingng arg; public MyPrototype(Ssortingng arg) { this.arg = arg; } public void action() { System.out.println(arg); } } @Component public class UsingMyPrototype { private ObjectProvider myPrototypeProvider; @Autowired public UsingMyPrototype(ObjectProvider myPrototypeProvider) { this.myPrototypeProvider = myPrototypeProvider; } public void usePrototype() { final MyPrototype myPrototype = myPrototypeProvider.getObject("hello"); myPrototype.action(); } } 

    Cela imprimera bien sûr la chaîne hello lors de l’appel à usePrototype.