Если Spring может успешно перехватывать вызовы функций внутри класса в классе @Configuration, почему он не поддерживает его в обычном компоненте?
Недавно я заметил, что Spring успешно перехватывает вызовы функций внутри класса в классе @Configuration, но не в обычном компоненте.
Звонок как это
@Repository
public class CustomerDAO {
@Transactional(value=TxType.REQUIRED)
public void saveCustomer() {
// some DB stuff here...
saveCustomer2();
}
@Transactional(value=TxType.REQUIRES_NEW)
public void saveCustomer2() {
// more DB stuff here
}
}
не удается запустить новую транзакцию, потому что, хотя код saveCustomer() выполняется в прокси-сервере CustomerDAO, код saveCustomer2() выполняется в развернутом классе CustomerDAO, как я могу видеть, посмотрев "this" в отладчике, и так Spring не имеет возможности перехватить вызов saveCustomer2.
Однако в следующем примере, когда транзакцияManager() вызывает метод createDataSource(), он корректно перехватывается и вызывает метод createDataSource() прокси-сервера, а не развернутого класса, о чем свидетельствует поиск этого элемента в отладчике.
@Configuration
public class PersistenceJPAConfig {
@Bean
public DriverManagerDataSource createDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
//dataSource.set ... DB stuff here
return dataSource;
}
@Bean
public PlatformTransactionManager transactionManager( ){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(createDataSource());
return transactionManager;
}
}
Поэтому мой вопрос заключается в том, почему Spring может правильно перехватывать вызовы функций внутри класса во втором примере, а не в первом. Используются ли разные типы динамических прокси?
Редактировать: Из ответов здесь и других источников теперь я понимаю следующее: @Transactional реализован с использованием Spring AOP, где шаблон прокси выполняется путем переноса/компоновки пользовательского класса. Прокси-сервер AOP достаточно универсален, так что многие аспекты могут быть объединены в цепочку и может быть прокси-сервером CGLib или динамическим прокси-сервером Java.
В классе @Configuration Spring также использует CGLib для создания расширенного класса, который наследуется от пользовательского класса @Configuration, и переопределяет пользовательские функции @Bean теми, которые выполняют дополнительную работу перед вызовом пользовательской/суперфункции, такой как проверка, это первый вызов функции или нет. Это класс прокси? Это зависит от определения. Вы можете сказать, что это прокси, который использует наследование от реального объекта, а не оборачивает его с помощью композиции.
Подводя итог, из ответов, приведенных здесь, я понимаю, что это два совершенно разных механизма. Почему был сделан этот выбор дизайна - другой, открытый вопрос.
Ответы
Ответ 1
Потому что прокси-серверы AOP и класс @Configuration
служат разным целям и реализуются существенно разными способами (даже при том, что оба используют прокси-серверы). По сути, AOP использует композицию, а @Configuration использует наследование.
AOP прокси
В основном они работают так, что создают прокси-серверы, которые выполняют соответствующую логику рекомендаций до/после делегирования вызова исходному (проксируемому) объекту. Контейнер регистрирует этот прокси вместо самого прокси-объекта, поэтому все зависимости установлены для этого прокси, и все вызовы от одного компонента к другому проходят через этот прокси. Однако сам проксируемый объект не имеет указателя на прокси (он не знает, что он проксирован, только прокси имеет указатель на целевой объект). Поэтому любые вызовы в этом объекте других методов не проходят через прокси.
(Я добавляю это здесь только для контраста с @Configuration, так как вы, кажется, правильно понимаете эту часть.)
@Configuration
Теперь, когда объекты, к которым вы обычно применяете прокси-сервер AOP, являются стандартной частью вашего приложения, класс @Configuration
отличается - с одной стороны, вы, вероятно, никогда не намереваетесь создавать какие-либо экземпляры этого класса непосредственно самостоятельно. Этот класс действительно является просто способом написания конфигурации контейнера bean-компонента, не имеет никакого значения вне Spring, и вы знаете, что он будет использоваться Spring специальным образом и что у него есть некоторая особая семантика вне простого Java-кода - например, что @Bean
-annotated фактически определяют бобы Spring.
Из-за этого Spring может делать с этим классом гораздо более радикальные вещи, не беспокоясь о том, что он что-то сломает в вашем коде (помните, вы знаете, что вы предоставляете этот класс только для Spring, и вы никогда не собираетесь создавать или использовать его экземпляр напрямую).
На самом деле он создает прокси, который является подклассом класса @Configuration
. Таким образом, он может перехватывать вызов каждого (non- final
non- private
) метода класса @Configuration
даже внутри одного и того же объекта (поскольку все методы фактически переопределяются прокси-сервером, а в Java все методы являются виртуальными).). Прокси-сервер делает именно это, чтобы перенаправить любые вызовы методов, которые он распознает как (семантически), ссылки на бины Spring на фактические экземпляры бинов вместо вызова метода суперкласса.
Ответ 2
Используются ли разные типы динамических прокси?
Почти точно
Давайте @Configuration
, в чем разница между классами @Configuration
и прокси-серверами AOP, отвечающими на следующие вопросы:
- Почему самозванный метод
@Transactional
не имеет транзакционной семантики, даже если Spring способен перехватывать самозванные методы? - Как
@Configuration
и AOP?
Почему самозванный метод @Transactional
не имеет транзакционной семантики?
Короткий ответ:
Вот как АОП сделал.
Длинный ответ:
- Декларативное управление транзакциями опирается на AOP (для большинства приложений Spring на Spring AOP)
Декларативное управление транзакциями Spring Frameworks стало возможным благодаря аспектно-ориентированному программированию Spring (AOP)
- Он основан на прокси (§5.8.1. Понимание прокси AOP)
Spring AOP основан на прокси.
Из того же пункта SimplePojo.java
:
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
И фрагмент прокси:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
Здесь важно понимать, что клиентский код внутри метода main(..)
класса Main
имеет ссылку на прокси.
Это означает, что вызовы метода для этой ссылки на объект являются вызовами прокси.
В результате прокси может делегировать всем перехватчикам (рекомендации), которые имеют отношение к этому конкретному вызову метода.
Однако, как только вызов наконец достигнет целевого объекта (SimplePojo
, в данном случае this.foo()
, будут вызываться любые вызовы методов, которые он может сделать для себя, такие как this.bar()
или this.foo()
против this
ссылки, а не прокси.
Это имеет важные последствия. Это означает, что самовывоз не приведет к тому, что совет, связанный с вызовом метода, получит шанс на выполнение.
(Ключевые части выделены.)
Вы можете подумать, что AOP работает следующим образом:
Представьте, что у нас есть класс Foo
который мы хотим прокси:
Foo.java
:
public class Foo {
public int getInt() {
return 42;
}
}
Там нет ничего особенного. Просто метод getInt
возвращающий 42
Перехватчик:
Interceptor.java
:
public interface Interceptor {
Object invoke(InterceptingFoo interceptingFoo);
}
LogInterceptor.java
(для демонстрации):
public class LogInterceptor implements Interceptor {
@Override
public Object invoke(InterceptingFoo interceptingFoo) {
System.out.println("log. before");
try {
return interceptingFoo.getInt();
} finally {
System.out.println("log. after");
}
}
}
InvokeTargetInterceptor.java
:
public class InvokeTargetInterceptor implements Interceptor {
@Override
public Object invoke(InterceptingFoo interceptingFoo) {
try {
System.out.println("Invoking target");
Object targetRetVal = interceptingFoo.method.invoke(interceptingFoo.target);
System.out.println("Target returned " + targetRetVal);
return targetRetVal;
} catch (Throwable t) {
throw new RuntimeException(t);
} finally {
System.out.println("Invoked target");
}
}
}
Наконец InterceptingFoo.java
:
public class InterceptingFoo extends Foo {
public Foo target;
public List<Interceptor> interceptors = new ArrayList<>();
public int index = 0;
public Method method;
@Override
public int getInt() {
try {
Interceptor interceptor = interceptors.get(index++);
return (Integer) interceptor.invoke(this);
} finally {
index--;
}
}
}
Проводить все вместе:
public static void main(String[] args) throws Throwable {
Foo target = new Foo();
InterceptingFoo interceptingFoo = new InterceptingFoo();
interceptingFoo.method = Foo.class.getDeclaredMethod("getInt");
interceptingFoo.target = target;
interceptingFoo.interceptors.add(new LogInterceptor());
interceptingFoo.interceptors.add(new InvokeTargetInterceptor());
interceptingFoo.getInt();
interceptingFoo.getInt();
}
Распечатает:
log. before
Invoking target
Target returned 42
Invoked target
log. after
log. before
Invoking target
Target returned 42
Invoked target
log. after
Теперь давайте взглянем на ReflectiveMethodInvocation
.
Вот часть его метода proceed
:
Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
++this.currentInterceptorIndex
должен выглядеть знакомо
Вы можете попытаться ввести несколько аспектов в свое приложение и увидеть, как увеличивается стек при методе proceed
когда вызывается метод advised
Наконец все заканчивается в MethodProxy.
Из его метода invoke
javadoc:
Вызвать оригинальный метод для другого объекта того же типа.
И, как я уже упоминал ранее, документация:
как только вызов, наконец, достигнет target
объекта, любые вызовы методов, которые он может совершить для себя, будут вызываться по ссылке this
, а не через прокси
Надеюсь сейчас, более или менее, понятно почему.
Как @Configuration
и AOP?
Ответ - они не связаны.
Так что spring здесь свободна делать все, что захочет. Здесь это не связано с семантикой прокси АОП.
Это улучшает такие классы, используя ConfigurationClassEnhancer
.
Взгляни на:
Возвращаясь к вопросу
Если Spring может успешно перехватывать вызовы функций внутри класса в классе @Configuration, почему он не поддерживает его в обычном компоненте?
Надеюсь с технической точки зрения понятно почему.
Теперь мои мысли с нетехнической стороны:
Я думаю, что это не сделано, потому что Spring AOP здесь достаточно долго...
Начиная с Spring Framework 5, была представлена среда Spring WebFlux.
В настоящее время Spring Team работает над улучшением модели реактивного программирования.
Смотрите некоторые заметные недавние сообщения в блоге:
Вводятся все новые и новые функции в направлении упрощенного построения приложений Spring. (см. этот коммит например)
Поэтому я думаю, что, хотя можно сделать то, что вы описали, это далеко от приоритета Spring Team # 1.
Ответ 3
прочитайте немного весеннего исходного кода. Я пытаюсь ответить на это.
@Configuration
том, как spring справиться с @Configuration
и @bean
. в ConfigurationClassPostProcessor, который является BeanFactoryPostProcessor, он улучшит все ConfigurationClasses и создаст Enhancer
в качестве подкласса. этот Enhancer
регистрирует два ЗВОНОКА (BeanMethodInterceptor, BeanFactoryAwareMethodInterceptor). Вы вызываете метод PersistenceJPAConfig
который будет проходить через ЗВОНОКИ. в BeanMethodInterceptor он получает бин из контейнера Spring.
это может быть не ясно. Вы можете увидеть исходный код в ConfigurationClassEnhancer.java BeanMethodInterceptor
. ConfigurationClassPostProcessor.java enhanceConfigurationClasses
Ответ 4
Вы не можете вызвать метод @Transactional в том же классе
Это ограничение Spring AOP (динамические объекты и cglib).
Если вы настроите Spring для использования транзакций AspectJ, ваш код будет работать.
Простая и, вероятно, лучшая альтернатива - это рефакторинг вашего кода. Например, один класс, который обрабатывает пользователей, и один, который обрабатывает каждого пользователя. Тогда будет работать обработка транзакций по умолчанию в Spring AOP.
Также @Transactional должен быть на слое сервиса, а не на @Repository
транзакции принадлежат сервисному уровню. Это тот, который знает о единицах работы и случаях использования. Это правильный ответ, если у вас есть несколько DAO, внедренных в Сервис, которые должны работать вместе в одной транзакции.
Поэтому вам необходимо переосмыслить свой подход к транзакциям, чтобы ваши методы можно было повторно использовать в потоке, включая несколько других операций DAO, которые можно свернуть.
Ответ 5
Spring использует прокси для вызова метода, и когда вы используете это... он обходит этот прокси. Для аннотаций @Bean Spring использует отражение, чтобы найти их.