Большинство пользователей Spring Framework выбирают декларативное управление транзакциями. Этот вариант оказывает наименьшее влияние на код приложения и, следовательно, наиболее соответствует идее неагрессивного облегченного контейнера.

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

Декларативное управление транзакциями в Spring Framework похоже на CMT из EJB, поскольку можно определить логику работы транзакций (или её отсутствие) вплоть до уровня отдельных методов. При необходимости можно выполнить вызов setRollbackOnly() в контексте транзакции. Различия между двумя типами управления транзакциями заключаются в следующем:

  • В отличие от CMT из EJB, который привязан к JTA, декларативное управление транзакциями в Spring Framework работает в любом окружении. Он может работать с транзакциями JTA или локальными транзакциями с помощью JDBC, JPA или Hibernate путем корректировки конфигурационных файлов.

  • Можно применять декларативное управление транзакциями в Spring Framework к любому классу, а не только к специальным классам, таким как классы EJB.

  • Spring Framework предлагает декларативные правила отката, которые не имеют аналогов в EJB. Предусмотрена как программная, так и декларативная поддержка правил отката.

  • Spring Framework позволяет настраивать логику работы транзакций с помощью АОП. Например, можно внедрить кастомную логику работы в случае отката транзакции. Также можно добавлять произвольные Advice наряду с транзакционными Advice. При использовании CMT из EJB нельзя влиять на управление транзакциями контейнера, кроме как с помощью setRollbackOnly().

  • Spring Framework не поддерживает распространение контекстов транзакций через удаленные вызовы, как это делают серверы приложений высшего класса. Если вам нужна эта функция, мы рекомендуем использовать EJB. Однако хорошенько подумайте, прежде чем использовать такую функцию, потому что, как правило, транзакции не должны охватываться удаленными вызовами.

Концепция правил отката очень важна. Эти правила позволяют задать, какие исключения (и выбрасываемые события) должны приводить к автоматическому откату. Вы можете задать это декларативно, в конфигурации, а не в коде Java. Поэтому, хотя вы все еще можете вызвать setRollbackOnly() для объекта TransactionStatus, чтобы откатить ткущую транзакцию назад, чаще всего лучше задать правило, согласно которому исключение MyApplicationExceptionException всегда должно приводить к откату. Существенным преимуществом этого варианта является то, что бизнес-объекты не будут зависеть от транзакционной инфраструктуры. Например, они обычно не требуют импортирования API-интерфейсов транзакций Spring или других API-интерфейсов Spring.

Хотя логика работы контейнера EJB по умолчанию автоматически откатывает транзакцию при системном исключении (обычно исключении времени выполнения), CMT из EJB не откатывает транзакцию автоматически при исключении приложения (то есть проверяемом исключении, отличном от java.rmi.RemoteException). Хотя логика работы Spring по умолчанию для декларативного управления транзакциями следует соглашению EJB (откат происходит автоматически только при непроверяемых исключениях), часто бывает полезно настроить эту логику работы.

Основные сведения о реализации декларативной транзакции в Spring Framework

Мало просто сказать, что нужно пометить классы аннотацией @Transactional, добавить аннотацию @EnableTransactionManagement в вашу конфигурацию и ожидать, что вы поймете, как все это работает. Чтобы дать более глубокое понимание, в этом разделе описаны внутренние механизмы декларативной транзакционной инфраструктуры Spring Framework в контексте вопросов, связанных с транзакциями.

Наиболее важные концепции, которые необходимо усвоить в отношении поддержки декларативных транзакций в Spring Framework, заключаются в том, что эта поддержка обеспечивается благодаря прокси АОП и что транзакционные Advice управляются метаданными (в настоящее время на основе XML или аннотаций). Сочетание АОП с транзакционными метаданными дает прокси АОП, который задействует TransactionInterceptor в сочетании с соответствующей реализацией TransactionManager для управления транзакциями вокруг вызовов методов.

АОП в Spring рассмотрено в разделе по АОП.

TransactionInterceptor в Spring Framework обеспечивает управление транзакциями для императивных и реактивных моделей программирования. Перехватчик определяет желаемый тип управления транзакциями, проверяя возвращаемый тип метода. Методы, возвращающие реактивный тип, такой как Publisher или Flow (или их подтип) на языке Kotlin, подходят для управления реактивными транзакциями. Все остальные возвращаемые типы, включая void, используют путь выполнения кода для императивного управления транзакциями.

Особенности управления транзакциями влияют на то, какой диспетчер транзакций требуется. Императивные транзакции требуют PlatformTransactionManager, а реактивные транзакции используют реализацию ReactiveTransactionManager.

Аннотация @Transactional обычно работает с привязанными к потоку транзакциями, управляемыми PlatformTransactionManager, открывая транзакцию для всех операций доступа к данным в текущем выполняемом потоке. Примечание: это не распространяется на вновь запущенные потоки внутри метода.

Реактивная транзакция, управляемая ReactiveTransactionManager, использует контекст из Reactor вместо локальных атрибутов потока. Как следствие, все участвующие операции доступа к данным должны выполняться в одном и том же контексте из Reactor в одном и том же реактивном конвейере.

На следующем рисунке показано концептуальное представление вызова метода для транзакционного прокси:

Пример реализации декларативной транзакции

Рассмотрим следующий интерфейс и его соответствующую реализацию. В этом примере классы Foo и Bar используются в качестве плейсхолдеров, чтобы вы могли сосредоточиться на использовании транзакций, не отвлекаясь на конкретную модель предметной области. Для целей данного примера тот факт, что класс DefaultFooService генерирует экземпляры UnsupportedOperationException в теле каждого реализованного метода, является положительным. Такая логика работы позволяет понять, как создаются транзакции, а затем откатываются в ответ на экземпляр UnsupportedOperationException. В следующем листинге показан интерфейс FooService:

Java
// интерфейс службы, который мы хотим сделать транзакционным
package x.y.service;
public interface FooService {
    Foo getFoo(String fooName);
    Foo getFoo(String fooName, String barName);
    void insertFoo(Foo foo);
    void updateFoo(Foo foo);
}
Kotlin
// интерфейс службы, который мы хотим сделать транзакционным
package x.y.service
interface FooService {
    fun getFoo(fooName: String): Foo
    fun getFoo(fooName: String, barName: String): Foo
    fun insertFoo(foo: Foo)
    fun updateFoo(foo: Foo)
}

