Co to jest zasięg proxy na wiosnę?

21

Jak wiemy, Spring używa serwerów proxy w celu dodania funkcjonalności ( @Transactionali @Scheduledna przykład). Istnieją dwie opcje - użycie dynamicznego proxy JDK (klasa musi implementować niepuste interfejsy) lub wygenerowanie klasy potomnej za pomocą generatora kodu CGLIB. Zawsze myślałem, że proxyMode pozwala mi wybierać między dynamicznym proxy JDK a CGLIB.

Ale udało mi się stworzyć przykład, który pokazuje, że moje założenie jest błędne:

Przypadek 1:

Singel:

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

Główny:

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

Wynik:

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

Tutaj widzimy dwie rzeczy:

  1. MyBeanBzostał utworzony tylko raz .
  2. Aby dodać @Transactionalfunkcjonalność MyBeanB, Spring użył CGLIB.

Przypadek 2:

Pozwól mi poprawić MyBeanBdefinicję:

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

W tym przypadku wynikiem jest:

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

Tutaj widzimy dwie rzeczy:

  1. MyBeanBzostał utworzony 3 razy.
  2. Aby dodać @Transactionalfunkcjonalność MyBeanB, Spring użył CGLIB.

Czy możesz wyjaśnić, co się dzieje? Jak naprawdę działa tryb proxy?

PS

Przeczytałem dokumentację:

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

ale nie jest to dla mnie jasne.

Aktualizacja

Przypadek 3:

