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

Как известно, 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;
    }
}

Main:

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.

Ответы

Ответ 1

Прокси-сервер, сгенерированный для поведения @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 { 

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

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

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

Таким образом, 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).


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