В следующем примере показана реализация предыдущего интерфейса:

Java
package x.y.service;
public class DefaultFooService implements FooService {
    @Override
    public Foo getFoo(String fooName) {
        // ...
    }
    @Override
    public Foo getFoo(String fooName, String barName) {
        // ...
    }
    @Override
    public void insertFoo(Foo foo) {
        // ...
    }
    @Override
    public void updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
package x.y.service
class DefaultFooService : FooService {
    override fun getFoo(fooName: String): Foo {
        // ...
    }
    override fun getFoo(fooName: String, barName: String): Foo {
        // ...
    }
    override fun insertFoo(foo: Foo) {
        // ...
    }
    override fun updateFoo(foo: Foo) {
        // ...
    }
}

Предположим, что первые два метода интерфейса FooService, getFoo(String) и getFoo(String, String), должны выполняться в контексте некой транзакции с семантикой "только для чтения", а другие методы, insertFoo(Foo) и updateFoo(Foo), должны выполняться в контексте транзакции с семантикой "чтение-запись". Следующая конфигурация подробно описана в следующих нескольких абзацах:

<!-- из файла "context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- это объект-служба, который мы хотим сделать транзакционным -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <!-- транзакционный Advice (что "происходит"; см. <aop:advisor/> bean ниже) -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- транзакционная семантика...... -->
        <tx:attributes>
            <!-- все методы, начинающиеся с "get", доступны только для чтения -->
            <tx:method name="get*" read-only="true"/>
            <!-- другие методы используют параметры транзакции по умолчанию (см. ниже) -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    <!-- убеждаемся, что вышеупомянутый транзакционный Advice выполняется при любом выполнении
        операции, определенной интерфейсом FooService -->
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>
    <!-- не забываем про DataSource -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>
    <!-- Аналогично, не забываем про TransactionManager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- другие определения <bean/--> -->
</beans>

Изучите предыдущую конфигурацию. Предполагается, что вам нужно сделать объект-службу, бин fooService, транзакционным. Семантика транзакции, которую необходимо применить, заключена в определении <tx:advice/>. Определение <tx:advice/> гласит: "все методы, начинающиеся с get, должны выполняться в контексте транзакции "только для чтения", а все остальные методы должны выполняться с семантикой транзакции по умолчанию". Атрибут transaction-manager тега <tx:advice/> указывается в имени бина TransactionManager, который будет управлять транзакциями (в данном случае это бин txManager).

Можно опустить атрибут transaction-manager в транзакционном Adviceе (<tx:advice/>), если имя бина TransactionManager, который нужно подключить, transactionManager. Если бин TransactionManager, который вам нужно связать, имеет любое другое имя, то нужно использовать атрибут transaction-manager явным образом, как это сделано в предыдущем примере.

Определение <aop:config/> позволяет транзакционным рекомендациям, определенным бином txAdvice, гарантированно запускаться в соответствующих точках программы. Сначала определяется срез, который приводится в соответствие с выполнением любой операции, определенной в интерфейсе FooService(fooServiceOperation). Затем срез привязывается к txAdvice с помощью Advisor. Результат показывает, что при выполнении fooServiceOperation начинает выполняться Advice, определенный txAdvice.

Выражение, определенное в элементе <aop:pointcut/>, является выражением срезов из AspectJ. Более подробную информацию о выражениях срезов в Spring см. в разделе по АОП.

Часто необходимо сделать весь уровень служб транзакционным. Лучший способ сделать это – изменить выражение среза так, чтобы оно соответствовало любой операции на уровне служб. В следующем примере показано, как это сделать:

<aop:config>
    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>
В предыдущем примере предполагается, что все служебные интерфейсы определены в пакете x.y.service. Более подробную информацию см. в разделе по АОП.

Теперь, когда мы проанализировали конфигурацию, то у вас может возникнуть вопрос: "А что, собственно, делает вся эта конфигурация?".

Конфигурация, показанная ранее, используется для создания транзакционного прокси вокруг объекта, который создается из определения бина fooService. Прокси сконфигурирован с использованием транзакционных Advice таким образом, что когда соответствующий метод вызывается на прокси, транзакция запускается, приостанавливается, помечается как доступная только для чтения и так далее, в зависимости от конфигурации транзакции, связанной с этим методом. Рассмотрим следующую программу, которая совершает тестовый прогон конфигурации, показанной ранее:

Java
public final class Boot {
    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");
        FooService fooService = ctx.getBean(FooService.class);
        fooService.insertFoo(new Foo());
    }
}
Kotlin
import org.springframework.beans.factory.getBean
fun main() {
    val ctx = ClassPathXmlApplicationContext("context.xml")
    val fooService = ctx.getBean<FooService>("fooService")
    fooService.insertFoo(Foo())
}

Результат выполнения предыдущей программы должен выглядеть следующим образом (вывод Log4J и трассировка стека от UnsupportedOperationException, сгенерированного методом insertFoo(..) класса DefaultFooService, были усечены для наглядности):

<!-- контейнер Spring запускается... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors
<!-- DefaultFooService фактически проксируется -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]
<!-- ... метод insertFoo(..) теперь вызывается на прокси -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo
<!-- здесь вступает в действие транзакционный Advice...  -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction
<!-- метод insertFoo(..) из DefaultFooService генерирует исключение... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]
<!-- и транзакция откатывается (по умолчанию экземпляры RuntimeException приводят к откату) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource
Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- Элементы трассировки стека инфраструктуры АОП удалены для наглядности -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)

Чтобы использовать управление реактивными транзакциями, код должен использовать реактивные типы.

Spring Framework использует ReactiveAdapterRegistry для определения того, является ли возвращаемый тип метода реактивным.

В следующем листинге показана модифицированная версия ранее использовавшегося FooService, но на этот раз в коде используются реактивные типы:

Java
// интерфейс реактивного сервиса, который мы хотим сделать транзакционным
package x.y.service;
public interface FooService {
    Flux<Foo> getFoo(String fooName);
    Publisher<Foo> getFoo(String fooName, String barName);
    Mono<Void> insertFoo(Foo foo);
    Mono<Void> updateFoo(Foo foo);
}
Kotlin
// интерфейс реактивного сервиса, который мы хотим сделать транзакционным
package x.y.service
interface FooService {
    fun getFoo(fooName: String): Flow<Foo>
    fun getFoo(fooName: String, barName: String): Publisher<Foo>
    fun insertFoo(foo: Foo) : Mono<Void>
    fun updateFoo(foo: Foo) : Mono<Void>
}

