Injection de beans prototypes dans une instance Singleton au printemps

1. Vue d’ensemble

Dans cet article rapide, nous allons montrer différentes approches consistant à injecter des beans prototypes dans une instance singleton . Nous discuterons des cas d’utilisation et des avantages/inconvénients de chaque scénario.

Par défaut, les haricots de printemps sont des singletons. Le problème se pose lorsque nous essayons de câbler des haricots de différentes portées. Par exemple, un prototype de haricot dans un singleton. Ceci est connu sous le nom de problème d’injection de haricots scoped .

Pour en savoir plus sur les champs d’application de haricots, cet article est un bon point de départ .

2. Problème d’injection de haricot prototype

Pour décrire le problème, configurons les beans suivants:

@Configuration
public class AppConfig {

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE__PROTOTYPE)
    public PrototypeBean prototypeBean() {
        return new PrototypeBean();
    }

    @Bean
    public SingletonBean singletonBean() {
        return new SingletonBean();
    }
}

Notez que le premier haricot a une portée prototype, l’autre est un singleton.

Maintenant, injectons le bean au prototype dans le singleton - et exposons ensuite si via la méthode getPrototypeBean ()

public class SingletonBean {

   //..

    @Autowired
    private PrototypeBean prototypeBean;

    public SingletonBean() {
        logger.info("Singleton instance created");
    }

    public PrototypeBean getPrototypeBean() {
        logger.info(String.valueOf(LocalTime.now()));
        return prototypeBean;
    }
}

Ensuite, chargeons ApplicationContext et récupérons le bean singleton deux fois:

public static void main(String[]args) throws InterruptedException {
    AnnotationConfigApplicationContext context
      = new AnnotationConfigApplicationContext(AppConfig.class);

    SingletonBean firstSingleton = context.getBean(SingletonBean.class);
    PrototypeBean firstPrototype = firstSingleton.getPrototypeBean();

   //get singleton bean instance one more time
    SingletonBean secondSingleton = context.getBean(SingletonBean.class);
    PrototypeBean secondPrototype = secondSingleton.getPrototypeBean();

    isTrue(firstPrototype.equals(secondPrototype), "The same instance should be returned");
}

Voici la sortie de la console:

Singleton Bean created
Prototype Bean created
11:06:57.894//should create another prototype bean instance here
11:06:58.895
  • Les deux beans n’ont été initialisés qu’une fois, ** au démarrage du contexte de l’application.

3. Injection de ApplicationContext

Nous pouvons également injecter le ApplicationContext directement dans un bean.

  • Pour cela, utilisez l’annotation @ Autowire ou implémentez l’interface ApplicationContextAware : **

public class SingletonAppContextBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public PrototypeBean getPrototypeBean() {
        return applicationContext.getBean(PrototypeBean.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
      throws BeansException {
        this.applicationContext = applicationContext;
    }
}

Chaque fois que la méthode getPrototypeBean () est appelée, une nouvelle instance de PrototypeBean sera renvoyée par ApplicationContext .

  • Cependant, cette approche a de sérieux inconvénients. ** Elle contredit le principe d’inversion de contrôle, car nous demandons directement les dépendances au conteneur.

En outre, nous récupérons le bean prototype à partir de applicationContext dans la classe SingletonAppcontextBean . Cela signifie coupler le code au Spring Framework .

4. Méthode d’injection

Une autre façon de résoudre le problème consiste à injecter une méthode avec l’annotation @ Lookup :

@Component
public class SingletonLookupBean {

    @Lookup
    public PrototypeBean getPrototypeBean() {
        return null;
    }
}

Spring substitue la méthode getPrototypeBean () annotée avec @Lookup. Elle enregistre ensuite le bean dans le contexte de l’application.

Chaque fois que nous demandons la méthode getPrototypeBean () , elle retourne une nouvelle instance PrototypeBean .

  • Il utilisera CGLIB pour générer le bytecode ** chargé de récupérer le PrototypeBean à partir du contexte de l’application.

5. javax.inject API

La configuration ainsi que les dépendances requises sont décrites dans ce lien: article/spring-annotations-resource-inject-autowire[câblage de ressort].

Voici le haricot singleton:

public class SingletonProviderBean {

    @Autowired
    private Provider<PrototypeBean> myPrototypeBeanProvider;

