web-dev-qa-db-fra.com

Pourquoi la méthode @PostConstruct n'est-elle pas appelée lors du câblage automatique d'un bean prototype avec un argument constructeur

J'ai un bean prototype-scope, que je souhaite recevoir par annotation @Autowired. Dans ce bean, il y a aussi la méthode @PostConstruct qui n'est pas appelée par Spring et je ne comprends pas pourquoi.

Ma définition de haricot:

package somepackage;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;

@Component
@Scope("prototype")
public class SomeBean {

    public SomeBean(String arg) {
        System.out.println("Constructor called, arg: " + arg);
    }

    @PostConstruct
    private void init() {
        System.out.println("Post construct called");
    }

}

Classe JUnit où je veux injecter un haricot:

package somepackage;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@ContextConfiguration("classpath*:applicationContext-test.xml")
public class SomeBeanTest {

    @Autowired
    ApplicationContext ctx;

    @Autowired
    @Value("1")
    private SomeBean someBean;

    private SomeBean someBean2;

    @Before
    public void setUp() throws Exception {
        someBean2 = ctx.getBean(SomeBean.class, "2");
    }

    @Test
    public void test() {
        System.out.println("test");
    }
}

Configuration de printemps:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="somepackage"/>

</beans>

La sortie de l'exécution:

Constructor called, arg: 1
Constructor called, arg: 2
Post construct called
test

Lorsque j'initialise un bean en appelant getBean à partir de ApplicationContext, tout fonctionne comme prévu. Ma question est de savoir pourquoi l’injection de bean par @Autowire et @Value n’appelle pas la méthode @PostConstruct

19
gerard

Pourquoi @Value est-il utilisé au lieu de @Autowired?

L'annotation @Value est utilisée pour injecter des valeurs et a normalement comme chaînes de destination, primitives, types boxed et collections Java.

Selon documentation de Spring :

L'annotation @Value peut être placée sur des champs, des méthodes et des paramètres de méthode/constructeur pour spécifier une valeur par défaut.

Value reçoit une expression de chaîne utilisée par spring pour gérer la conversion en objet de destination. Cette conversion peut s'effectuer via la conversion de type de Spring , le éditeur de propriété de bean Java , et la Expression de SpEL de Spring . L'objet résultant de cette conversion, en principe, n'est pas géré par spring (même si vous pouvez renvoyer un bean déjà géré à partir de l'une de ces méthodes).

Par contre, le AutowiredAnnotationBeanPostProcessor est un

Implémentation BeanPostProcessor qui envoie automatiquement les champs annotés, les méthodes de définition et les méthodes de configuration arbitraires. Les membres à injecter sont détectés via une annotation Java 5: par défaut, les annotations @Autowired et @Value de Spring.

Cette classe gère l’injection de champ, résout les dépendances et appelle finalement la méthode doResolveDependency , dans cette méthode où la 'priorité' de l’injection est résolue, springs vérifie si une valeur sugestionnée est présente normalement une chaîne d'expression, cette valeur sugestionnée est le contenu de l'annotation Value; ainsi, s'il est présent, un appel à la classe SimpleTypeConverter est effectué, sinon spring cherche des beans candicate et résout le fil automatique.

Simplement, la raison pour laquelle @Autowired est ignoré et @Value est utilisée, c'est parce que la stratégie d'injection de valeur est vérifiée en premier. De toute évidence, il doit toujours être une priorité, spring pourrait également émettre une exception lorsque plusieurs annotations en conflit sont utilisées, mais dans ce cas, il est déterminé par le contrôle précédent effectué sur la valeur succombée. 

Je n'ai rien trouvé qui soit en rapport avec cette "priorité", c'est le printemps, mais c'est simple, car il n'est pas destiné à utiliser ces annotations ensemble, tout comme, par exemple, il n'est pas destiné à utiliser @Autowired et @Resource ensemble.


Pourquoi @Value crée une nouvelle intance de l'objet

Auparavant, je disais que la classe SimpleTypeConverter était appelée lorsque la valeur suggérée était présente, l'appel spécifique était destiné à la méthode convertIfNecessary , c'est celle qui effectue la conversion de la chaîne en objet de destination. être fait avec l'éditeur de propriété ou un convertisseur personnalisé, mais aucun de ceux-ci n'est utilisé ici. Une expression SpEL n'est pas utilisée non plus, mais un littéral de chaîne. 

Spring vérifie d’abord si l’objet de destination est une chaîne ou une collection/un tableau (peut convertir par exemple une liste délimitée par des virgules). n’est pas une interface mais une classe, il vérifie l’existence d’une Constructor(String) pour créer finalement l’objet (non géré par spring). Fondamentalement, ce convertisseur essaie de nombreuses manières différentes de convertir la chaîne en objet final.

Cette instanciation ne fonctionnera qu'en utilisant une chaîne en tant qu'argument. Si vous utilisez, par exemple, une expression SpEL pour renvoyer un @Value("#{2L}") long et que vous utilisez un objet avec un Constructor(Long), il lancera un IllegalStateException avec un message similaire:

Impossible de convertir la valeur de type 'Java.lang.Long' en type requis 'com.fiberg.test.springboot.object.Hut': aucun éditeur ni stratégie de conversion ne correspond


Solution possible

Utilisation d'une simple classe @Configuration en tant que fournisseur.

public class MyBean {
    public MyBean(String myArg) { /* ... */ }
    // ...
    @PostConstruct public init() { /* ... */ }
}

@Configuration
public class MyBeanSupplier {
    @Lazy
    @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE, 
           proxyMode = ScopedProxyMode.NO)
    public MyBean getMyBean(String myArg) {
        return new MyBean(myArg);
    }
}

