Was ist ein Proxy mit Gültigkeitsbereich im Frühjahr?

21

Wie wir wissen, verwendet Spring Proxys, um Funktionen hinzuzufügen ( @Transactionalund @Scheduledzum Beispiel). Es gibt zwei Möglichkeiten: Verwenden eines dynamischen JDK-Proxys (die Klasse muss nicht leere Schnittstellen implementieren) oder Generieren einer untergeordneten Klasse mithilfe des CGLIB-Codegenerators. Ich habe immer gedacht, dass ich mit proxyMode zwischen einem dynamischen JDK-Proxy und CGLIB wählen kann.

Aber ich konnte ein Beispiel erstellen, das zeigt, dass meine Annahme falsch ist:

Fall 1:

Singleton:

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

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

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

Prototyp:

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

Main:

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

Ausgabe:

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

Hier können wir zwei Dinge sehen:

  1. MyBeanBwurde nur einmal instanziiert .
  2. Um die @TransactionalFunktionalität für hinzuzufügen MyBeanB, verwendete Spring CGLIB.

Fall 2:

Lassen Sie mich die MyBeanBDefinition korrigieren :

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

In diesem Fall lautet die Ausgabe:

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

Hier können wir zwei Dinge sehen:

  1. MyBeanBwurde 3 mal instanziiert .
  2. Um die @TransactionalFunktionalität für hinzuzufügen MyBeanB, verwendete Spring CGLIB.

Können Sie erklären, was los ist? Wie funktioniert der Proxy-Modus wirklich?

PS

Ich habe die Dokumentation gelesen:

/**
 * 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
 */

aber es ist mir nicht klar.

Aktualisieren

Fall 3:

Ich habe einen weiteren Fall untersucht, in dem ich die Schnittstelle extrahiert habe aus 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 {

und in diesem Fall ist die Ausgabe:

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

Hier können wir zwei Dinge sehen:

  1. MyBeanBwurde 3 mal instanziiert .
  2. Um die @TransactionalFunktionalität für hinzuzufügen MyBeanB, verwendete Spring einen dynamischen JDK-Proxy.
gstackoverflow
quelle
Bitte zeigen Sie uns Ihre Transaktionskonfiguration.
Sotirios Delimanolis
@SotiriosDelimanolis Ich habe keine spezielle Konfiguration
gstackoverflow
Ich weiß nichts über Scoped Beans oder andere Arten von Enterprise Framework-Magie, die in Spring oder JEE enthalten sind. @SotiriosDelimanolis hat eine wunderbare Antwort zu diesem Thema geschrieben. Ich möchte nur die JDK- und CGLIB-Proxys kommentieren: In den Fällen 1 und 2 erweitert Ihre MyBeanBKlasse keine Schnittstellen, sodass es nicht verwunderlich ist, dass in Ihrem Konsolenprotokoll CGLIB-Proxy-Instanzen angezeigt werden. In Fall 3, in dem Sie eine Schnittstelle einführen und implementieren, erhalten Sie folglich einen JDK-Proxy. Sie beschreiben dies sogar in Ihrem Einführungstext.
Kriegaex
Für Nicht-Schnittstellentypen haben Sie also wirklich keine Wahl. Sie müssen CGLIB-Proxys sein, da JDK-Proxys nur für Schnittstellentypen funktionieren. Sie können CGLIB-Proxys jedoch auch für Schnittstellentypen erzwingen, wenn Sie Spring AOP verwenden. Dies wird über <aop:config proxy-target-class="true">bzw. konfiguriert @EnableAspectJAutoProxy(proxyTargetClass = true).
Kriegaex
@kriegaex Möchten Sie sagen, dass Aspectj CGlib für die Proxy-Generierung verwendet?
gstackoverflow

Antworten:

10

Der für das @TransactionalVerhalten generierte Proxy dient einem anderen Zweck als die Proxys mit Gültigkeitsbereich.

Der @TransactionalProxy umschließt die spezifische Bean, um das Sitzungsverwaltungsverhalten hinzuzufügen. Alle Methodenaufrufe führen die Transaktionsverwaltung vor und nach der Delegierung an die eigentliche Bean durch.

Wenn Sie es veranschaulichen, würde es so aussehen

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

Für unsere Zwecke können Sie das Verhalten im Wesentlichen ignorieren (entfernen @Transactionalund Sie sollten dasselbe Verhalten sehen, außer Sie haben keinen cglib-Proxy).

Der @ScopeProxy verhält sich anders. In der Dokumentation heißt es:

[...] Sie müssen ein Proxy-Objekt einfügen, das dieselbe öffentliche Schnittstelle wie das Objekt mit Gültigkeitsbereich bereitstellt , das jedoch auch das reale Zielobjekt aus dem relevanten Bereich (z. B. eine HTTP-Anforderung) abrufen und Methodenaufrufe an das reale Objekt delegieren kann .

Was Spring wirklich tut, ist die Erstellung einer Singleton-Bean-Definition für einen Factory-Typ, der den Proxy darstellt. Das entsprechende Proxy-Objekt fragt jedoch bei jedem Aufruf den Kontext für die tatsächliche Bean ab.

Wenn Sie es veranschaulichen, würde es so aussehen

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

Da MyBeanBes sich um eine Prototyp-Bean handelt, gibt der Kontext immer eine neue Instanz zurück.

Nehmen Sie für die Zwecke dieser Antwort an, dass Sie die MyBeanBdirekt mit abgerufen haben

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

Dies ist im Wesentlichen das, was Spring tut, um ein @AutowiredInjektionsziel zu erreichen.


In Ihrem ersten Beispiel

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

Sie deklarieren eine Prototyp-Bean- Definition (durch die Anmerkungen). @Scopehat ein proxyModeElement, das

Gibt an, ob eine Komponente als Proxy mit Gültigkeitsbereich konfiguriert werden soll und wenn ja, ob der Proxy auf Schnittstellen oder Unterklassen basieren soll.

DerScopedProxyMode.DEFAULT Standardwert ist " Dies" gibt normalerweise an, dass kein Proxy mit Gültigkeitsbereich erstellt werden soll, es sei denn, auf der Ebene der Komponenten-Scan-Anweisungen wurde ein anderer Standard konfiguriert.

Spring erstellt also keinen Proxy mit Gültigkeitsbereich für die resultierende Bean. Sie holen diese Bohne mit ab

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

Sie haben jetzt einen Verweis auf ein neues MyBeanBObjekt, das von Spring erstellt wurde. Wie bei jedem anderen Java-Objekt werden Methodenaufrufe direkt an die referenzierte Instanz gesendet.

Bei getBean(MyBeanB.class)erneuter Verwendung würde Spring eine neue Instanz zurückgeben, da die Bean-Definition für eine Prototyp-Bean gilt . Sie tun das nicht, also gehen alle Ihre Methodenaufrufe an dasselbe Objekt.


In Ihrem zweiten Beispiel

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

Sie deklarieren einen Proxy mit Gültigkeitsbereich, der über cglib implementiert wird. Wenn Sie eine Bohne dieses Typs von Spring with anfordern

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

Spring weiß, dass MyBeanBes sich um einen Proxy mit Gültigkeitsbereich handelt, und gibt daher ein Proxy-Objekt zurück, das die API von erfüllt MyBeanB(dh alle öffentlichen Methoden implementiert), das intern weiß, wie eine tatsächliche Bean vom Typ MyBeanBfür jeden Methodenaufruf abgerufen wird .

Versuche zu rennen

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

Dies gibt einen trueHinweis darauf zurück, dass Spring ein Singleton-Proxy-Objekt (kein Prototyp-Bean) zurückgibt.

Bei einem Methodenaufruf verwendet Spring innerhalb der Proxy-Implementierung eine spezielle getBeanVersion, die zwischen der Proxy-Definition und der tatsächlichen MyBeanBBean-Definition zu unterscheiden weiß . Dadurch wird eine neue MyBeanBInstanz zurückgegeben (da es sich um einen Prototyp handelt), und Spring delegiert den Methodenaufruf durch Reflektion (klassisch Method.invoke) an diese Instanz .


Ihr drittes Beispiel ist im Wesentlichen dasselbe wie Ihr zweites.

Sotirios Delimanolis
quelle
Im zweiten Fall habe ich also zwei Proxys: scoped_proxy, der transactional_proxy umschließt, der natürliche MyBeanB_bean umschließt ? scoped_proxy -> transactional_proxy -> MyBeanB_bean
gstackoverflow
Ist es möglich, CGLIB-Proxy für scoped_proxy und JDK_Dynamic_proxy für transactiona_proxy zu haben?
gstackoverflow
1
@gstackoverflow Wenn Sie dies tun context.getBean(MyBeanB.class), erhalten Sie nicht den Proxy, sondern die eigentliche Bean. @AutowiredRuft den Proxy ab (tatsächlich schlägt er fehl, wenn Sie MyBeanBanstelle des Schnittstellentyps injizieren ). Ich weiß nicht, warum Spring dich getBean(MyBeanB.class)mit INTERFACES machen lässt.
Sotirios Delimanolis
1
@gstackoverflow Vergiss es @Transactional. Mit @Autowired MyBeanBInterfaceProxys mit und ohne Gültigkeitsbereich injiziert Spring das Proxy-Objekt. Wenn Sie dies getBean(MyBeanB.class)jedoch nur tun , gibt Spring den Proxy nicht zurück, sondern die Ziel-Bean.
Sotirios Delimanolis
1
Es ist erwähnenswert, dass dies eine Implementierung des Delegationsmusters in Bezug auf Bohnen im Frühjahr ist
Stephan