Більшість користувачів 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' з 0 спільними інтерпретаторами і 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] - створення нового transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction
<!--- з DefaultFooService генерує виняток... -->
[RuleBasedTransactionAttribute] - Використовуючи правила для визначення, де transaction повинен rollback на java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback для transaction на x.y.service.FooService.insert та транзакція відкочується (за замовчуванням екземпляри RuntimeException призводять до відкату) -->
[DataSourceTransactionManager] - Rolling back JDBC Transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection до DataSource Except .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

// інтерфейс реактивного сервісу, який ми хочемо зробити транзакційним
// the reactive service interface that we want to make transactional
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 {
        // деяка бізнес-логіка...
    } catch ( NoProductInStockException ex) {
        // запускаємо відкат програмно
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}
Kotlin

fun resolvePosition() {
    try {
        // деяка бізнес-логіка...
    } 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.

  • Транзакція виконується за принципом "читання-запис".

  • Час очікування транзакції за замовчуванням дорівнює часу очікування за замовчуванням базової системи транзакцій або відсутня, якщо значення часу очікування не підтримуються. ні.

Ти можеш змінити ці параметри за замовчуванням. У наступній таблиці наведено різні атрибути тегів <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 робить декларації набагато ближче до коду, що згадується. Небезпека надмірної зв'язаності невелика, оскільки код, призначений для транзакційного використання, майже завжди розгортається саме таким чином. Також підтримується як заміна власної анотації 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, який nи хочеi підключити, іменований як transactionManager. Якщо бін TransactionManager, який тобі потрібно впровадити, має будь-яке інше ім'я, необхідно використовувати атрибут transaction-manager, як у попередньому прикладі.

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

title">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 за умовчанням для обробки анотацій @Transactionalproxy, що дозволяє перехоплювати дзвінки лише через проксі. Локальні виклики в межах одного класу перехопити так само не можна. Для більш розширеного режиму перехоплення розглянь можливість переходу на режим 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-ом об'єкта.

  • Транзакція фіксується.

  • Профілюючий аспект повідомляє точну тривалість всього виклику транзакційного методу. У цьому розділі ми особливо не торкаємося АОП (за винятком його застосування до транзакцій). Дивіться розділ АОП для ознайомлення з детальним освітленням АОП-конфігурації та АОП в цілому.

У наступному коді показаний простий профільуючий аспект, розглянутий раніше:


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;
    // дозволяється контролювати відповідність громадського
    public int getOrder() {
        return this.order;
    }
    public void setOrder(int order) {
        this.order = order;
    }
    // Цей мето - Advice "instead of"
    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
    // дозволяється контролювати відповідність громадського
    override fun getOrder(): Int {
        return this.order
    }
    fun setOrder(order: Int) {
        this.order = order
    }
    // Цей мето - Advice "instead of"
    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, то покажемо, як зробити це програмно.

У наступному прикладі показано, як створити диспетчер транзакцій та налаштувати 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.