Spring Java Config: comment créer un @Bean à portée prototype avec des arguments d'exécution?

134

En utilisant Java Config de Spring, j'ai besoin d'acquérir / d'instancier un bean à portée prototype avec des arguments de constructeur qui ne peuvent être obtenus qu'au moment de l'exécution. Considérez l'exemple de code suivant (simplifié par souci de concision):

@Autowired
private ApplicationContext appCtx;

public void onRequest(Request request) {
    //request is already validated
    String 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 String name;

    @Autowired
    private SomeComponent someComponent;

    @Autowired
    private AnotherComponent anotherComponent;

    public Thing(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

Avis nameest final: il ne peut être fourni par un constructeur, et garantit immuabilité. Les autres dépendances sont des dépendances spécifiques à l'implémentation de la Thingclasse et ne doivent pas être connues (étroitement liées à) l'implémentation du gestionnaire de requêtes.

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

<bean id="thing", class="com.whatever.Thing" scope="prototype">
    <!-- other post-instantiation properties omitted -->
</bean>

Comment obtenir la même chose avec la configuration Java? Ce qui suit ne fonctionne pas avec Spring 3.x:

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

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

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

Mais cela va à l' encontre de l'intérêt d'utiliser 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 faire cela, je pourrais éviter:

  • définition d'une interface Factory
  • définition d'une implémentation Factory
  • rédaction de tests pour l'implémentation Factory

C'est une tonne de travail (relativement parlant) pour quelque chose d'aussi trivial que Spring prend déjà en charge via la configuration XML.

Les Hazlewood
la source
15
Excellente question.
Sotirios Delimanolis
Cependant, y a-t-il une raison pour laquelle vous ne pouvez pas simplement instancier la classe vous-même et devez l'obtenir à partir de Spring? At-il des dépendances sur d'autres beans?
Sotirios Delimanolis
@SotiriosDelimanolis oui, l' Thingimplémentation est en fait plus complexe et a des dépendances sur d'autres beans (je les ai juste omis par souci de concision). En tant que tel, je ne veux pas que l'implémentation du gestionnaire de requêtes les sache, car cela couplerait étroitement le gestionnaire aux API / beans dont il n'a pas besoin. Je mettrai à jour la question pour refléter votre (excellente) question.
Les Hazlewood
Je ne suis pas sûr que Spring autorise cela sur un constructeur, mais je sais que vous pouvez mettre des @Qualifierparamètres à un setter avec @Autowiredsur le setter lui-même.
CodeChimp
2
Au printemps 4, votre exemple avec @Beanfonctionne. La @Beanméthode est appelée avec les arguments appropriés que vous avez passés getBean(..).
Sotirios Delimanolis

Réponses:

94

Dans une @Configurationclasse, une @Beanméthode comme celle-ci

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

est utilisé pour enregistrer une définition de bean et fournir la fabrique pour créer le bean . Le bean qu'il définit n'est instancié que sur demande en utilisant des arguments qui sont déterminés soit directement, soit en analysant cela ApplicationContext.

Dans le cas d'un prototypebean, un nouvel objet est créé à chaque fois et donc la @Beanméthode correspondante est également exécutée.

Vous pouvez récupérer un bean de la ApplicationContextvia sa BeanFactory#getBean(String name, Object... args)méthode qui indique

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

Paramètres:

args arguments à 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 tous les autres cas.

En d'autres termes, pour ce prototypebean de portée, vous fournissez les arguments qui seront utilisés, non pas dans le constructeur de la classe du bean, mais dans l' @Beanappel de la méthode.

C'est au moins vrai pour les versions Spring 4+.

Sotirios Delimanolis
la source
12
Mon problème avec cette approche est que vous ne pouvez pas limiter la @Beanméthode à l'appel manuel. Si jamais @Autowire Thingla @Beanméthode sera appelée, elle mourra probablement de ne pas pouvoir injecter le paramètre. Même chose si vous @Autowire List<Thing>. J'ai trouvé cela un peu fragile.
Jan Zyka
@JanZyka, y a-t-il un moyen pour que je puisse autowire Thing autre que ce qui est décrit dans ces réponses (qui sont toutes essentiellement les mêmes si vous plissez les yeux). Plus précisément, si je connais les arguments à l'avance (au moment de la compilation / configuration), y a-t-il un moyen d'exprimer ces arguments dans une annotation avec laquelle je peux qualifier le @Autowire?
M. Prokhorov
52

Avec Spring> 4.0 et Java 8, vous pouvez le faire avec plus de sécurité de type:

@Configuration    
public class ServiceConfig {

    @Bean
    public Function<String, Thing> thingFactory() {
        return name -> thing(name); // or this::thing
    } 

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

}

Usage:

@Autowired
private Function<String, Thing> thingFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = thingFactory.apply(name);

    // ...
}

Vous pouvez désormais récupérer votre bean au moment de l'exécution. C'est un modèle 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 custom @FunctionalInterfacepour passer plus de deux paramètres).

