Что такое прокси-сервер с ограниченной областью действия в Spring?

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

Но мне удалось создать пример, который показывает, что мое предположение неверно:

Дело 1:

Одиночка:

@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 для MyBeanB, Spring использовал 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 для MyBeanB, Spring использовал CGLIB.

Не могли бы вы объяснить, что происходит? Как на самом деле работает прокси-режим?

P.S.

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

/**
 * 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 для MyBeanB, Spring использовал динамический прокси JDK.

person gstackoverflow    schedule 30.09.2019    source источник
comment
Пожалуйста, покажите нам вашу транзакционную конфигурацию.   -  person Sotirios Delimanolis    schedule 02.10.2019
comment
@SotiriosDelimanolis У меня нет особой конфигурации   -  person gstackoverflow    schedule 02.10.2019
comment
Я не знаю о bean-компонентах с ограниченной областью действия или какой-либо другой магии корпоративной среды, содержащейся в Spring или JEE. @SotiriosDelimanolis написал замечательный ответ об этом, я хочу прокомментировать только JDK и прокси-серверы CGLIB: в случаях 1 и 2 ваш класс MyBeanB не расширяет никаких интерфейсов, поэтому неудивительно, что ваш журнал консоли показывает экземпляры прокси-сервера CGLIB. В случае 3 вы вводите и реализуете интерфейс, следовательно, вы получаете прокси-сервер JDK. Вы даже описываете это во вступительном тексте.   -  person kriegaex    schedule 03.10.2019
comment
Таким образом, для неинтерфейсных типов у вас действительно нет выбора, они должны быть прокси-серверами CGLIB, потому что прокси-серверы JDK работают только для интерфейсных типов. Однако вы можете применять прокси-серверы CGLIB даже для типов интерфейса при использовании Spring AOP. Это настраивается через <aop:config proxy-target-class="true"> или @EnableAspectJAutoProxy(proxyTargetClass = true) соответственно.   -  person kriegaex    schedule 03.10.2019
comment
@kriegaex Вы хотите сказать, что Aspectj использует CGlib для генерации прокси?   -  person gstackoverflow    schedule 03.10.2019
comment
Я говорил о Spring AOP. AspectJ вообще не использует прокси.   -  person kriegaex    schedule 03.10.2019
comment
@kriegaex, не могли бы вы уточнить, что вы имеете в виду? АОП помогает нам вставить часть кода до/после/вместо.... какой-то метод/... Spring использует его для транзакционных, запланированных, асинхронных... аннотаций. Но bean-компонент со вставленным AOP является прокси   -  person gstackoverflow    schedule 03.10.2019
comment
Прочтите руководство по Spring AOP. , особенно глава понимание АОП-прокси. Spring AOP поддерживает подмножество обозначения pointcut. Если вам нужно больше мощности, используйте AspectJ через LTW.   -  person kriegaex    schedule 04.10.2019
comment
Добавление ссылки на документацию: ScopedProxy   -  person Stephan    schedule 05.10.2019


Ответы (1)


Прокси, сгенерированный для поведения @Transactional, служит другой цели, чем прокси с заданной областью действия.

Прокси-сервер @Transactional — это тот, который обертывает конкретный компонент, чтобы добавить поведение управления сеансом. Все вызовы методов будут выполнять управление транзакциями до и после делегирования фактическому компоненту.

Если вы проиллюстрируете это, это будет выглядеть так

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

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

@Scope proxy ведет себя иначе. В документации указано:

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

На самом деле Spring создает определение одноэлементного компонента для типа фабрики, представляющей прокси. Однако соответствующий прокси-объект запрашивает контекст фактического компонента для каждого вызова.

Если вы проиллюстрируете это, это будет выглядеть так

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

Поскольку MyBeanB является прототипом bean-компонента, контекст всегда будет возвращать новый экземпляр.

Для целей этого ответа предположим, что вы получили MyBeanB напрямую с помощью

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

что, по сути, и делает Spring, чтобы удовлетворить цель внедрения @Autowired.


В вашем первом примере

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

Вы объявляете прототип bean (через аннотации). @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. При запросе bean-компонента этого типа из Spring с помощью

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

Spring знает, что MyBeanB является прокси-сервером с ограниченной областью действия, и поэтому возвращает прокси-объект, который удовлетворяет API MyBeanB (т. е. реализует все его общедоступные методы), который внутренне знает, как получить фактический bean-компонент типа MyBeanB для каждого вызова метода.

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

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

Это вернет true намек на тот факт, что Spring возвращает одноэлементный прокси-объект (а не прототип bean-компонента).

При вызове метода внутри реализации прокси Spring будет использовать специальную версию getBean, которая знает, как отличить определение прокси от фактического определения bean-компонента MyBeanB. Это вернет новый экземпляр MyBeanB (поскольку это прототип), и Spring делегирует ему вызов метода через отражение (классический Method.invoke).


Ваш третий пример по существу такой же, как ваш второй.

person Sotirios Delimanolis    schedule 02.10.2019