Qu'est-ce qu'un proxy de portée au printemps?

21

Comme nous le savons, Spring utilise des proxys pour ajouter des fonctionnalités ( @Transactionalet @Scheduledpar exemple). Il existe deux options: utiliser un proxy dynamique JDK (la classe doit implémenter des interfaces non vides) ou générer une classe enfant à l'aide du générateur de code CGLIB. J'ai toujours pensé que proxyMode me permet de choisir entre un proxy dynamique JDK et CGLIB.

Mais j'ai pu créer un exemple qui montre que mon hypothèse est fausse:

Cas 1:

Singleton:

@Service
public class MyBeanA {
    @Autowired
    private MyBeanB myBeanB;

    public void foo() {
        System.out.println(myBeanB.getCounter());
    }

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

Prototype:

@Service
@Scope(value = "prototype")
public class MyBeanB {
    private static final AtomicLong COUNTER = new AtomicLong(0);

    private Long index;

    public MyBeanB() {
        index = COUNTER.getAndIncrement();
        System.out.println("constructor invocation:" + index);
    }

    @Transactional // just to force Spring to create a proxy
    public long getCounter() {
        return index;
    }
}

Principale:

MyBeanA beanA = context.getBean(MyBeanA.class);
beanA.foo();
beanA.foo();
MyBeanB myBeanB = beanA.getMyBeanB();
System.out.println("counter: " + myBeanB.getCounter() + ", class=" + myBeanB.getClass());

Production:

constructor invocation:0
0
0
counter: 0, class=class test.pack.MyBeanB$$EnhancerBySpringCGLIB$$2f3d648e

Ici, nous pouvons voir deux choses:

  1. MyBeanBn'a été instanciée qu'une seule fois .
  2. Pour ajouter la @Transactionalfonctionnalité de MyBeanB, Spring a utilisé CGLIB.

Cas 2:

Permettez-moi de corriger la MyBeanBdéfinition:

@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBeanB {

Dans ce cas, la sortie est:

constructor invocation:0
0
constructor invocation:1
1
constructor invocation:2
counter: 2, class=class test.pack.MyBeanB$$EnhancerBySpringCGLIB$$b06d71f2

Ici, nous pouvons voir deux choses:

  1. MyBeanBa été instancié 3 fois.
  2. Pour ajouter la @Transactionalfonctionnalité de MyBeanB, Spring a utilisé CGLIB.

Pourriez-vous expliquer ce qui se passe? Comment fonctionne réellement le mode proxy?

PS

J'ai lu la documentation:

/**
 * Specifies whether a component should be configured as a scoped proxy
 * and if so, whether the proxy should be interface-based or subclass-based.
 * <p>Defaults to {@link ScopedProxyMode#DEFAULT}, which typically indicates
 * that no scoped proxy should be created unless a different default
 * has been configured at the component-scan instruction level.
 * <p>Analogous to {@code <aop:scoped-proxy/>} support in Spring XML.
 * @see ScopedProxyMode
 */

mais ce n'est pas clair pour moi.

Mise à jour

Cas 3:

J'ai enquêté sur un autre cas, dans lequel j'ai extrait l'interface de MyBeanB:

public interface MyBeanBInterface {
    long getCounter();
}



@Service
public class MyBeanA {
    @Autowired
    private MyBeanBInterface myBeanB;


@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES)
public class MyBeanB implements MyBeanBInterface {

et dans ce cas la sortie est:

constructor invocation:0
0
constructor invocation:1
1
constructor invocation:2
counter: 2, class=class com.sun.proxy.$Proxy92

Ici, nous pouvons voir deux choses:

  1. MyBeanBa été instancié 3 fois.
  2. Pour ajouter la @Transactionalfonctionnalité de MyBeanB, Spring a utilisé un proxy dynamique JDK.
gstackoverflow
la source
Veuillez nous montrer votre configuration transactionnelle.
Sotirios Delimanolis
@SotiriosDelimanolis Je n'ai aucune configuration spéciale
gstackoverflow
Je ne connais pas les beans étendus ou tout autre type de magie de cadre d'entreprise contenu dans Spring ou JEE. @SotiriosDelimanolis a écrit une merveilleuse réponse à ce sujet, je veux commenter uniquement sur les proxys JDK vs CGLIB: dans les cas 1 et 2, votre MyBeanBclasse n'extend aucune interface, il n'est donc pas surprenant que le journal de votre console affiche des instances de proxy CGLIB. Dans le cas 3, vous introduisez et implémentez une interface, vous obtenez donc un proxy JDK. Vous décrivez même cela dans votre texte d'introduction.
kriegaex
Donc, pour les types sans interface, vous n'avez vraiment pas le choix, ils doivent être des proxys CGLIB car les proxys JDK ne fonctionnent que pour les types d'interfaces. Vous pouvez cependant appliquer des proxys CGLIB même pour les types d'interface lorsque vous utilisez Spring AOP. Ceci est configuré via <aop:config proxy-target-class="true">ou @EnableAspectJAutoProxy(proxyTargetClass = true), respectivement.
kriegaex
@kriegaex Voulez-vous dire qu'Aspectj utilise CGlib pour la génération de proxy?
gstackoverflow

Réponses:

10

Le proxy généré pour le @Transactionalcomportement sert un objectif différent de celui des proxys de portée.

Le @Transactionalproxy est celui qui encapsule le bean spécifique pour ajouter un comportement de gestion de session. Toutes les invocations de méthode effectueront la gestion des transactions avant et après la délégation au bean réel.

Si vous l'illustrez, cela ressemblerait à

main -> getCounter -> (cglib-proxy -> MyBeanB)

Pour nos besoins, vous pouvez essentiellement ignorer son comportement (supprimer @Transactionalet vous devriez voir le même comportement, sauf que vous n'aurez pas le proxy cglib).

Le @Scopeproxy se comporte différemment. La documentation indique:

[...] vous devez injecter un objet proxy qui expose la même interface publique que l'objet délimité mais qui peut également récupérer l'objet cible réel à partir de la portée appropriée (comme une demande HTTP) et déléguer des appels de méthode à l'objet réel .

Ce que fait réellement Spring, c'est la création d'une définition de bean singleton pour un type de fabrique représentant le proxy. Cependant, l'objet proxy correspondant interroge le contexte du bean réel pour chaque appel.

Si vous l'illustrez, cela ressemblerait à

main -> getCounter -> (cglib-scoped-proxy -> context/bean-factory -> new MyBeanB)

Puisqu'il MyBeanBs'agit d'un prototype de bean, le contexte retournera toujours une nouvelle instance.

Aux fins de cette réponse, supposons que vous ayez récupéré MyBeanBdirectement le

MyBeanB beanB = context.getBean(MyBeanB.class);

qui est essentiellement ce que Spring fait pour satisfaire une @Autowiredcible d'injection.


Dans votre premier exemple,

@Service
@Scope(value = "prototype")
public class MyBeanB { 

Vous déclarez une définition de bean prototype (via les annotations). @Scopea un proxyModeélément qui

Spécifie si un composant doit être configuré en tant que proxy de portée et, dans l’affirmative, si le proxy doit être basé sur une interface ou sur une sous-classe.

La valeur par défaut est ScopedProxyMode.DEFAULT, ce qui indique généralement qu'aucun proxy de portée ne doit être créé à moins qu'une valeur par défaut différente n'ait été configurée au niveau de l'instruction d'analyse des composants.

Spring ne crée donc pas de proxy de portée pour le bean résultant. Vous récupérez ce bean avec

MyBeanB beanB = context.getBean(MyBeanB.class);

Vous avez maintenant une référence à un nouvel MyBeanBobjet créé par Spring. C'est comme tout autre objet Java, les invocations de méthode iront directement à l'instance référencée.

Si vous l'utilisiez à getBean(MyBeanB.class)nouveau, Spring retournerait une nouvelle instance, car la définition du bean est pour un bean prototype . Vous ne faites pas cela, donc toutes vos invocations de méthode vont au même objet.


Dans votre deuxième exemple,

@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBeanB {

vous déclarez un proxy de portée implémenté via cglib. Lorsque vous demandez un bean de ce type à Spring avec

MyBeanB beanB = context.getBean(MyBeanB.class);

Spring sait qu'il MyBeanBs'agit d'un proxy de portée et retourne donc un objet proxy qui satisfait l'API de MyBeanB(c'est-à-dire implémente toutes ses méthodes publiques) qui sait en interne comment récupérer un bean de type réel MyBeanBpour chaque appel de méthode.

Essayez de courir

System.out.println("singleton?: " + (context.getBean(MyBeanB.class) == context.getBean(MyBeanB.class)));

Cela renverra une trueallusion au fait que Spring retourne un objet proxy singleton (pas un bean prototype).

Lors d'un appel de méthode, à l'intérieur de l'implémentation du proxy, Spring utilisera une getBeanversion spéciale qui sait faire la distinction entre la définition de proxy et la MyBeanBdéfinition de bean réelle . Cela retournera une nouvelle MyBeanBinstance (puisqu'il s'agit d'un prototype) et Spring lui déléguera l'invocation de la méthode par réflexion (classique Method.invoke).


Votre troisième exemple est essentiellement le même que votre second.

Sotirios Delimanolis
la source
Donc, dans le deuxième cas, j'ai 2 proxys: scoped_proxy qui enveloppe transactional_proxy qui enveloppe naturel MyBeanB_bean ? scoped_proxy -> transactional_proxy -> MyBeanB_bean
gstackoverflow
Est-il possible d'avoir un proxy CGLIB pour scoped_proxy et JDK_Dynamic_proxy pour transactiona_proxy?
gstackoverflow
1
@gstackoverflow Lorsque vous le faites context.getBean(MyBeanB.class), vous n'obtenez pas réellement le proxy, vous obtenez le bean réel. @Autowiredobtient le proxy (en fait, il échouera si vous injectez à la MyBeanBplace du type d'interface). Je ne sais pas pourquoi Spring vous laisse faire getBean(MyBeanB.class)avec INTERFACES.
Sotirios Delimanolis
1
@gstackoverflow Oubliez @Transactional. Avec des @Autowired MyBeanBInterfaceproxys étendus et limités, Spring injectera l'objet proxy. Si vous le faites getBean(MyBeanB.class)cependant, Spring ne renverra pas le proxy, il renverra le bean cible.
Sotirios Delimanolis
1
Il convient de noter qu'il s'agit d'une implémentation de modèle de délégation en ce qui concerne les haricots au printemps
Stephan