Що таке проксі-сервер весною?


21

Як ми знаємо, Spring використовує проксі для додавання функціональності ( @Transactionalі, @Scheduledнаприклад). Є два варіанти - використання динамічного проксі-сервера JDK (клас повинен реалізувати не порожні інтерфейси) або генерування дочірнього класу за допомогою генератора коду CGLIB. Я завжди думав, що proxyMode дозволяє мені вибирати між динамічним проксі JDK і CGLIB.

Але мені вдалося створити приклад, який показує, що моє припущення неправильне:

Випадок 1:

Singleton:

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

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

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

Прототип:

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

Основні:

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

Вихід:

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

Тут ми можемо побачити дві речі:

  1. MyBeanBбув створений лише один раз .
  2. Для додання @Transactionalфункціональності для MyBeanBSpring використовується CGLIB.

Випадок 2:

Дозвольте виправити MyBeanBвизначення:

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

У цьому випадку вихід:

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

Тут ми можемо побачити дві речі:

  1. MyBeanBінстанціювали 3 рази.
  2. Для додання @Transactionalфункціональності для MyBeanBSpring використовується CGLIB.

Чи можете ви пояснити, що відбувається? Як реально працює режим проксі?

PS

Я прочитав документацію:

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

але мені це незрозуміло.

Оновлення

Випадок 3:

Я дослідив ще один випадок, коли я витягнув інтерфейс із 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 {

і в цьому випадку вихід:

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

Тут ми можемо побачити дві речі:

  1. MyBeanBінстанціювали 3 рази.
  2. Для додання @Transactionalфункціональності MyBeanBSpring використовував динамічний проксі JDK.

Будь ласка, покажіть нам вашу конфігурацію транзакцій.
Сотіріос

@SotiriosDelimanolis Я не маю спеціальної конфігурації
gstackoverflow

Я не знаю про квашені боби або будь-яку іншу магію корпоративних рамок, що містяться у Spring або JEE. @SotiriosDelimanolis написав чудову відповідь про цей матеріал, я хочу прокоментувати лише JDK проти CGLIB-проксі: У 1 та 2 випадках ваш MyBeanBклас не розширює жодних інтерфейсів, тому не дивно, що ваш консольний журнал показує екземпляри проксі CGLIB. У випадку 3 ви вводите та реалізуєте інтерфейс, отже, ви отримуєте проксі-сервер JDK. Ви навіть описуєте це у своєму вступному тексті.
kriegaex

Тож для неінтерфейсних типів у вас дійсно немає вибору, вони повинні бути проксі-серверами CGLIB, оскільки проксі-сервери JDK працюють лише для типів інтерфейсу. Однак ви можете застосувати проксі-сервери CGLIB навіть для типів інтерфейсів, коли використовуєте Spring AOP. Це налаштовано через <aop:config proxy-target-class="true">або @EnableAspectJAutoProxy(proxyTargetClass = true), відповідно.
kriegaex

@kriegaex Ви хочете сказати, що Aspectj використовує CGlib для генерації проксі?
gstackoverflow

Відповіді:


10

Проксі-сервер, створений для @Transactionalповедінки, виконує іншу мету, ніж проксі-сервери.

@TransactionalПроксі один , який обертає конкретний компонент , щоб додати поведінку сеансу управління. Усі виклики методу будуть виконувати управління транзакціями до і після делегування фактичного компонента.

Якщо ви проілюструєте це, це виглядатиме так

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

Для наших цілей ви можете по суті ігнорувати його поведінку (видалити, @Transactionalі ви повинні побачити таку ж поведінку, за винятком того, що у вас не буде проксі-сервера cglib).

@ScopeПроксі поводиться по- різному. У документації зазначено:

[...] вам потрібно ввести проксі-об'єкт, який відкриває той же публічний інтерфейс, що і об'єкт масштабування, але який також може отримати реальний цільовий об'єкт з відповідної області (наприклад, запит HTTP) та делегувати виклики методу на реальний об'єкт .

Що насправді робить Spring - це створення однотонного визначення квасолі для типу фабрики, що представляє проксі. Однак, відповідний об'єкт проксі-сервера запитує контекст для фактичного біна для кожного виклику.

Якщо ви проілюструєте це, це виглядатиме так

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

Оскільки MyBeanBє прототипом, контекст завжди буде повертати новий екземпляр.

Для цілей цієї відповіді припустімо, що ви отримали MyBeanBбезпосередньо за допомогою

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

що по суті є тим, що робить Spring для задоволення @Autowiredцілі ін'єкції.


У вашому першому прикладі

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

Ви оголошуєте визначення прототипу бобів (через анотації). @Scopeмає proxyModeелемент, який

Вказує, чи слід налаштовувати компонент як проксі-сервер, і якщо так, чи повинен бути проксі-сервер на основі інтерфейсу чи підкласу.

За замовчуванням ScopedProxyMode.DEFAULT, що зазвичай вказує на те, що жоден проксі-сервер не повинен створюватися, якщо інший за замовчуванням не налаштований на рівні інструкцій щодо сканування компонентів.

Таким чином, Spring не створює проксі-сервер для отриманих бобів. Ви отримаєте цей боб за допомогою

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

Тепер у вас є посилання на новий MyBeanBоб’єкт, створений Spring. Це як і будь-який інший об'єкт Java, виклики методів перейдуть безпосередньо до посиланого екземпляра.

Якщо ви getBean(MyBeanB.class)знову використовуєте , Spring поверне новий екземпляр, оскільки визначення bean є для прототипу . Ви цього не робите, тому всі виклики методу йдуть на один і той же об'єкт.


У вашому другому прикладі

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

Ви оголошуєте проксі-сервер, який реалізується через cglib. При запиті квасолі такого типу від Spring with

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

Spring знає, що MyBeanBце проксі-сервер, і тому повертає проксі-об'єкт, що задовольняє API MyBeanB(тобто реалізує всі його загальнодоступні методи), що внутрішньо знає, як отримати фактичний тип MyBeanBфайлу для кожного виклику методу.

Спробуйте запустити

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

Це поверне trueнатяк на те, що Spring повертає однотонний проксі-об'єкт (а не прототип).

Для виклику методу, всередині реалізації проксі, Spring використовує спеціальну getBeanверсію, яка знає, як розрізнити визначення проксі та фактичне MyBeanBвизначення bean. Це поверне новий MyBeanBекземпляр (оскільки це прототип), і Spring делегує виклик методу до нього через відображення (класичний Method.invoke).


Ваш третій приклад по суті такий же, як і ваш другий.


Так що для іншого випадку у мене є два проксі: scoped_proxy, який обгортає transactional_proxy, який обертає природний MyBeanB_bean ? scoped_proxy -> transactional_proxy -> MyBeanB_bean
gstackoverflow

Чи можливо мати проксі CGLIB для scoped_proxy та JDK_Dynamic_proxy для transactiona_proxy?
gstackoverflow

1
@gstackoverflow Коли ви context.getBean(MyBeanB.class)дійсно не отримуєте проксі, ви отримуєте фактичний боб. @Autowiredотримує проксі-сервер (насправді він вийде з ладу, якщо ви MyBeanBвведете замість типу інтерфейсу). Я не знаю, чому весна дозволяє вам робити getBean(MyBeanB.class)ІНТЕРФЕЙС.
Сотіріос

1
@gstackoverflow Забудь про @Transactional. За допомогою @Autowired MyBeanBInterfaceпроксі-серверів Spring та інженерія додадуть об'єкт проксі. Якщо ви просто зробите getBean(MyBeanB.class)це, Spring не поверне проксі, він поверне цільовий боб.
Сотіріос

1
Варто зазначити, що це реалізація схеми делегування щодо бобів протягом весни
Стефан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.