    public PrototypeBean getPrototypeInstance() {
        return myPrototypeBeanProvider.get();
    }
}

Nous utilisons Provider interface pour injecter le haricot prototype. Pour chaque appel à la méthode getPrototypeInstance () , la méthode _myPrototypeBeanProvider . g et () renvoie une nouvelle instance de PrototypeBean_ .

6. Scoped Proxy

Par défaut, Spring conserve une référence à l’objet réel pour effectuer l’injection. Ici, nous créons un objet proxy pour connecter l’objet réel à l’objet dépendant.

Chaque fois que la méthode sur l’objet proxy est appelée, le proxy décide lui-même de créer une nouvelle instance de l’objet réel ou de réutiliser celle existante.

Pour le configurer, nous modifions la classe Appconfig afin d’ajouter une nouvelle annotation @ Scope :

@Scope(
  value = ConfigurableBeanFactory.SCOPE__PROTOTYPE,
  proxyMode = ScopedProxyMode.TARGET__CLASS)

Par défaut, Spring utilise la bibliothèque CGLIB pour sous-classer directement les objets.

Pour éviter l’utilisation de CGLIB, nous pouvons configurer le mode proxy avec _ScopedProxyMode . _ INTERFACES, afin d’utiliser le proxy dynamique JDK à la place.

7. ObjectFactory Interface

public class SingletonObjectFactoryBean {

    @Autowired
    private ObjectFactory<PrototypeBean> prototypeBeanObjectFactory;

    public PrototypeBean getPrototypeInstance() {
        return prototypeBeanObjectFactory.getObject();
    }
}

Regardons la méthode getPrototypeInstance () ; getObject () renvoie une nouvelle instance de PrototypeBean pour chaque demande. Ici, nous avons plus de contrôle sur l’initialisation du prototype.

De plus, ObjectFactory fait partie du framework; cela signifie qu’il faut éviter une configuration supplémentaire pour utiliser cette option.

8. Créer un bean à l’exécution à l’aide de java.util.Function

Une autre option consiste à créer les instances de prototype de bean à l’exécution, ce qui nous permet également d’ajouter des paramètres aux instances.

Pour voir un exemple, ajoutons un champ de nom à notre classe PrototypeBean :

public class PrototypeBean {
    private String name;

    public PrototypeBean(String name) {
        this.name = name;
        logger.info("Prototype instance " + name + " created");
    }

   //...
}

Ensuite, nous allons injecter une fabrique de haricots dans notre bean singleton en utilisant l’interface java.util.Function :

public class SingletonFunctionBean {

    @Autowired
    private Function<String, PrototypeBean> beanFactory;

    public PrototypeBean getPrototypeInstance(String name) {
        PrototypeBean bean = beanFactory.apply(name);
        return bean;
    }

}

Enfin, nous devons définir les haricots usine, prototype et singleton dans notre configuration:

@Configuration
public class AppConfig {
    @Bean
    public Function<String, PrototypeBean> beanFactory() {
        return name -> prototypeBeanWithParam(name);
    }

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

    @Bean
    public SingletonFunctionBean singletonFunctionBean() {
        return new SingletonFunctionBean();
    }
   //...
}

9. Essai

Écrivons maintenant un simple test JUnit pour exercer le cas avec l’interface ObjectFactory :

@Test
public void givenPrototypeInjection__WhenObjectFactory__ThenNewInstanceReturn() {

    AbstractApplicationContext context
     = new AnnotationConfigApplicationContext(AppConfig.class);

    SingletonObjectFactoryBean firstContext
     = context.getBean(SingletonObjectFactoryBean.class);
    SingletonObjectFactoryBean secondContext
     = context.getBean(SingletonObjectFactoryBean.class);

    PrototypeBean firstInstance = firstContext.getPrototypeInstance();
    PrototypeBean secondInstance = secondContext.getPrototypeInstance();

    assertTrue("New instance expected", firstInstance != secondInstance);
}

Après le lancement réussi du test, nous pouvons constater que chaque fois que la méthode getPrototypeInstance () est appelée, une nouvelle instance de bean prototype est créée.

10. Conclusion

Dans ce court tutoriel, nous avons appris plusieurs manières d’injecter le haricot prototype dans l’instance singleton.

Comme toujours, le code complet de ce tutoriel est disponible sur GitHub project