Golyshev romain
la source
1
Je trouve cette approche très utile et propre. Merci!
Alex Objelean
1
Qu'est-ce qu'un tissu? Je comprends votre utilisation ... mais pas la terminologie ... je ne pense pas avoir entendu parler du "motif de tissu"
AnthonyJClink
1
@AnthonyJClink Je suppose que je viens de l'utiliser à la fabricplace de factory, mon mauvais :)
Roman Golyshev
1
@AbhijitSarkar oh, je vois. Mais vous ne pouvez pas passer un paramètre à a Providerou à an ObjectFactory, ou est-ce que je me trompe? Et dans mon exemple, vous pouvez lui passer un paramètre de chaîne (ou n'importe quel paramètre)
Roman Golyshev
2
Si vous ne voulez pas (ou n'avez pas besoin) d'utiliser les méthodes de cycle de vie des haricots printemps (qui sont différentes pour les haricots prototypes ...), vous pouvez ignorer @Beanet Scopeannotations sur la Thing thingméthode. De plus cette méthode peut être rendue privée pour se cacher et ne laisser que l'usine.
m52509791
17

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

ObjectProvider - Il vous permet simplement de l'ajouter en tant que dépendance à votre bean à portée Prototype "argumenté" et de l'instancier en utilisant l'argument.

Voici un exemple simple de son utilisation:

@Configuration
public class MyConf {
    @Bean
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public MyPrototype createPrototype(String arg) {
        return new MyPrototype(arg);
    }
}

public class MyPrototype {
    private String arg;

    public MyPrototype(String arg) {
        this.arg = arg;
    }

    public void action() {
        System.out.println(arg);
    }
}


@Component
public class UsingMyPrototype {
    private ObjectProvider<MyPrototype> myPrototypeProvider;

    @Autowired
    public UsingMyPrototype(ObjectProvider<MyPrototype> myPrototypeProvider) {
        this.myPrototypeProvider = myPrototypeProvider;
    }

    public void usePrototype() {
        final MyPrototype myPrototype = myPrototypeProvider.getObject("hello");
        myPrototype.action();
    }
}

Cela affichera bien sûr la chaîne Hello lors de l'appel de usePrototype.

David Barda
la source
15

MISE À JOUR par commentaire

Premièrement, je ne sais pas pourquoi vous dites "cela ne fonctionne pas" pour quelque chose qui fonctionne très bien dans Spring 3.x. Je soupçonne que quelque chose ne va pas dans votre configuration quelque part.

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(String 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.toString());
    singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    TransferService transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter One");
    System.out.println(transferService.toString());
    transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter Two");
    System.out.println(transferService.toString());
}

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

Ainsi, le Bean «Singleton» est demandé deux fois. Cependant, comme on pouvait 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'objet existant. Le constructeur (méthode @Bean) n'est pas invoqué une seconde fois. Par respect pour cela, lorsque le Bean 'Prototype' est demandé deux fois au même objet 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 transmettre toutes ces références au constructeur. Cela permettra à la classe créée d'être un pur POJO et de rendre les objets de référence contenus immuables comme ils devraient l'être. Ainsi, le service de transfert pourrait ressembler à quelque chose comme:

public class TransferServiceImpl implements TransferService {

    private final String name;

    private final AccountRepository accountRepository;

    public TransferServiceImpl(AccountRepository accountRepository, String 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 très heureux d'avoir créé les classes sans tous les @Autowired. Si vous avez besoin de composants auto-câblés, conservez-les en local dans les fichiers de configuration java.

Cela appellera la méthode ci-dessous dans BeanFactory. Notez dans la description comment cela est destiné à votre cas d'utilisation exact.

/**
 * Return an instance, which may be shared or independent, of the specified bean.
 * <p>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 retrieve
 * @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(String name, Object... args) throws BeansException;
JoeG
la source
3
Merci pour la réponse! Cependant, je pense que vous avez mal compris la question. La partie la plus importante de la question est qu'une valeur d'exécution doit être fournie comme argument de constructeur lors de l'acquisition (instanciation) du prototype.
Les Hazlewood
J'ai mis à jour ma réponse. En fait, il semblait que la gestion de la valeur d'exécution avait été effectuée correctement, alors j'ai laissé cette partie de côté. Il est cependant explicitement pris en charge comme vous pouvez le voir dans les mises à jour et la sortie du programme.
JoeG
0

Vous pouvez obtenir un effet similaire simplement en utilisant une classe interne :

@Component
class ThingFactory {
    private final SomeBean someBean;

    ThingFactory(SomeBean someBean) {
        this.someBean = someBean;
    }

    Thing getInstance(String name) {
        return new Thing(name);
    }

    class Thing {
        private final String name;

        Thing(String name) {
            this.name = name;
        }

        void foo() {
            System.out.format("My name is %s and I can " +
                    "access bean from outer class %s", name, someBean);
        }
    }
}
pmartycz
la source
0

dans votre fichier XML de beans, utilisez l'attribut scope = "prototype"

manpreet singh
la source
-1

Réponse tardive avec une approche légèrement différente. C'est un suivi de cette question récente qui renvoie cette question elle-même.

Oui, comme cela a été dit, vous pouvez déclarer le bean prototype qui accepte un paramètre dans une @Configurationclasse qui permet de créer un nouveau bean à chaque injection.
Cela fera de cette @Configuration classe une usine et pour ne pas donner trop de responsabilités à cette usine, cela ne devrait pas inclure d'autres grains.

@Configuration    
public class ServiceFactory {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Thing thing(String name) {
       return new Thing(name);
   }

}

Mais vous pouvez également injecter ce bean de configuration pour créer Things:

@Autowired
private ServiceFactory serviceFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = serviceFactory.thing(name); // create a new bean at each invocation
    // ...    
}

Il est à la fois sûr de type et concis.

davidxxx
la source
1
Merci pour la réponse, mais c'est un anti-motif de printemps. Les objets de configuration ne doivent pas «fuir» dans le code de l'application - ils existent pour configurer le graphique de votre objet d'application et l'interface avec les constructions Spring. Cela s'apparente aux classes XML dans vos beans d'application (c'est-à-dire un autre mécanisme de configuration). Autrement dit, si Spring propose un autre mécanisme de configuration, vous devrez refactoriser le code de votre application - un indicateur clair que cela viole la séparation des préoccupations. Il est préférable que votre Config crée des instances d'une interface Usine / Fonction et injecte l'Usine - pas de couplage étroit avec la configuration.
Les Hazlewood
1) Je suis tout à fait d'accord que dans le cas général, les objets de configuration ne doivent pas fuir en tant que champ. Mais dans ce cas précis, injecter un objet de configuration qui définit un et un seul bean pour produire des haricots prototypes, IHMO cela prend tout son sens: cette classe de configuration devient une usine. Où est la question de la séparation des préoccupations si elle ne fait que cela? ...
davidxxx
... 2) À propos de "Autrement dit, si Spring propose un autre mécanisme de configuration", c'est un argument erroné car lorsque vous décidez d'utiliser un framework dans votre application, vous associez votre application à cela. Donc, dans tous les cas, vous devrez également refactoriser toutes les applications Spring qui dépendent de la @Configurationmodification de ce mécanisme.
davidxxx
1
... 3) La réponse que vous avez acceptée propose d'utiliser BeanFactory#getBean(). Mais c'est bien pire en termes de couplage puisque c'est une usine qui permet d'obtenir / instancier n'importe quel bean de l'application et pas seulement celui dont le bean actuel a besoin. Avec une telle utilisation vous pouvez mélanger les responsabilités de votre classe très facilement puisque les dépendances qu'elle peut extraire sont illimitées, ce qui n'est vraiment pas conseillé mais cas exceptionnel.
davidxxx
@ davidxxx - J'ai accepté la réponse il y a des années, avant que JDK 8 et Spring 4 ne soient de facto. La réponse de Roman ci-dessus est plus correcte pour les utilisations modernes du printemps. En ce qui concerne votre déclaration "parce que lorsque vous décidez d'utiliser un framework dans votre application, vous associez votre application à cela" est assez antithétique aux recommandations de l'équipe Spring et aux meilleures pratiques Java Config - demandez à Josh Long ou Jeurgen Hoeller si vous obtenez un chance de leur parler en personne (j'ai, et je peux vous assurer qu'ils conseillent explicitement de ne pas coupler le code de votre application à Spring autant que possible). À votre santé.
Les Hazlewood