Vous pouvez définir MyBean en tant que classe statique dans la classe MyBeanSupplier si c'est la seule méthode dont il dispose. De plus, vous ne pouvez pas utiliser le mode proxy ScopedProxyMode.TARGET_CLASS, car vous devrez fournir les arguments sous forme de beans et les arguments passés à getMyBean seraient ignorés.

Avec cette approche, vous ne seriez pas en mesure de transférer automatiquement le bean lui-même, mais au lieu de cela, vous feriez le transfert automatique du fournisseur, puis appelez la méthode get.

// ...
public class SomeBeanTest {
    @Autowired private MyBeanSupplier supplier;
    // ...
    public void setUp() throws Exception {
        someBean = supplier.getMyBean("2");
    }
}

Vous pouvez également créer le bean en utilisant le contexte de l'application.

someBean = ctx.getBean(SomeBean.class, "2");

Et la méthode @PostConstruct doit être appelée quelle que soit celle que vous utilisez, mais @PreDestroy _ { n'est pas appelé dans les beans prototypes }.

9
Jose Da Silva

J'ai lu les journaux de débogage et la trace de pile pour les deux scénarios plusieurs fois et mes observations sont les suivantes: -

  1. Lorsqu'il crée le bean dans le cas de @Autowire, il finit par injecter une valeur au constructeur via l'utilisation de certains convertisseurs. Voir capture d'écran ci-dessous: -

 converters are used

  1. Le @Autowire est inefficace. Donc, dans votre code, si vous supprimez même @Autowired, cela fonctionnera toujours. Par conséquent, la prise en charge de n ° 1 lorsque @Value est utilisée sur une propriété a essentiellement créé l’objet.

Solution:- 

Vous devriez recevoir un haricot avec le nom arg avec la valeur que vous souhaitez. Par exemple. J'ai préféré utiliser la classe de configuration (vous pouvez créer le bean dans le fichier de contexte) et ai fait ci-dessous: -

@Configuration
public class Configurations {

    @Bean
    public String arg() {
        return "20";
    }
}

Alors la classe de test serait comme ci-dessous (Notez que vous pouvez utiliser change ContextConfiguration pour utiliser classpath afin de lire le fichier de contexte): -

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SomeBean.class, Configurations.class})
public class SomeBeanTest {

    @Autowired
    ApplicationContext ctx;

    @Autowired
    String arg;

    @Autowired
    private SomeBean someBean;

    private SomeBean someBean2;

    @Before
    public void setUp() throws Exception {
        someBean2 = ctx.getBean(SomeBean.class, "2");
    }

    @Test
    public void test() {
        System.out.println("\n\n\n\ntest" + someBean.getName());
    }
}

J'apprendrais donc aussi à faire attention à l'utilisation de @Value, car il pourrait être trompeur d'aider le câblage automatique en injectant de la valeur à partir d'un bean printanier créé en arrière-plan et de rendre les applications inutilisables.

2
Vinay Prajapati

Lorsque vous exécutez le test, un nouveau bean est créé pour le test (c'est-à-dire pas la classe SomeBean, la classe SomeBeanTest). @Value sera instanciée en tant que valeur membre (pas un bean) et, par conséquent, BeanPostProcessor par défaut (AutowiredAnnotationBeanPostProcessor) ne tentera pas de l'initialiser.

Pour montrer que j'ai déplacé votre System.out.println () vers log.info () (garder les lignes synchronisées). L'activation de la journalisation au niveau du débogage indique:

DEBUG org.springframework.beans.factory.annotation.InjectionMetadata - Élément de traitement injecté du bean 'somepackage.SomeBeanTest': AutowiredFieldElement pour org.springframework.context.ApplicationContext somepackage.SomeBeanTest.ctx

DEBUG org.springframework.core.annotation.AnnotationUtils - Echec de l'annotation méta-introspect [interface org.springframework.beans.factory.annotation.Autowired]: Java.lang.NullPointerException

DEBUG org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Transfert automatique par type à partir du nom de bean nommé 'somepackage.SomeBeanTest' vers le bean nommé 'org.springframework.context.support.GenericApplicationContext@39c0f4a'

DEBUG org.springframework.beans.factory.annotation.InjectionMetadata - Élément de traitement du bean injecté 'somepackage.SomeBeanTest': AutowiredFieldElement pour un objet privé somepackage.SomeBean somepackage.SomeBeanTest.someBean

DEBUG org.springframework.core.annotation.AnnotationUtils - Echec de l'annotation méta-introspect [interface org.springframework.beans.factory.annotation.Value]: Java.lang.NullPointerException

DEBUG org.springframework.beans.BeanUtils - Aucun éditeur de propriété [somepackage.SomeBeanEditor] trouvé pour le type somepackage.SomeBean selon la convention de suffixe 'Editeur'

INFO somepackage.SomeBean - Constructeur appelé, arg: 0

DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Avant la méthode de test: ....

INFO somepackage.SomeBeanTest - test

DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Après la méthode de test:

Une solution de contournement qui fonctionne consiste à initialiser le bean manuellement:

@Value("0")
private SomeBean someBean;

@PostConstruct
private void customInit() {
    log.info("Custom Init method called");
    someBean.init();
}

Ce qui produira:

INFO somepackage.SomeBean - Constructeur appelé, arg: 0

INFO somepackage.SomeBeanTest - Méthode d'initialisation personnalisée appelée

INFO somepackage.SomeBean - Publication post appelée

INFO somepackage.SomeBeanTest - test

0