Если Spring может успешно перехватывать вызовы функций внутри класса в классе @Configuration, почему он не поддерживает его в обычном компоненте? - PullRequest
21 голосов
/ 18 мая 2019

Я недавно заметил, что Spring успешно перехватывает вызовы функций внутри класса в классе @Configuration, но не в обычном bean-компоненте.

Такой вызов

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

Тем не менее, в следующем примере, когда TransactionsManager () вызывает метод createDataSource (), он корректно перехватывается и вызывает метод createDataSource () прокси, а не развернутого класса, о чем свидетельствует просмотр «this» вотладчик.

@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 функциями, которые выполняют дополнительную работу перед вызовом пользовательской / суперфункции, такой как проверка, является ли это первым вызовом функции или нет.Это класс прокси?Это зависит от определения.Вы можете сказать, что это прокси, который использует наследование от реального объекта вместо того, чтобы оборачивать его с помощью композиции.

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

Ответы [ 5 ]

9 голосов
/ 21 мая 2019

Используются ли разные типы динамических прокси?

Почти точно

Давайте выясним, в чем разница между классами @Configuration иПрокси-серверы AOP отвечают на следующие вопросы:

  1. Почему метод с собственным вызовом @Transactional не имеет транзакционной семантики, даже если Spring способен перехватывать методы с собственным вызовом?
  2. Как @Configurationи АОП связаны между собой?

Почему метод с собственным вызовом @Transactional не имеет транзакционной семантики?

Краткий ответ:

Этокак AOP сделал.

Длинный ответ:

  1. Декларативное управление транзакциями опирается на AOP (для большинстваприложений Spring в Spring AOP )

Декларативное управление транзакциями в Spring Framework стало возможным благодаря 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 имеет ссылку на прокси 1069 *.

Это означает, что вызовы методов для этой ссылки на объект являются вызовами для прокси .

В результате прокси может делегировать все перехватчики (рекомендации), которые имеют отношение к этому конкретному вызову метода.

Однако, как только вызов наконец достиг целевого объекта (SimplePojo, в данном случае ссылка), любые вызовы методов, которые он может выполнить для себя, такие как 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, когда вызывается рекомендованный метод

Наконец все заканчивается на MethodProxy .

Из его invoke метода javadoc:

Invoke thОригинальный метод для другого объекта того же типа.

И, как я упоминал ранее, документация:

после того, как вызов наконец достиг объекта target, любойвызовы методов, которые он может совершать сам по себе, будут вызываться по ссылке this, а не по доверенности

Надеюсь, теперь более или менее понятно, почему.

Как @Configuration и АОП связаны?

Ответ: они не связаны .

Так что Весна здесь свободна делать все, что захочет.Здесь он не привязан к семантике прокси AOP .

Он расширяет такие классы, используя ConfigurationClassEnhancer.

Взгляните на:

Возвращаясь к вопросу

If Springможет успешно перехватывать вызовы функций внутри класса в классе @Configuration, почему он не поддерживает его в обычном компоненте?

I надеюсь с технической точки зрения понятно, почему.

Теперь мои мысли с нетехнической стороны:

Я думаю, что это не сделано, потому что Spring AOP здесь достаточно долго ...

Начиная с Spring Framework 5, была введена Spring WebFlux Framework.

В настоящее время Spring Team усердно работает над улучшением реактивного модель программирования

См. некоторые заметные последние сообщения в блоге :

Больше и больше возможностей для Внедрен менее подходящий подход к созданию приложений Spring.(см., например, этот коммит )

Так что я думаю, что, несмотря на то, что возможно сделать то, что вы описали, это далеко от приоритета Spring Team # 1 на данный момент

6 голосов
/ 23 мая 2019

Поскольку прокси-серверы AOP и класс @Configuration служат разным целям и реализуются существенно разными способами (даже при том, что оба используют прокси-серверы). По сути, AOP использует композицию, в то время как @Configuration использует наследование .

AOP-прокси

Принцип работы этих функций заключается в том, что они создают прокси, которые выполняютсоответствующая логическая схема до / после делегирование вызова исходного (прокси) объекта.Контейнер регистрирует этот прокси вместо самого прокси-объекта, поэтому все зависимости установлены для этого прокси, и все вызовы от одного компонента к другому проходят через этот прокси.Однако сам проксируемый объект не имеет указателя на прокси (он не знает, что он проксирован, только прокси имеет указатель на целевой объект).Поэтому любые вызовы в этом объекте других методов не проходят через прокси.

(я добавляю это здесь только для сравнения с @Configuration, так как вы, кажется, правильно понимаете эту часть.)

@ Configuration

Теперь, когда объекты, к которым вы обычно применяете прокси-сервер AOP, являются стандартной частью вашего приложения, класс @Configuration отличается - например,вы, вероятно, никогда не намереваетесь создавать какие-либо экземпляры этого класса напрямую.Этот класс действительно является просто способом написания конфигурации контейнера bean-компонента, он не имеет никакого значения вне Spring, и вы знаете , что он будет использоваться Spring специальным образом и что у него есть особая семантика внепростой Java-код - например, что @Bean -аннотированные методы фактически определяют бины Spring.

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

На самом деле он создает прокси, который является подклассом @Configuration класс .Таким образом, он может перехватывать вызов любого (не final non- private) метода класса @Configuration, даже внутри одного и того же объекта (поскольку все методы фактически переопределяются прокси, а Java имеет всеметоды виртуальные).Прокси-сервер делает именно это, чтобы перенаправить любые вызовы методов, которые он распознает как (семантически) ссылки на bean-компоненты Spring, на фактические экземпляры bean-компонентов вместо вызова метода суперкласса.

4 голосов
/ 18 мая 2019

прочитайте немного весеннего исходного кода.Я пытаюсь ответить на него.

Дело в том, как весной справиться с @Configuration и @bean.в ConfigurationClassPostProcessor, который является BeanFactoryPostProcessor, он улучшит все ConfigurationClasses и создаст Enhancer в качестве подкласса.это Enhancer регистрирует два ЗВОНКА (BeanMethodInterceptor, BeanFactoryAwareMethodInterceptor).Вызовите PersistenceJPAConfig, метод пройдет через ЗВОНОКИ.в BeanMethodInterceptor он будет получать бин из контейнера Spring.

это может быть неясно.Вы можете увидеть исходный код в ConfigurationClassEnhancer.java BeanMethodInterceptor. ConfigurationClassPostProcessor.java enhanceConfigurationClasses

1 голос
/ 20 мая 2019

Вы не можете вызвать метод @Transactional в том же классе

Это ограничение Spring AOP (динамические объекты и cglib).

Если вы настроите Spring для использования транзакций в AspectJ, ваш код будет работать.

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

Также @Transactional должен быть на Сервисном слое , а не на @ Repository

транзакции принадлежат уровню сервиса. Это тот, который знает о единицах работы и случаях использования. Это правильный ответ, если у вас есть несколько DAO, внедренных в Службу, которые должны работать вместе в одной транзакции.

Таким образом, вам необходимо переосмыслить свой подход к транзакциям, чтобы ваши методы можно было повторно использовать в потоке, включая несколько других операций DAO, которые с возможностью прокрутки

0 голосов
/ 18 мая 2019

Spring использует прокси для вызова метода, и когда вы используете это ... он обходит этот прокси. Для аннотаций @Bean Spring использует отражение, чтобы найти их.

...