Если 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, отвечающими на следующие вопросы:

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

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

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

Вот как АОП сделал.

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

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

Декларативное управление транзакциями Spring Frameworks стало возможным благодаря аспектно-ориентированному программированию Spring (AOP)

  1. Он основан на прокси (§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 использует отражение, чтобы найти их.