Весна - @Transactional - Что происходит в фоновом режиме?


334

Я хочу знать, что на самом деле происходит, когда вы аннотируете метод с помощью @Transactional? Конечно, я знаю, что Spring обернет этот метод в транзакции.

Но у меня есть следующие сомнения:

  1. Я слышал, что Spring создает прокси-класс ? Может кто-нибудь объяснить это более подробно . Что на самом деле находится в этом прокси-классе? Что происходит с реальным классом? И как я могу увидеть созданный Spring прокси класс
  2. Я также прочитал в Spring документы, которые:

Примечание. Поскольку этот механизм основан на прокси-серверах, будут перехватываться только внешние вызовы методов, поступающие через прокси-сервер . Это означает, что «самовывоз», то есть метод в целевом объекте, вызывающий какой-либо другой метод целевого объекта, не приведет к реальной транзакции во время выполнения, даже если вызванный метод помечен знаком @Transactional!

Источник: http://static.springsource.org/spring/docs/2.0.x/reference/transaction.html

Почему в транзакции будут только внешние вызовы методов, а не методы самовывоза?


2
Соответствующее обсуждение здесь: stackoverflow.com/questions/3120143/…
dma_k

Ответы:


255

Это большая тема. Справочный документ Spring посвящен нескольким главам. Я рекомендую ознакомиться с ними по аспектно-ориентированному программированию и транзакциям , так как поддержка декларативных транзакций Spring использует AOP в своей основе.

Но на очень высоком уровне Spring создает прокси для классов, которые объявляют @Transactional для самого класса или для членов. Прокси обычно невидим во время выполнения. Он предоставляет Spring способ внедрить поведение до, после или вокруг вызовов методов в объекте, который проксируется. Управление транзакциями - это только один пример поведения, которое можно подключить. Проверки безопасности - другой. И вы можете предоставить свои собственные, например, для ведения журнала. Поэтому, когда вы аннотируете метод с помощью @Transactional , Spring динамически создает прокси, который реализует тот же интерфейс (ы), что и класс, который вы аннотируете. И когда клиенты делают вызовы в ваш объект, вызовы перехватываются, а поведение вводится через механизм прокси.

Между прочим, транзакции в EJB работают аналогично.

Как вы заметили, прокси-механизм работает только тогда, когда поступают вызовы от какого-то внешнего объекта. Когда вы делаете внутренний вызов внутри объекта, вы действительно делаете вызов через ссылку « this », которая обходит прокси. Однако есть способы обойти эту проблему. Я объясняю один подход в этом сообщении на форуме, в котором я использую BeanFactoryPostProcessor для внедрения экземпляра прокси-сервера в « самоссылающиеся » классы во время выполнения. Я сохраняю эту ссылку в переменной-члене " me ». Затем, если мне нужно сделать внутренние вызовы, которые требуют изменения статуса транзакции потока, я направляю вызов через прокси (например, me.someMethod () ".) В сообщении на форуме объясняется более подробно. Обратите внимание, что BeanFactoryPostProcessorкод теперь будет немного другим, так как он был написан еще во времена Spring 1.x. Но, надеюсь, это даст вам идею. У меня есть обновленная версия, которую я мог бы сделать доступной.


4
>> Прокси в основном невидим во время выполнения Oh !! Мне любопытно увидеть их :) Отдых .. Ваш ответ был очень всеобъемлющим. Это второй раз, когда вы мне помогаете .. Спасибо за всю помощь.
пик

17
Нет проблем. Вы можете увидеть прокси-код, если перейдете к отладчику. Это, наверное, самый простой способ. Там нет магии; это просто классы внутри пакетов Spring.
Роб Х

И если метод с аннотацией @Transaction реализует интерфейс, пружина будет использовать динамический прокси-API для внедрения транзакции и не будет использовать прокси. Я предпочитаю, чтобы мои транзакционные классы реализовывали интерфейсы в любом случае.
Майкл Уайлс

1
Я также нашел схему «я» (используя явную разводку, чтобы сделать это так, как мне нравится), но я думаю, что если вы делаете это таким образом, вам, вероятно, лучше провести рефакторинг, чтобы вы этого не делали должен. Но да, это иногда может быть очень неловко!
Donal Fellows

2
2019: По мере того, как этот ответ стареет, упомянутое сообщение на форуме больше не доступно, и в нем будет описан случай, когда необходимо выполнить внутренний вызов внутри объекта, не обходя прокси, используяBeanFactoryPostProcessor . Тем не менее, есть (на мой взгляд) очень похожий метод, описанный в этом ответе: stackoverflow.com/a/11277899/3667003 ... и другие решения также во всей цепочке.
Z3d4s

196

Когда Spring загружает ваши определения bean-компонентов и настроен на поиск @Transactionalаннотаций, он создает эти прокси-объекты вокруг вашего фактического bean-компонента . Эти прокси-объекты являются экземплярами классов, которые автоматически генерируются во время выполнения. Поведение этих прокси-объектов по умолчанию при вызове метода состоит в том, чтобы просто вызывать тот же метод для «целевого» компонента (т. Е. Вашего компонента).

