web-dev-qa-db-fra.com

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

À l'aide de la configuration Java 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. Prenons 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 Chose 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;
    }
}

La remarque name est final: elle ne peut être fournie que par 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 doivent pas être connues (étroitement associées à) l'implémentation du gestionnaire de demandes.

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 puis-je obtenir la même chose avec Java config? 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 défait tout le point 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 le faire, je pourrais éviter:

  • définir une interface d'usine
  • définir une implémentation d'usine
  • écrire des tests pour l'implémentation de l'usine

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.

107
Les Hazlewood

Dans une classe @Configuration, une méthode @Bean 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 à l'aide d'arguments déterminés directement ou par analyse de ApplicationContext

Dans le cas d'un bean prototype, un nouvel objet est créé à chaque fois et, par conséquent, la méthode @Bean correspondante est également exécutée. 

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

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

Paramètres:

args arguments à utiliser si vous créez un prototype en utilisant des arguments explicites à une méthode d'usine statique. Il n'est pas valide d'utiliser une valeur args non nulle dans tout autre cas.

En d'autres termes, pour ce bean prototype scoped, vous fournissez les arguments qui seront utilisés, non 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 de Spring 4+.

79

Avec Spring> 4.0 et Java 8, vous pouvez le faire de manière plus sécurisée: 

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

    // ...
}

Alors maintenant, vous pouvez obtenir votre haricot au moment de l'exécution. Ceci est bien sûr un modèle d’usine, mais vous pouvez gagner un peu de temps sur l’écriture d’une classe spécifique telle que ThingFactory (vous devrez cependant écrire @FunctionalInterface personnalisé pour transmettre plus de deux paramètres).

31
Roman Golyshev

MISE À JOUR par commentaire

Premièrement, je ne suis pas sûr de savoir pourquoi vous dites "cela ne fonctionne pas" pour quelque chose qui fonctionne parfaitement 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());
}

Spring 3.2.8 et Java 7 donnent les résultats suivants:

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

Donc, le haricot 'Singleton' est demandé deux fois. Cependant, comme on pouvait s'y attendre, le printemps ne le crée qu'une fois. La deuxième fois, il voit qu'il a ce haricot et renvoie simplement l'objet 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 objet de contexte, nous voyons que la référence change dans la sortie ET que le constructeur (méthode @Bean) IS a invoqué deux fois.

La question est donc de savoir comment injecter un singleton dans un prototype. La classe de configuration ci-dessus montre comment faire cela aussi! Vous devriez 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 tellement heureux d'avoir créé les classes sans tous les @Autowired. Si vous avez besoin de composants câblés automatiquement, conservez-les dans les fichiers de configuration Java.

Ceci 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;
14
JoeG

Depuis le printemps 4.3, il existe une nouvelle façon de procéder, qui a été cousue pour cette question.

ObjectProvider - Il vous permet simplement de l'ajouter en tant que dépendance à votre bean de portée "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(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();
    }
}

Ceci va bien sûr imprimer une chaîne de caractères lors de l'appel de usePrototype.

2
David Barda

Vous pouvez obtenir un effet similaire en utilisant simplement un inner class :

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