В следующем примере показана реализация предыдущего интерфейса:

Java
package x.y.service;
public class DefaultFooService implements FooService {
    @Override
    public Flux<Foo> getFoo(String fooName) {
        // ...
    }
    @Override
    public Publisher<Foo> getFoo(String fooName, String barName) {
        // ...
    }
    @Override
    public Mono<Void> insertFoo(Foo foo) {
        // ...
    }
    @Override
    public Mono<Void> updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
package x.y.service
class DefaultFooService : FooService {
    override fun getFoo(fooName: String): Flow<Foo> {
        // ...
    }
    override fun getFoo(fooName: String, barName: String): Publisher<Foo> {
        // ...
    }
    override fun insertFoo(foo: Foo): Mono<Void> {
        // ...
    }
    override fun updateFoo(foo: Foo): Mono<Void> {
        // ...
    }
}

Императивное и реактивное управление транзакциями имеют одинаковую семантику для определения границ транзакций и атрибутов транзакций. Основное различие между императивными и реактивными транзакциями заключается в отложенном характере последних. Для начала TransactionInterceptor декорирует возвращаемый реактивный тип транзакционным оператором и очищает транзакцию. Поэтому вызов транзакционного реактивного метода передает фактическое управление транзакциями типу подписки, который активирует обработку реактивного типа.

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

Возвращаемые значения методов императивных транзакций возвращаются из транзакционных методов при успешном завершении метода, так что частично вычисленные результаты не выходят из замыкания метода.

Методы реактивных транзакций возвращают тип реактивной функции-обёртки, который представляет собой последовательность вычислений вместе с обещанием начать и завершить вычисления.

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

Откат декларативной транзакции

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

Рекомендуемый способ дать команду транзакционной инфраструктуре Spring Framework откатить работу транзакции – это сгенерировать Exception из кода, который в данный момент выполняется в контексте транзакции. Код транзакционной инфраструктуры Spring Framework перехватывает любое необработанное Exception, если оно поднимается по стеку вызовов, и определяет, нужно ли помечать транзакцию для отката.

В конфигурации по умолчанию код транзакционной инфраструктуры Spring Framework помечает транзакцию для отката только в случае непроверяемых исключений во время выполнения. То есть если сгенерированное исключение является экземпляром или подклассом RuntimeException. (Экземпляры Error также, по умолчанию, приводят к откату). Проверяемые исключения, которые генерируются из транзакционного метода, не приводят к откату в конфигурации по умолчанию.

Можно настроить, какие именно типы Exception помечают транзакцию для отката, включая проверяемые исключения, задав правила отката.

Правила отката

Правила отката определяют, нужно ли откатывать транзакцию при возникновении определенного исключения, и эти правила основываются на шаблонах. Шаблон может быть полным именем класса или подстрокой полностью уточненного имени класса для типа исключения (который должен быть подклассом Throwable), без поддержки подстановочных знаков в настоящее время. Например, значение "javax.servlet.ServletException" или "ServletException" будет совпадать с javax.servlet.ServletException и его подклассами.

Правила отката могут быть настроены в XML с помощью атрибутов rollback-for и no-rollback-for, которые позволяют задавать шаблоны в виде строк. При использовании аннотации @Transactional правила отката можно сконфигурировать с помощью атрибутов rollbackFor/noRollbackFor и rollbackForClassName/noRollbackForClassName, которые позволяют задавать шаблоны в виде ссылок на Class или строк, соответственно. Если тип исключения указан как ссылка на класс, то в качестве шаблона будет использоваться его полное имя. Следовательно, аннотация @Transactional(rollbackFor = example.CustomException.class) эквивалентна аннотации @Transactional(rollbackForClassName = "example.CustomException").

Вам нужно тщательно продумать, насколько конкретен шаблон и нужно ли включать информацию о пакете (это необязательно). Например, "Exception" будет соответствовать почти всему и, вероятно, будет скрывать другие правила. "java.lang.Exception" было бы подходящим, если бы "Exception" определяло правило для всех проверяемых исключений. При использовании более уникальных имен исключений, таких как "BaseBusinessException", скорее всего, не будет необходимости использовать полное имя класса для шаблона исключений.

Более того, правила отката могут стать причиной непреднамеренного совпадению для одноименных исключений и вложенных классов. Это связано с тем, что сгенерированное исключение считается соответствующим для данного правила отката, если имя сгенерированного исключения содержит шаблон исключений, сконфигурированный для этого правила отката. Например, если правило сконфигурировано на соответствие com.example.CustomException, оно будет соответствовать исключению с именем com.example.CustomExceptionV2 (исключение из того же пакета, что и CustomException, но с дополнительным суффиксом) или исключению с именем com.example.CustomException$AnotherException (исключение, объявленное как вложенный класс в CustomException).

Следующий фрагмент XML демонстрирует, как настроить откат для проверяемого, специфичного для приложения типа Exception путем предоставления шаблона исключения через атрибут rollback-for:

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
    <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

Если вам не требуется, чтобы транзакция откатывалась при генерации исключения, также можно задать правила "без отката". В следующем примере инфраструктуре транзакций Spring Framework дается команда зафиксировать соответствующую транзакцию даже в случае необработанного исключения InstrumentNotFoundException:

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

Если инфраструктура транзакций Spring Framework перехватывает исключение и обращается к сконфигурированным правилам отката, чтобы определить, отмечать ли транзакцию для отката, приоритетным становится самое строгое правило. Так, в случае следующей конфигурации любое исключение, кроме InstrumentNotFoundException, приводит к откату соответствующей транзакции:

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
    </tx:attributes>
</tx:advice>

Вы также можете указать обязательный откат программно. Несмотря на свою простоту, этот процесс довольно агрессивен и жестко связывает ваш код с инфраструктурой транзакций Spring Framework. В следующем примере показано, как программно указать обязательный откат:

Java
public void resolvePosition() {
    try {
        // sнекоторая бизнес-логика...
    } catch (NoProductInStockException ex) {
        // запускаем откат программно
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}
Kotlin
fun resolvePosition() {
    try {
        // sнекоторая бизнес-логика...
    } catch (ex: NoProductInStockException) {
        // запускаем откат программно
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

Настоятельно рекомендуется использовать декларативный подход к откату, если это вообще возможно. Программный откат доступен в случае крайней необходимости, но его использование идет вразрез с принципами чистой архитектуры на основе POJO.

Настройка разной транзакционной семантики для разных бинов

Рассмотрим сценарий, в котором есть несколько объектов уровня служб, и вам нужно применить совершенно разные транзакционные конфигурации к каждому из них. Это можно сделать, определив отдельные элементы <aop:advisor/> с различными значениями атрибутов pointcut и advice-ref.

Для сравнения сначала предположим, что все классы уровня служб определены в корневом пакете x.y.service. Чтобы все бины, которые являются экземплярами классов, определенных в этом пакете (или в подпакетах) и имена которых заканчиваются на Service, имели транзакционную конфигурацию по умолчанию, можно написать следующее:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <aop:config>
        <aop:pointcut id="serviceOperation"
                expression="execution(* x.y.service..*Service.*(..))"/>
        <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>
    </aop:config>
    <!-- эти два бина будут транзакционными... -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>
    <!-- ... а эти два бина – не будут... -->
    <bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
    <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->
    <tx:advice id="txAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    <!-- другие бины транзакционной инфраструктуры, такие как TransactionManager, опущены... -->
</beans>

В следующем примере показано, как сконфигурировать два разных бина с совершенно разными транзакционными параметрами:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <aop:config>
        <aop:pointcut id="defaultServiceOperation"
                expression="execution(* x.y.service.*Service.*(..))"/>
        <aop:pointcut id="noTxServiceOperation"
                expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>
        <aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>
        <aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>
    </aop:config>
    <!-- этот бин будет транзакционным (см. срез "defaultServiceOperation") -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <!-- этот бин также будет транзакционным, но с совершенно другими транзакционными параметрами -->
    <bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>
    <tx:advice id="defaultTxAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    <tx:advice id="noTxAdvice">
        <tx:attributes>
            <tx:method name="*" propagation="NEVER"/>
        </tx:attributes>
    </tx:advice>
    <!-- другие бины транзакционной инфраструктуры, такие как TransactionManager, опущены... -->
</beans>

Параметры <tx:advice/>

В этом разделе кратко описаны различные транзакционные параметры, которые можно задать с помощью тега <tx:advice/>. Параметры <tx:advice/> по умолчанию следующие:

  • Параметр распространения является REQUIRED.

  • Уровень изоляции - DEFAULT.

  • Транзакция выполняется по принципу "чтение-запись".

  • Время ожидания транзакции по умолчанию равно времени ожидания по умолчанию базовой системы транзакций или отсутствует, если значения времени ожидания не поддерживаются.

  • Любое RuntimeException вызывает откат, а любое проверяемое Exception – нет.

Вы можете изменить эти параметры по умолчанию. В следующей таблице приведены различные атрибуты тегов <tx:method/>, которые вложены в теги <tx:advice/> и <tx:attributes/>:

Таблица 1. Параметры <tx:method/>
Атрибут Обязательный? По умолчанию Описание

name

Да

Имена методов, с которыми должны быть связаны атрибуты транзакции. Символ подстановки (*) можно использовать для того, чтобы связать одни и те же параметры атрибутов транзакции с несколькими методами (например, get*, handle*, on*Event и так далее).

propagation

Нет

REQUIRED

Логика работы при распространении транзакций.

isolation

Нет

DEFAULT

Уровень изоляции транзакций. Применимо только к настройкам распространения REQUIRED или REQUIRES_NEW.

timeout

Нет

-1

Время ожидания транзакции (секунды). Применимо только к распространению REQUIRED или REQUIRES_NEW.

read-only

Нет

false

Транзакция для "чтения-записи" против транзакции "только для чтения". Применяется только к REQUIRED или REQUIRES_NEW.

rollback-for

Нет

Список экземпляров Exception, которые вызывают откат, через запятую. Например, com.foo.MyBusinessException,ServletException.

no-rollback-for

No

Список экземпляров Exception, которые не вызывают откат, через запятую. Например, com.foo.MyBusinessException,ServletException.

Использование аннотации @Transactional

В дополнение к декларативному подходу к конфигурации транзакций на основе XML, можно использовать подход на основе аннотаций. Объявление семантики транзакций непосредственно в исходном коде Java делает декларации гораздо ближе к затрагиваемому коду. Опасность чрезмерной связанности невелика, поскольку код, предназначенный для транзакционного использования, почти всегда развертывается именно таким образом.

Стандартная аннотация javax.transaction.Transactional также поддерживается как замена собственной аннотации Spring. Более подробную информацию можно найти в документации по JTA 1.2.

Простоту использования аннотации @Transactional лучше всего проиллюстрировать примером, который поясняется в тексте далее. Рассмотрим следующее определение класса:

Java
// класс службы, который мы хотим сделать транзакционным
@Transactional
public class DefaultFooService implements FooService {
    @Override
    public Foo getFoo(String fooName) {
        // ...
    }
    @Override
    public Foo getFoo(String fooName, String barName) {
        // ...
    }
    @Override
    public void insertFoo(Foo foo) {
        // ...
    }
    @Override
    public void updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
// класс службы, который мы хотим сделать транзакционным
@Transactional
class DefaultFooService : FooService {
    override fun getFoo(fooName: String): Foo {
        // ...
    }
    override fun getFoo(fooName: String, barName: String): Foo {
        // ...
    }
    override fun insertFoo(foo: Foo) {
        // ...
    }
    override fun updateFoo(foo: Foo) {
        // ...
    }
}

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

Если POJO-класс, подобный приведенному выше, определен как бин в среде Spring, можно сделать экземпляр бина транзакционным с помощью аннотации @EnableTransactionManagement в классе, аннотированном @Configuration. Подробную информацию см. в javadoc.

В XML-конфигурации тег <tx:annotation-driven/> обеспечивает аналогичный механзим:

<!-- из файла "context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- это объект-служба, который мы хотим сделать транзакционным -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <!-- активируем конфигурацию логики работы транзакций на основе аннотаций -->
    <!-- TransactionManager по-прежнему необходим -->
    <tx:annotation-driven transaction-manager="txManager"/> 
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- (эта зависимость определяется в другом месте) -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- другие определения <bean/--> -->
</beans>
  1. Строка, которая делает экземпляр бина транзакционным.
Можно опустить атрибут transaction-manager в теге <tx:annotation-driven/>, если бин TransactionManager, который вы хотите подключить, именован как transactionManager. Если бин TransactionManager, который вам нужно внедрить, имеет любое другое имя, необходимо использовать атрибут transaction-manager, как в предыдущем примере.

Реактивные транзакционные методы используют реактивные типы возврата в отличие от императивных механизмов программирования, как показано в следующем листинге:

Java
// класс реактивной службы, который мы хотим сделать транзакционным
@Transactional
public class DefaultFooService implements FooService {
    @Override
    public Publisher<Foo> getFoo(String fooName) {
        // ...
    }
    @Override
    public Mono<Foo> getFoo(String fooName, String barName) {
        // ...
    }
    @Override
    public Mono<Void> insertFoo(Foo foo) {
        // ...
    }
    @Override
    public Mono<Void> updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
// класс реактивной службы, который мы хотим сделать транзакционным
@Transactional
class DefaultFooService : FooService {
    override fun getFoo(fooName: String): Flow<Foo> {
        // ...
    }
    override fun getFoo(fooName: String, barName: String): Mono<Foo> {
        // ...
    }
    override fun insertFoo(foo: Foo): Mono<Void> {
        // ...
    }
    override fun updateFoo(foo: Foo): Mono<Void> {
        // ...
    }
}

Обратите внимание, что для возвращаемого Publisher существуют особые соображения относительно сигналов отмены Reactive Streams. Более подробно см. подраздел "Сигналы отмены" в разделе "Использование TransactionalOperator".

Видимость метода и аннотация @Transactional

Если вы используете транзакционные прокси со стандартной конфигурацией Spring, то аннотацию @Transactional нужно применять только к методам с public видимостью. Если вы пометите protected, private методы или методы с областью видимости в пределах пакета аннотацией @Transactional, ошибки не возникнет, но аннотированный метод не выдаст сконфигурированных транзакционных параметров. Если нужно аннотировать непубличные методы, ознакомьтесь с подсказкой в следующем абзаце, который описывает прокси на основе классов, или используйте связывание во время загрузки или компиляции на основе AspectJ (будет описано дальше).

При использовании аннотации @EnableTransactionManagement в классе, помеченном аннотацией @Configuration, protected методы или методы с областью видимости в пределах пакета также можно сделать транзакционными для прокси на основе класса, зарегистрировав кастомный бин transactionAttributeSource, как показано в примере далее. Обратите внимание, однако, что транзакционные методы в прокси на основе интерфейса всегда должны быть public и определены в проксированном интерфейсе.

/**
 * Зарегистрируйте кастомный AnnotationTransactionAttributeSource с использованием
 * флага publicMethodsOnly, установленным в false, чтобы активировать поддержку
 * защищенных методов и и методов с областью видимости в пределах пакета,
 * помеченные аннотацией @Transactional, в
 * прокси на основе классов.
 *
 * @see ProxyTransactionManagementConfiguration#transactionAttributeSource()
 */
@Bean
TransactionAttributeSource transactionAttributeSource() {
    return new AnnotationTransactionAttributeSource(false);
}

Spring TestContext Framework по умолчанию поддерживает незакрытые методы тестирования, аннотированные @Transactional. Примеры см. в разделе "Управление транзакциями" в главе о тестировании.

Аннотацию @Transactional можно применить к определению интерфейса, методу интерфейса, определению класса или методу класса. Однако одного лишь наличия аннотации @Transactional недостаточно для активации транзакционной логики работы. Аннотация @Transactional – это просто метаданные, которые могут быть потреблены некоторой инфраструктурой времени выполнения, которая поддерживает аннотацию @Transactional и может использовать метаданные для конфигурирования соответствующих бинов с транзакционной логикой работы. В предыдущем примере элемент <tx:annotation-driven/> переключает транзакционную логику работы.

Команда Spring рекомендует помечать аннотацией @Transactional только конкретные классы (и методы конкретных классов), а не аннотировать интерфейсы. Вы, конечно, можете пометить аннотацией @Transactional интерфейс (или метод интерфейса), но только это будет работать так, как обычно работает, если использовать прокси на основе интерфейса. Тот факт, что аннотации Java не наследуются от интерфейсов, означает, что если использовать прокси на основе класса (proxy-target-class="true") или аспект на основе связывания (mode="aspectj"), параметры транзакции не будут распознаваться инфраструктурой проксирования и связывания, а объект не будет обернут в транзакционный прокси.
В режиме прокси (который используется по умолчанию) перехватываются только внешние вызовы методов, поступающие через прокси. Это означает, что самовызов (по сути, метод внутри целевого объекта вызывает другой метод целевого объекта) не приводит к появлению фактической транзакции во время выполнения, даже если вызываемый метод помечен аннотацией @Transactional. Кроме того, прокси должен быть полностью инициализирован, чтобы обеспечить ожидаемую логику работы, поэтому не стоит полагаться на эту функцию в инициализационном коде – например, в методе, аннотированном @PostConstruct.

Рассмотрите возможность использования режима AspectJ (см. атрибут mode в следующей таблице), если ожидается, что самовызову также будут обернуты транзакциями. В этом случае прокси вообще будет далеко не на первом месте. Вместо этого целевой класс изменяется (то есть его байт-код модифицируется), чтобы начать поддерживать логику работы аннотации @Transactional во время выполнения для метода любого вида.

Таблица 2. Настройки транзакций на основе аннотаций
Атрибут XML Атрибут аннотации По умолчанию Описание

transaction-manager

Н/Д (см. javadoc по TransactionManagementConfigurer )

transactionManager

Имя используемого диспетчера транзакций. Обязателен, только если имя диспетчера транзакций не является transactionManager, как в предыдущем примере.

mode

mode

proxy

Режим по умолчанию (proxy) обрабатывает аннотированные бины для проксирования с помощью АОП-фреймворка Spring (следуя семантике прокси (которая была описана ранее), применяемой только к вызовам методов, поступающим через прокси). Альтернативный режим (aspectj) вместо этого связывает затронутые классы с аспектом транзакции на основе AspectJ из Spring, изменяя байт-код целевого класса для применения к вызову метода либого вида. Связывание на основе AspectJ требует наличия spring-aspects.jar в classpath, а также активации свзяывания во время загрузки (или связывания во время компиляции). (Подробнее о том, как настроить связывание во время загрузки, см. в разделе "Конфигурация Spring" ).

proxy-target-class

proxyTargetClass

false

Применяется только в режиме proxy. Управляет тем, какой тип транзакционных прокси создается для классов, аннотированных @Transactional. Если атрибут proxy-target-class имеет значение true, создаются прокси на основе классов. Если proxy-target-class имеет значение false или атрибут опущен, то создаются стандартные прокси на основе интерфейса JDK. (Подробное описание различных типов прокси см. в разделе "Механизмы проксирования").

order

order

Ordered.LOWEST_PRECEDENCE

Определяет порядок транзакционных Advice, которые применяются к бинам, помеченным аннотацией @Transactional. (Для получения дополнительной информации о правилах, связанных с упорядочиванием Advice АОП, см. раздел "Упорядочивание Advice"). Отсутствие заданного порядка означает, что подсистема АОП будет определять порядок Advice.

Режим снабжения Advice по умолчанию для обработки аннотаций @Transactional - proxy, что позволяет перехватывать вызовы только через прокси. Локальные вызовы в пределах одного класса перехватить таким же образом нельзя. Для более расширенного режима перехвата рассмотрите возможность перехода на режим aspectj в сочетании со связыванием во время компиляции или во время загрузки.
Атрибут proxy-target-class управляет тем, какой тип транзакционных прокси создается для классов, аннотированных @Transactional. Если proxy-target-class имеет значение true, создаются прокси на основе классов. Если proxy-target-class имеет значение false или атрибут опущен, создаются стандартные прокси на основе интерфейса JDK. (Описание различных типов прокси см. в разделе "Механизмы проксирования").
@EnableTransactionManagement и <tx:annotation-driven/> ищут @Transactional только для бинов в том же контексте приложения, в котором они определены. Это означает, что если вы поместите конфигурацию, управляемую аннотациями, в WebApplicationContext для DispatcherServlet, он будет проверять наличие бинов с аннотацией @Transactional только в ваших контроллерах, но не в ваших службах. Более подробную информацию см. в разделе, посвященному MVC.

При вычислении транзакционных параметров метода приоритет имеет наиболее производное местоположение. В следующем примере класс DefaultFooService аннотирован на уровне класса с настройками транзакции "только для чтения", но аннотация @Transactional на методе updateFoo(Foo) в том же классе имеет приоритет над настройками транзакции, определенными на уровне класса.

Java
@Transactional(readOnly = true)
public class DefaultFooService implements FooService {
    public Foo getFoo(String fooName) {
        // ...
    }
    // эти настройки приоритетнее для данного метода
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
@Transactional(readOnly = true)
class DefaultFooService : FooService {
    override fun getFoo(fooName: String): Foo {
        // ...
    }
    // эти настройки приоритетнее для данного метода
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    override fun updateFoo(foo: Foo) {
        // ...
    }
}

Параметры аннотации @Transactional

Аннотация @Transactional – это метаданные, указывающие, что интерфейс, класс или метод должен иметь транзакционную семантику (например, "при вызове этого метода начинать абсолютно новую транзакцию в режиме "только для чтения", приостанавливая все уже существующие транзакции"). Параметры @Transactional по умолчанию следующие:

  • Параметром распространения является PROPAGATION_REQUIRED.

  • Уровень изоляции - ISOLATION_DEFAULT.

  • Транзакция выполняется по принципу "чтение-запись".

  • Время ожидания транзакции по умолчанию равно времени ожидания по умолчанию основной системы транзакций, или не равно, если значения времени ожидания не поддерживаются.

  • Любые RuntimeException или Error вызывают откат, а любое проверяемое Exception – нет.

Вы можете изменить эти параметры по умолчанию. В следующей таблице приведены различные свойства аннотации @Transactional:

Параметры аннотации @Transactional
Свойство Тип Описание

value

String

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

transactionManager

String

Псевдоним для value.

label

Массив String меток для добавления выразительного описания к транзакции.

Метки могут быть вычислены диспетчерами транзакций, чтобы связать специфическую для реализации логику работы с фактической транзакцией.

propagation

enum: Propagation

Дополнительный параметр распространения.

isolation

enum: Isolation

Дополнительный уровень изоляции. Применяется только к значениям распространения REQUIRED или REQUIRES_NEW.

timeout

int (в секундах детализации)

Необязательное время ожидания транзакции. Применяется только к значениям распространения REQUIRED или REQUIRES_NEW.

timeoutString

String (в секундах детализации)

Альтернатива для задания timeout в секундах в виде String значения – например, в качестве плейсхолдера.

readOnly

boolean

Транзакция в режиме "чтение-запись" против транзакции в режиме "только для чтения". Применимо только к значениям REQUIRED или REQUIRES_NEW.

rollbackFor

Массив объектов Class, которые должны быть производными от Throwable.

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

rollbackForClassName

Массив шаблонов имен исключений.

Необязательный массив шаблонов имен исключений, которые должны вызывать откат.

noRollbackFor

Массив объектов Class, которые должны быть производными от Throwable.

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

noRollbackForClassName

Массив шаблонов имен исключений.

Необязательный массив шаблонов имен исключений, которые не должны вызывать откат.

Более подробно о семантике правил отката, шаблонах и предупреждениях о возможных непреднамеренных совпадениях смотрите в разделе "Правила отката".

В настоящее время нельзя явным образом управлять именем транзакции, где "имя" означает имя транзакции, которое отображается в мониторе транзакций, если это применимо (например, монитор транзакций WebLogic), и в журналируемых выходных данных. Для декларативных транзакций именем транзакции всегда является полностью уточненное имя класса + . + имя метода транзакционно снабженного Advice-ом класса. Например, если метод handlePayment(..) класса BusinessService запускает транзакцию, именем транзакции будет: com.example.BusinessService.handlePayment.

Использование нескольких диспетчеров транзакций при помощи аннотации @Transactional

Большинству приложений Spring нужен только один диспетчер транзакций, но могут возникнуть ситуации, когда потребуется несколько независимых диспетчеров транзакций в одном приложении. Можно использовать value или атрибут transactionManager аннотации @Transactional, чтобы опционально задать идентификатор используемого TransactionManager. Это может быть либо имя бина, либо значение квалификатора бина диспетчера транзакций. Например, используя нотацию квалификатора, можно объединить нижеприведенный код Java с нижеприведенными объявлениями бинов диспетчера транзакций в контексте приложения:

Java
public class TransactionalService {
    @Transactional("order")
    public void setSomething(String name) { ... }
    @Transactional("account")
    public void doSomething() { ... }
    @Transactional("reactive-account")
    public Mono<Void> doSomethingReactive() { ... }
}
Kotlin
class TransactionalService {
    @Transactional("order")
    fun setSomething(name: String) {
        // ...
    }
    @Transactional("account")
    fun doSomething() {
        // ...
    }
    @Transactional("reactive-account")
    fun doSomethingReactive(): Mono<Void> {
        // ...
    }
}

В следующем листинге показаны объявления бинов:

<tx:annotation-driven/>
    <bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
        <qualifier value="order"/>
    </bean>
    <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
        <qualifier value="account"/>
    </bean>
    <bean id="transactionManager3" class="org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager">
        ...
        <qualifier value="reactive-account"/>
    </bean>

В этом случае отдельные методы для TransactionalService работают под управлением отдельных диспетчеров транзакций, различающихся по квалификаторам order, account и reactive-account. Имя целевого бина по умолчанию <tx:annotation-driven>, transactionManager, все еще используется, если не удается найти специально уточненный бин TransactionManager.

Кастомные составные аннотации

Если обнаруживается, что одни и те же атрибуты с аннотацией @Transactional неоднократно используются во многих различных методах, поддержка мета-аннотаций в Spring позволит определить кастомные составные аннотации для конкретных случаев использования. В качестве примера рассмотрим следующее определение аннотации:

Java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency")
public @interface OrderTx {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "account", label = "retryable")
public @interface AccountTx {
}
Kotlin
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "order", label = ["causal-consistency"])
annotation class OrderTx
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "account", label = ["retryable"])
annotation class AccountTx

Предыдущие аннотации позволяют записать пример из предыдущего раздела следующим образом:

Java
public class TransactionalService {
    @OrderTx
    public void setSomething(String name) {
        // ...
    }
    @AccountTx
    public void doSomething() {
        // ...
    }
}
Kotlin
class TransactionalService {
    @OrderTx
    fun setSomething(name: String) {
        // ...
    }
    @AccountTx
    fun doSomething() {
        // ...
    }
}

В предыдущем примере мы использовали синтаксис для определения квалификатора диспетчера транзакций и транзакционных меток, но мы также могли бы добавить логику работы распространения, правила отката, время ожидания и другие функции.

Распространение транзакций

В этом разделе описывается определенная семантика распространения транзакций в Spring. Обратите внимание, что данный раздел не является надлежащим введением в тему распространения транзакций. Скорее, здесь подробно описана семантика распространения транзакций в Spring.

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

Understanding PROPAGATION_REQUIRED

PROPAGATION_REQUIRED обеспечивает выполнение физической транзакции либо локально для текущей области доступности, если транзакция еще не существует, либо путем присуствтия в существующей "внешней" транзакции, определенной для большей области доступности. Это вполне приемлемое значение по умолчанию при обычной организации стека вызовов в одном потоке (например, фасад службы, который делегирует полномочия нескольким методам репозитория, где все базовые ресурсы должны присутствовать в транзакции на уровне служб).

По умолчанию участвующая транзакция присоединяется к характеристикам внешней области доступности, молча игнорируя локальный уровень изоляции, значение времени ожидания или флаг "только для чтения" (если таковой имеется). Рассмотрите возможность переключения флага validateExistingTransactions в true для вашего диспетчера транзакций, если хотите, чтобы объявления уровня изоляции были недоступны при участии в существующей транзакции с другим уровнем изоляции. Этот режим также отклоняет несоответствия режима "только для чтения "(то есть если внутренняя транзакция в режиме "чтения-записи" пытается войти во внешнюю область доступности режима "только для чтения").

Если параметр распространения имеет значение PROPAGATION_REQUIRED, для каждого метода, к которому применяется этот параметр, создается область логических транзакций. Каждая такая область логических транзакций может определять статус "только откат" индивидуально, причем область внешних транзакций логически независима от области внутренних транзакций. В случае стандартной логики работы PROPAGATION_REQUIRED все эти области доступности отображаются на одну и ту же физическую транзакцию. Таким образом, маркер "только откат", установленный в области доступности внутренних транзакций, влияет на возможность внешней транзакции выполнить фиксацию.

Однако в случае, в котором область доступности внутренних транзакций устанавливает маркер "только для отката", внешняя транзакция самостоятельно не принимает решение об откате, поэтому откат (молча вызванный областью доступности внутренних транзакций) является непредвиденным. В этот момент генерируется соответствующее исключение UnexpectedRollbackException. Это ожидаемая логика работы, поэтому вызывающий транзакцию код не может сработать неверно, приняв, что фиксация была выполнена, хотя на самом деле этого не было. Таким образом, если внутренняя транзакция (о которой внешний вызывающий код не знает) молча пометит транзакцию как подлежащую откату, внешний вызывающий код все равно вызовет фиксацию. Внешний вызывающий код должен получить UnexpectedRollbackException, чтобы четко указать, что вместо этого был выполнен откат.

Основные сведения о PROPAGATION_REQUIRES_NEW

PROPAGATION_REQUIRES_NEW, в отличие от PROPAGATION_REQUIRED, всегда использует независимую физическую транзакцию для каждой затрагиваемой области доступности транзакций, никогда не участвуя в существующей транзакции, предназначенной для внешней области доступности. В такой схеме транзакции, лежащие в основе ресурса, различаются и, следовательно, могут фиксироваться или откатываться независимо друг от друга, причем на внешнюю транзакцию не влияет статус отката внутренней транзакции, а блокировки с внутренней транзакции снимаются сразу после ее завершения. Такая независимая внутренняя транзакция может также объявить свой собственный уровень изоляции, время ожидания и параметры "только чтение" и не наследовать характеристики внешней транзакции.

Основные сведения о PROPAGATION_NESTED

PROPAGATION_NESTED использует одну физическую транзакцию с несколькими точками сохранения, на которые можно откатиться. Такие частичные откаты позволяют области доступности внутренних транзакций инициировать откат для своей области доступности, при этом внешняя транзакция может продолжать физическую транзакцию, несмотря на то, что некоторые операции были откачены. Этот параметр обычно отображается на точки сохранения JDBC, поэтому работает только с транзакциями ресурсов JDBC. См. DataSourceTransactionManager из Spring.

Снабжение транзакционных операций советами

Предположим, вам нужно выполнять как транзакционные операции, так и некоторые базовые профилирующие Advice. Как это можно сделать в контексте <tx:annotation-driven/>?

Если вызывается метод updateFoo(Foo), то ожидать можно следующих действий:

  • Запускается сконфигурированный профилирующий аспект.

  • Начинает выполняться транзакционный Advice.

  • Начинает выполняться метод для снабженного Advice-ом объекта.

  • Транзакция фиксируется.

  • Профилирующий аспект сообщает точную продолжительность всего вызова транзакционного метода.

В этой главе мы особо не касаемся АОП (за исключением его применения к транзакциям). Смотрите раздел по АОП для ознакомления с подробным освещением АОП-конфигурации и АОП в целом.

В следующем коде показан простой профилирующий аспект, рассмотренный ранее:

Java
package x.y;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
import org.springframework.core.Ordered;
public class SimpleProfiler implements Ordered {
    private int order;
    // allows us to control the ordering of advice
    public int getOrder() {
        return this.order;
    }
    public void setOrder(int order) {
        this.order = order;
    }
    // этот метод - Advice "вместо"
    public Object profile(ProceedingJoinPoint call) throws Throwable {
        Object returnValue;
        StopWatch clock = new StopWatch(getClass().getName());
        try {
            clock.start(call.toShortString());
            returnValue = call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
        return returnValue;
    }
}
Kotlin
class SimpleProfiler : Ordered {
    private var order: Int = 0
    // allows us to control the ordering of advice
    override fun getOrder(): Int {
        return this.order
    }
    fun setOrder(order: Int) {
        this.order = order
    }
    // этот метод - Advice "вместо"
    fun profile(call: ProceedingJoinPoint): Any {
        var returnValue: Any
        val clock = StopWatch(javaClass.name)
        try {
            clock.start(call.toShortString())
            returnValue = call.proceed()
        } finally {
            clock.stop()
            println(clock.prettyPrint())
        }
        return returnValue
    }
}

Упорядочение Advice управляется с помощью интерфейса Ordered. Подробную информацию об упорядочивании Advice см. в разделе "Упорядочивание Advice".

Следующая конфигурация создает бин fooService, к которому в нужном порядке применяются профилирующие и транзакционные аспекты:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <!-- это аспект -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- выполняется до транзакционного Advice (поэтому порядковый номер ниже) -->
        <property name="order" value="1"/>
    </bean>
    <tx:annotation-driven transaction-manager="txManager" order="200"/>
    <aop:config>
            <!-- этот Advice выполняется вместо транзакционного Advice... -->
            <aop:aspect id="profilingAspect" ref="profiler">
                <aop:pointcut id="serviceMethodWithReturnValue"
                        expression="execution(!void x.y..*Service.*(..))"/>
                <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
            </aop:aspect>
    </aop:config>
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

Аналогичным образом можно сконфигурировать любое количество дополнительных аспектов.

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <!-- профилирующий Advice -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- выполняется до транзакционного Advice (поэтому порядковый номер ниже) -->
        <property name="order" value="1"/>
    </bean>
    <aop:config>
        <aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
        <!-- запускается после профилирующего Advice (ср. атрибут очередности) -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" order="2"/>
        <!-- значение очередности выше, чем у профилирующего аспекта -->
        <aop:aspect id="profilingAspect" ref="profiler">
            <aop:pointcut id="serviceMethodWithReturnValue"
                    expression="execution(!void x.y..*Service.*(..))"/>
            <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
        </aop:aspect>
    </aop:config>
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    <!-- другие определения <bean/>, такие как DataSource и TransactionManager  -->
</beans>

Результатом предыдущей конфигурации является бин fooService, к которому в таком порядке применяются профилирующие и транзакционные аспекты. Если нужно, чтобы профилирующий Advice выполнялся после транзакционного Adviceа на входе и перед транзакционным Advice-ом на выходе, можно изменить значение свойства order бина профилирующего аспекта таким образом, чтобы оно было выше значения очередности транзакционного Advice.

Аналогичным образом можно настроить дополнительные аспекты.

Использование аннотации @Transactional с использованием AspectJ

Вы также можете использовать средства поддержки аннотации @Transactional из Spring Framework вне контейнера Spring с помощью аспекта AspectJ. Для этого сначала пометьте свои классы (и, по желанию, методы классов) аннотацией @Transactional, а затем привяжите (свяжите) свое приложение к org.springframework.transaction.aspectj.AnnotationTransactionAspect, определенным в файле spring-aspects.jar. Вам также нужно сконфигурировать аспект с помощью диспетчера транзакций. Вы можете использовать IoC-контейнер Spring Framework, чтобы внедрить зависимости в аспект. Самый простой способ сконфигурировать аспект управления транзакциями – использовать элемент <tx:annotation-driven/> и задать атрибут mode для aspectj, как описано в разделе "Использование аннотации @Transactional". Поскольку мы фокусируемся на приложениях, которые выполняются вне контейнера Spring, то покажем, как сделать это программно.

Прежде чем продолжать, можете ознакомиться с разделами "Использование аннотации @Transactional" и "АОП" соответственно.

В следующем примере показано, как создать диспетчер транзакций и сконфигурировать AnnotationTransactionAspect на его использование:

Java
// конструируем соответствующий диспетчер транзакций
DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());
// конфигурируем AnnotationTransactionAspect на его использование; это нужно сделать перед выполнением любых транзакционных методов
AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);
Kotlin
// конструируем соответствующий диспетчер транзакций
val txManager = DataSourceTransactionManager(getDataSource())
// конфигурируем AnnotationTransactionAspect на его использование; это нужно сделать перед выполнением любых транзакционных методов
AnnotationTransactionAspect.aspectOf().transactionManager = txManager
При использовании данного аспекта необходимо аннотировать класс реализации (или методы внутри этого класса, или и то, и другое), а не интерфейс (если таковой имеется), который реализуется этим классом. AspectJ следует правилу Java касательно того, что аннотации для интерфейсов не наследуются.

Аннотация @Transactional для класса определяет семантику транзакций по умолчанию для выполнения любого публичного метода в классе.

Аннотация @Transactional для метода в классе переопределяет семантику транзакций по умолчанию, заданную аннотацией класса (если она присутствует). Вы можете аннотировать любой метод, независимо от его видимости.

Чтобы связать свои приложения с AnnotationTransactionAspect, нужно либо собрать свое приложение с помощью AspectJ (см. "Руководство по разработке на AspectJ"), либо использовать связывание во время загрузки. См. раздел "Связывание во время загрузки с помощью AspectJ в Spring Framework" для ознакомления с принципами связывания во время загрузки с помощью AspectJ.