Che cos'è un proxy con ambito in primavera?


21

Come sappiamo, Spring usa i proxy per aggiungere funzionalità ( @Transactionale @Scheduledper esempio). Esistono due opzioni: utilizzare un proxy dinamico JDK (la classe deve implementare interfacce non vuote) o generare una classe figlio utilizzando il generatore di codice CGLIB. Ho sempre pensato che proxyMode mi permettesse di scegliere tra un proxy dinamico JDK e CGLIB.

Ma sono stato in grado di creare un esempio che dimostra che la mia ipotesi è sbagliata:

Caso 1:

Singleton:

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

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

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

Prototipo:

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

Produzione:

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

Qui possiamo vedere due cose:

  1. MyBeanBè stato istanziato una sola volta .
  2. Per aggiungere la @Transactionalfunzionalità per MyBeanB, Spring ha usato CGLIB.

Caso 2:

Vorrei correggere la MyBeanBdefinizione:

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

In questo caso l'output è:

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

Qui possiamo vedere due cose:

  1. MyBeanBè stato istanziato 3 volte.
  2. Per aggiungere la @Transactionalfunzionalità per MyBeanB, Spring ha usato CGLIB.

Potresti spiegare cosa sta succedendo? Come funziona davvero la modalità proxy?

PS

Ho letto la documentazione:

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

ma non è chiaro per me.

Aggiornare

Caso 3:

Ho studiato un altro caso, in cui ho estratto l'interfaccia da 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 {

e in questo caso l'output è:

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

Qui possiamo vedere due cose:

  1. MyBeanBè stato istanziato 3 volte.
  2. Per aggiungere la @Transactionalfunzionalità per MyBeanB, Spring ha usato un proxy dinamico JDK.

Mostraci la tua configurazione transazionale.
Sotirios Delimanolis,

@SotiriosDelimanolis Non ho alcuna configurazione speciale
gstackoverflow

Non conosco i fagioli scoped o qualsiasi altro tipo di magia del framework aziendale contenuta in Spring o JEE. @SotiriosDelimanolis ha scritto una risposta meravigliosa su quella roba, voglio commentare solo i proxy JDK vs. CGLIB: nei casi 1 e 2 la tua MyBeanBclasse non estende alcuna interfaccia, quindi non sorprende che il tuo log della console mostri le istanze proxy CGLIB. Nel caso 3 si introduce e implementa un'interfaccia, di conseguenza si ottiene un proxy JDK. Lo descrivi anche nel tuo testo introduttivo.
Kriegaex,

Quindi per i tipi non di interfaccia non hai davvero scelta, devono essere proxy CGLIB perché i proxy JDK funzionano solo per i tipi di interfaccia. Tuttavia, è possibile applicare i proxy CGLIB anche per i tipi di interfaccia quando si utilizza Spring AOP. Questo è configurato tramite <aop:config proxy-target-class="true">o @EnableAspectJAutoProxy(proxyTargetClass = true), rispettivamente.
kriegaex,

@kriegaex Vuoi dire che Aspectj utilizza CGlib per la generazione di proxy?
gstackoverflow,

Risposte:


10

Il proxy generato per il @Transactionalcomportamento ha uno scopo diverso rispetto ai proxy con ambito.

Il @Transactionalproxy è uno che avvolge il bean specifico per aggiungere il comportamento di gestione della sessione. Tutte le chiamate ai metodi eseguiranno la gestione delle transazioni prima e dopo la delega al bean effettivo.

Se lo illustri, sembrerebbe

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

Per i nostri scopi, puoi essenzialmente ignorare il suo comportamento (rimuovi @Transactionale dovresti vedere lo stesso comportamento, tranne che non avrai il proxy cglib).

Il @Scopeproxy si comporta diversamente. La documentazione afferma:

[...] è necessario iniettare un oggetto proxy che espone la stessa interfaccia pubblica dell'oggetto con ambito ma che può anche recuperare l'oggetto di destinazione reale dall'ambito pertinente (come una richiesta HTTP) e delegare le chiamate del metodo sull'oggetto reale .

