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

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

19 голосов
/ 30 сентября 2019

Как мы знаем, 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 функциональность для MyBeanB, Spring использовал динамический прокси JDK.

1 Ответ

9 голосов
/ 02 октября 2019

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

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

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

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

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

Прокси @Scope 1015 * ведет себя по-разному. В документации говорится:

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

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

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

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

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

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

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

, что по сути то, что Spring делает для удовлетворения@Autowired target инъекции.


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

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

Вы объявляете определение прототипа компонента (через аннотации). @Scope имеет элемент proxyMode, который

Указывает, должен ли компонент быть настроен как прокси-сервер с областью действия, и если да, должен ли прокси-сервербыть основанным на интерфейсе или подклассе.

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

Таким образом, Spring не создает прокси-сервер области действия для получаемого компонента. Вы получаете этот компонент с помощью

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

Теперь у вас есть ссылка на новый MyBeanB объект, созданный Spring. Это подобно любому другому объекту Java, вызовы методов будут идти непосредственно к ссылочному экземпляру.

Если вы снова использовали getBean(MyBeanB.class), Spring вернет новый экземпляр, поскольку определение компонента предназначено для прототипа боб . Вы этого не делаете, поэтому все ваши вызовы методов идут к одному и тому же объекту.


Во втором примере

@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).


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

...