Zbadałem jeszcze jeden przypadek, w którym wyodrębniłem interfejs z 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 {

iw tym przypadku wynikiem jest:

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

Tutaj widzimy dwie rzeczy:

  1. MyBeanBzostał utworzony 3 razy.
  2. Aby dodać tę @Transactionalfunkcjonalność MyBeanB, Spring użył dynamicznego proxy JDK.
gstackoverflow
źródło
Pokaż nam swoją konfigurację transakcyjną.
Sotirios Delimanolis
@SotiriosDelimanolis Nie mam żadnej specjalnej konfiguracji
gstackoverflow
Nie wiem o ziarnach fasoli ani żadnej innej magii frameworku korporacyjnego zawartej w Spring lub JEE. @SotiriosDelimanolis napisał wspaniałą odpowiedź na ten temat, chcę skomentować tylko proxy JDK vs. CGLIB: w przypadkach 1 i 2 twoja MyBeanBklasa nie rozszerza żadnych interfejsów, więc nic dziwnego, że twój dziennik konsoli pokazuje instancje proxy CGLIB. W przypadku 3 wprowadzenia i implementacji interfejsu otrzymujesz serwer proxy JDK. Opisujesz to nawet w tekście wprowadzającym.
kriegaex
Zatem dla typów nieinterfejsowych naprawdę nie masz wyboru, muszą to być serwery proxy CGLIB, ponieważ serwery proxy JDK działają tylko dla typów interfejsów. Możesz jednak wymuszać proxy CGLIB, nawet dla typów interfejsów, używając Spring AOP. Jest to konfigurowane odpowiednio za pomocą <aop:config proxy-target-class="true">lub @EnableAspectJAutoProxy(proxyTargetClass = true).
kriegaex
@kriegaex Czy chcesz powiedzieć, że Aspectj używa CGlib do generowania proxy?
gstackoverflow

Odpowiedzi:

10

Proxy wygenerowane dla @Transactionalzachowania służy innym celom niż zakresowe proxy.

Serwer @Transactionalproxy to taki, który otacza konkretny komponent bean, aby dodać zachowanie zarządzania sesją. Wszystkie wywołania metod wykonają zarządzanie transakcjami przed i po delegowaniu do faktycznego komponentu bean.

Jeśli to zilustrujesz, to będzie wyglądać

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

Dla naszych celów możesz zasadniczo zignorować jego zachowanie (usuń @Transactionali powinieneś zobaczyć to samo zachowanie, z tym wyjątkiem, że nie będziesz mieć proxy cglib).

Serwer @Scopeproxy zachowuje się inaczej. Dokumentacja stwierdza:

[...] musisz wstrzyknąć obiekt proxy, który udostępnia ten sam interfejs publiczny co obiekt o zasięgu, ale który może również pobrać prawdziwy obiekt docelowy z odpowiedniego zakresu (takiego jak żądanie HTTP) i delegować wywołania metod na obiekt rzeczywisty .

Spring naprawdę robi tworzenie definicji singleton bean dla typu fabryki reprezentującej serwer proxy. Odpowiedni obiekt proxy sprawdza jednak kontekst dla faktycznego komponentu bean dla każdego wywołania.

Jeśli to zilustrujesz, to będzie wyglądać

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

Ponieważ MyBeanBjest to prototypowy komponent bean, kontekst zawsze zwróci nową instancję.

Dla celów tej odpowiedzi załóż, że odzyskałeś MyBeanBbezpośrednio za pomocą

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

co jest zasadniczo tym, co robi Spring, aby osiągnąć @Autowiredcel wstrzyknięcia.


W twoim pierwszym przykładzie

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

Deklarujesz prototypową definicję komponentu bean (poprzez adnotacje). @Scopema proxyModeelement, który

Określa, czy komponent powinien być skonfigurowany jako serwer proxy o zasięgu, a jeśli tak, czy serwer proxy powinien być oparty na interfejsie, czy na podklasie.

Domyślnie ScopedProxyMode.DEFAULT, co zwykle oznacza, że ​​nie należy tworzyć serwera proxy o zasięgu, chyba że na poziomie instrukcji skanowania składników skonfigurowano inne ustawienie domyślne.

Zatem Spring nie tworzy zasięgowego proxy dla wynikowej fasoli. Możesz pobrać tę fasolę za pomocą

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

Masz teraz odniesienie do nowego MyBeanBobiektu utworzonego przez Spring. Jest to jak każdy inny obiekt Java, wywołania metod będą kierowane bezpośrednio do instancji, do której się odwołuje.

Jeśli użyjesz go getBean(MyBeanB.class)ponownie, Spring zwróci nową instancję, ponieważ definicja komponentu bean dotyczy komponentu bean . Nie robisz tego, więc wszystkie wywołania metod są kierowane do tego samego obiektu.


W twoim drugim przykładzie

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

deklarujesz zakresowy serwer proxy zaimplementowany przez cglib. Podczas zamawiania fasoli tego typu od Spring with

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

Spring wie, że MyBeanBjest to proxy o zasięgu i dlatego zwraca obiekt proxy, który spełnia API MyBeanB(tj. Implementuje wszystkie swoje metody publiczne), który wewnętrznie wie, jak pobrać rzeczywistą bean typu MyBeanBdla każdego wywołania metody.

Spróbuj uruchomić

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

Będzie to powrót truesugerując się tym, że wiosna jest zwrócenie obiektu Singleton proxy (nie prototyp Bean).

Podczas wywoływania metody w implementacji proxy Spring użyje specjalnej getBeanwersji, która wie, jak odróżnić definicję proxy od faktycznej MyBeanBdefinicji komponentu bean. Zwróci to nową MyBeanBinstancję (ponieważ jest to prototyp), a Spring przekaże do niej wywołanie metody poprzez odbicie (klasyczne Method.invoke).


Twój trzeci przykład jest zasadniczo taki sam jak twój drugi.

Sotirios Delimanolis
źródło
Więc w drugim przypadku mam 2 serwery proxy: scoped_proxy, który otacza transakcyjny_proxy, który otacza naturalny MyBeanB_bean ? scoped_proxy -> transactional_proxy -> MyBeanB_bean
gstackoverflow
Czy możliwe jest posiadanie proxy CGLIB dla scoped_proxy i JDK_Dynamic_proxy dla transactiona_proxy?
gstackoverflow
1
@gstackoverflow Kiedy to zrobisz context.getBean(MyBeanB.class), tak naprawdę nie dostajesz proxy, dostajesz rzeczywistą fasolę. @Autowiredpobiera proxy (w rzeczywistości nie powiedzie się, jeśli wstrzykniesz MyBeanBzamiast typu interfejsu). Nie wiem, dlaczego Spring pozwala ci na getBean(MyBeanB.class)INTERFACES.
Sotirios Delimanolis
1
@gstackoverflow Zapomnij o @Transactional. Za pomocą @Autowired MyBeanBInterfaceserwerów proxy o zasięgu i zasięgach Spring wstrzykuje obiekt proxy. Jeśli jednak to zrobisz getBean(MyBeanB.class), Spring nie zwróci proxy, zwróci docelową fasolę.
Sotirios Delimanolis
1
Warto zauważyć, że jest to implementacja wzoru delegacji w odniesieniu do fasoli w okresie wiosennym
Stephan