Ciò che Spring sta realmente facendo è creare una definizione del bean singleton per un tipo di factory che rappresenta il proxy. L'oggetto proxy corrispondente, tuttavia, esegue una query sul contesto per il bean effettivo per ogni chiamata.

Se lo illustri, sembrerebbe

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

Poiché MyBeanBè un bean prototipo, il contesto restituirà sempre una nuova istanza.

Ai fini di questa risposta, supponi di aver recuperato MyBeanBdirettamente con

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

che è essenzialmente ciò che fa Spring per soddisfare un @Autowiredobiettivo di iniezione.


Nel tuo primo esempio,

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

Dichiarare una definizione di bean prototipo (tramite le annotazioni). @Scopeha un proxyModeelemento che

Specifica se un componente deve essere configurato come proxy con ambito e, in tal caso, se il proxy deve essere basato su interfaccia o sottoclasse.

Il valore predefinito è ScopedProxyMode.DEFAULT, che indica in genere che non è necessario creare un proxy con ambito a meno che non sia stato configurato un valore predefinito diverso a livello di istruzione di scansione dei componenti.

Quindi Spring non sta creando un proxy con ambito per il bean risultante. Si recupera quel fagiolo con

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

Ora hai un riferimento a un nuovo MyBeanBoggetto creato da Spring. Questo è come qualsiasi altro oggetto Java, le invocazioni del metodo andranno direttamente all'istanza di riferimento.

Se lo utilizzassi di getBean(MyBeanB.class)nuovo, Spring restituirà una nuova istanza, poiché la definizione del bean è per un bean prototipo . Non lo stai facendo, quindi tutte le tue invocazioni di metodo vanno allo stesso oggetto.


Nel tuo secondo esempio,

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

si dichiara un proxy con ambito implementato tramite cglib. Quando si richiede un bean di questo tipo da Spring con

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

Spring sa che MyBeanBè un proxy con ambito e quindi restituisce un oggetto proxy che soddisfa l'API di MyBeanB(cioè implementa tutti i suoi metodi pubblici) che sa internamente come recuperare un bean di tipo effettivo MyBeanBper ogni chiamata di metodo.

Prova a correre

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

Ciò restituirà un truesuggerimento al fatto che Spring sta restituendo un oggetto proxy singleton (non un bean prototipo).

In una chiamata di metodo, all'interno dell'implementazione del proxy, Spring utilizzerà una getBeanversione speciale che sa distinguere tra la definizione del proxy e la MyBeanBdefinizione effettiva del bean. Restituirà una nuova MyBeanBistanza (dal momento che è un prototipo) e Spring delegherà l'invocazione del metodo ad essa tramite la riflessione (classica Method.invoke).


Il tuo terzo esempio è essenzialmente lo stesso del secondo.


Quindi, per un secondo caso, ho 2 proxy: scoped_proxy che avvolge il file transazionale che avvolge MyBeanB_bean naturale ? scoped_proxy -> transactional_proxy -> MyBeanB_bean
gstackoverflow

È possibile avere un proxy CGLIB per scoped_proxy e JDK_Dynamic_proxy per transactiona_proxy?
gstackoverflow,

1
@gstackoverflow Quando lo fai context.getBean(MyBeanB.class), non stai effettivamente ottenendo il proxy, stai ottenendo il bean reale. @Autowiredottiene il proxy (infatti fallirà se si inietta MyBeanBinvece del tipo di interfaccia). Non so perché Spring ti permetta di fare getBean(MyBeanB.class)INTERFACCE.
Sotirios Delimanolis,

1
@gstackoverflow Dimentica @Transactional. Con @Autowired MyBeanBInterfacee proxy con ambito, Spring inietterà l'oggetto proxy. Se lo fai getBean(MyBeanB.class), tuttavia, Spring non restituirà il proxy, restituirà il bean di destinazione.
Sotirios Delimanolis,

1
Vale la pena notare che si tratta di un'implementazione del modello di delega per quanto riguarda Beans in Spring
Stephan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.