Что такое прокси в Spring?


21

Как мы знаем, 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.

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

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

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

Так что для неинтерфейсных типов у вас действительно нет выбора, они должны быть прокси CGLIB, потому что прокси JDK работают только для типов интерфейса. Однако при использовании Spring AOP вы можете использовать прокси-серверы CGLIB даже для типов интерфейсов. Это настраивается через <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это прототип bean, контекст всегда будет возвращать новый экземпляр.

Для целей этого ответа предположим, что вы получили 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-компонента предназначено для 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(то есть реализует все свои открытые методы), который внутренне знает, как извлечь фактический компонент типа MyBeanBдля каждого вызова метода.

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

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

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

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


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


Так что для второго случая у меня есть 2 прокси: scoped_proxy, который оборачивает транзакцию транзистором_прокси, который оборачивает естественный MyBeanB_bean ? scoped_proxy -> транзакция_прокси -> MyBeanB_bean
gstackoverflow

Можно ли иметь прокси CGLIB для scoped_proxy и JDK_Dynamic_proxy для транзакции a_proxy?
gstackoverflow

1
@gstackoverflow Когда вы делаете context.getBean(MyBeanB.class), вы на самом деле не получаете прокси, вы получаете фактический бин. @Autowiredполучает прокси (на самом деле он потерпит неудачу, если вы введете MyBeanBвместо типа интерфейса). Я не знаю, почему Spring позволяет вам делать getBean(MyBeanB.class)с интерфейсами.
Сотириос Делиманолис

1
@gstackoverflow Забудьте о @Transactional. С помощью @Autowired MyBeanBInterfaceпрокси-серверов и с областями видимости Spring будет вводить объект прокси. Если вы просто сделаете это getBean(MyBeanB.class), Spring не вернет прокси, он вернет целевой боб.
Сотириос Делиманолис

1
Стоит отметить, что это реализация шаблона делегирования в отношении Beans в Spring
Stephan
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.