Однако прокси также могут быть снабжены перехватчиками, и когда они присутствуют, эти перехватчики будут вызываться прокси, прежде чем он вызовет метод вашего целевого компонента. Для целевых компонентов, помеченных как @Transactional, Spring создаст TransactionInterceptorи передаст его сгенерированному прокси-объекту. Поэтому, когда вы вызываете метод из клиентского кода, вы вызываете метод для прокси-объекта, который сначала вызывает TransactionInterceptor(который начинает транзакцию), который, в свою очередь, вызывает метод для вашего целевого компонента. По завершении вызова TransactionInterceptorфиксирует / откатывает транзакцию. Это прозрачно для клиентского кода.

Что касается «внешнего метода», если ваш компонент вызывает один из своих собственных методов, он не будет делать это через прокси. Помните, Spring оборачивает ваш компонент в прокси, ваш компонент не знает об этом. Только вызовы "извне" вашего компонента проходят через прокси.

Это помогает?


36
> Помните, Spring оборачивает ваш бин в прокси, ваш бин не знает об этом Это все сказано. Какой отличный ответ. Спасибо за помощь.
пик

Отличное объяснение, для прокси и перехватчиков. Теперь я понимаю, что Spring реализует прокси-объект для перехвата вызовов целевого компонента. Спасибо!
Дхараг

Я думаю , что вы пытаетесь описать эту картину документации Spring и увидеть эту картину мне очень помогает: docs.spring.io/spring/docs/4.2.x/spring-framework-reference/...
WesternGun

44

Как визуальный человек, мне нравится взвешивать диаграмму последовательности паттерна прокси. Если вы не знаете, как читать стрелки, я прочитал первый, например, так: Clientвыполняет Proxy.method().

  1. Клиент вызывает метод для цели с его точки зрения и молча перехватывается прокси
  2. Если определен аспект before, прокси выполнит его
  3. Затем фактический метод (цель) выполняется
  4. After-return и after-throwing являются необязательными аспектами, которые выполняются после возврата метода и / или если метод выдает исключение
  5. После этого прокси выполняет аспект after (если он определен)
  6. Наконец прокси возвращается к вызывающему клиенту

Диаграмма последовательности шаблонов прокси (Мне разрешили опубликовать фотографию при условии, что я упомянул ее происхождение. Автор: Ноэль Ваес, веб-сайт: www.noelvaes.eu)


27

Самый простой ответ:

В зависимости от того, какой метод вы объявляете, @Transactionalграница транзакции начинается и заканчивается, когда метод завершается.

Если вы используете вызов JPA, то все коммиты находятся в этой границе транзакции .

Допустим, вы сохраняете entity1, entity2 и entity3. Теперь при сохранении entity3 происходит исключение , тогда как enitiy1 и entity2 входят в одну и ту же транзакцию, поэтому entity1 и entity2 будут откатываться с entity3.

Транзакция:

  1. entity1.save
  2. entity2.save
  3. entity3.save

Любое исключение приведет к откату всех транзакций JPA с DB. Внутренне транзакция JPA используется Spring.


2
«Исключение A̶n̶y̶ приведет к откату всех транзакций JPA с БД». Примечание. Только RuntimeException приводит к откату. Проверенные исключения, не приведут к откату.
Арджун

2

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

Например, у вас есть класс, который выглядит следующим образом

@Component("mySubordinate")
public class CoreBusinessSubordinate {

    public void doSomethingBig() {
        System.out.println("I did something small");
    }

    public void doSomethingSmall(int x){
        System.out.println("I also do something small but with an int");    
  }
}

и у вас есть аспект, который выглядит так:

@Component
@Aspect
public class CrossCuttingConcern {

    @Before("execution(* com.intertech.CoreBusinessSubordinate.*(..))")
    public void doCrossCutStuff(){
        System.out.println("Doing the cross cutting concern now");
    }
}

Когда вы выполняете это так:

 @Service
public class CoreBusinessKickOff {

    @Autowired
    CoreBusinessSubordinate subordinate;

    // getter/setters

    public void kickOff() {
       System.out.println("I do something big");
       subordinate.doSomethingBig();
       subordinate.doSomethingSmall(4);
   }

}

Результаты вызова kickOff выше указанного кода выше.

I do something big
Doing the cross cutting concern now
I did something small
Doing the cross cutting concern now
I also do something small but with an int

но когда вы меняете код на

@Component("mySubordinate")
public class CoreBusinessSubordinate {

    public void doSomethingBig() {
        System.out.println("I did something small");
        doSomethingSmall(4);
    }

    public void doSomethingSmall(int x){
       System.out.println("I also do something small but with an int");    
   }
}


public void kickOff() {
  System.out.println("I do something big");
   subordinate.doSomethingBig();
   //subordinate.doSomethingSmall(4);
}

Видите ли, метод внутренне вызывает другой метод, поэтому он не будет перехвачен, и результат будет выглядеть следующим образом:

I do something big
Doing the cross cutting concern now
I did something small
I also do something small but with an int

Вы можете обойти это, делая это

public void doSomethingBig() {
    System.out.println("I did something small");
    //doSomethingSmall(4);
    ((CoreBusinessSubordinate) AopContext.currentProxy()).doSomethingSmall(4);
}

Фрагменты кода взяты из: https://www.intertech.com/Blog/secrets-of-the-spring-aop-proxy/


1

Все существующие ответы верны, но я чувствую, что не могу дать только эту сложную тему.

Чтобы получить исчерпывающее практическое объяснение, вам, возможно, захочется взглянуть на это подробное руководство Spring @Transactional , в котором изо всех сил рассматривается управление транзакциями в ~ 4000 простых слов с множеством примеров кода.


Это было здорово ...
Alpit Anand
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.