Ключом к пониманию абстракции транзакций Spring является понятие транзакционной стратегии. Транзакционная стратегия определяется TransactionManager, в частности интерфейсом org.springframework.transaction.PlatformTransactionManager – для управления императивными транзакциями, а также интерфейсом org.springframework.transaction.ReactiveTransactionManager – для управления реактивными транзакциями. В следующем листинге показано определение API-интерфейса PlatformTransactionManager:

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}

Это в первую очередь интерфейс поставщика услуг (SPI), хотя его можно использовать программно из кода приложения. Поскольку PlatformTransactionManager является интерфейсом, его можно легко реализовать в качестве объекта-имитации или функции-заглушки по мере необходимости. Он не привязан к стратегии поиска, такой как JNDI. Реализации PlatformTransactionManager определяются как любой другой объект (или бин) в IoC-контейнере Spring Framework. Одно это преимущество уже делает транзакции из Spring Framework достойной абстракцией, даже если вы работаете с JTA. Тестировать транзакционный код становится гораздо проще, чем если бы он использовал JTA напрямую.

Опять же, в соответствии с философией Spring, исключение TransactionException которое может быть сгенерировано любым из методов интерфейса PlatformTransactionManager, является непроверяемым (то есть расширяет класс java.lang.RuntimeException). Сбои инфраструктуры транзакций почти всегда критичные. В редких случаях, когда код приложения действительно может восстановиться после сбоя транзакции, разработчик приложения все же может осуществлять перехват и обработку TransactionException. Важным моментом является то, что разработчиков могут делать это без принуждения.

Метод getTransaction(..) возвращает объект TransactionStatus в зависимости от параметра TransactionDefinition. Возвращаемый TransactionStatus может представлять новую транзакцию или может представлять существующую транзакцию, если соответствующая транзакция существует в текущем стеке вызовов. В последнем случае, как и в случае с транзакционными контекстами Java EE, TransactionStatus ассоциируется с потоком выполнения.

Начиная с версии Spring Framework 5.2, Spring также предоставляет абстракцию управления транзакциями для реактивных приложений, использующих реактивные типы или сопрограммы Kotlin. В следующем листинге показана транзакционная стратегия, определенная org.springframework.transaction.ReactiveTransactionManager:

public interface ReactiveTransactionManager extends TransactionManager {
    Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;
    Mono<Void> commit(ReactiveTransaction status) throws TransactionException;
    Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}

Интерфейс TransactionDefinition определяет:

  • Распространение: Как правило, весь код в области доступности транзакции выполняется в этой транзакции. Однако можно задать логику работы в случаях, если транзакционный метод запускается, когда транзакционный контекст уже существует. Например, выполнение кода может продолжиться в существующей транзакции (обычный случай) или же существующая транзакция может быть приостановлена и создана новая транзакция. Spring предлагает любые варианты распространения транзакций, знакомые по CMT из EJB. Для ознакомления с семантикой распространения транзакций в Spring, см. раздел "Распространение транзакций".

  • Изоляцию: Степень, в которой данная транзакция изолирована от работы других транзакций. Например, может ли эта транзакция видеть незафиксированные записи от других транзакций?

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

  • Статус "только для чтения": Можно использовать транзакцию в режиме "только для чтения", при котором ваш код будет читать, но не сможет изменять данные. Транзакции "только для чтения" могут быть полезны в контексте оптимизации в некоторых случаях, например, если вы используете Hibernate.

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

Интерфейс TransactionStatus позволяет транзакционному коду без всяких препятствий контролировать выполнение транзакции и запрашивать статус транзакции. Концепции должны быть знакомы, поскольку они являются общими для всех API-интерфейсов транзакций. В следующем листинге показан интерфейс TransactionStatus:

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    @Override
    boolean isNewTransaction();
    boolean hasSavepoint();
    @Override
    void setRollbackOnly();
    @Override
    boolean isRollbackOnly();
    void flush();
    @Override
    boolean isCompleted();
}

Независимо от того, какой способ управления транзакциями в Spring вы выбрали – декларативный или программный – определение правильной реализации TransactionManager является абсолютно необходимым. Обычно эта реализация определяется через внедрение зависимостей.

Реализации TransactionManager обычно требуют знания окружения, в котором они работают: JDBC, JTA, Hibernate и так далее. В следующих примерах показано, как можно определить локальную реализацию PlatformTransactionManager (в данном случае с помощью обычного JDBC).

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

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>

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

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

Если вы используете JTA в контейнере Java EE, то, значит, заействуете контейнерный DataSource, полученный через JNDI, в сочетании с JtaTransactionManager из Spring. В следующем примере показано, как будет выглядеть версия поиска через JTA и JNDI:

<?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:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee
        https://www.springframework.org/schema/jee/spring-jee.xsd">
    <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>
    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />
    <!-- другие определения <bean/--> -->
</beans>

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

В предыдущем определении бина dataSource используется тег <jndi-lookup/> из пространства имен jee. Для получения дополнительной информации см. раздел "Схема JEE".
Если вы используете JTA, определение диспетчера транзакций должно выглядеть одинаково, независимо от того, какая технология доступа к данным используется, будь то JDBC, Hibernate через JPA или любая другая поддерживаемая технология. Это связано с тем, что транзакции JTA являются глобальными транзакциями, которые могут задействовать любой транзакционный ресурс.

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

Настройка транзакций Hibernate

Также можно с легкостью использовать локальные транзакции из Hibernate, как будет показано в следующих примерах. В этом случае необходимо определить LocalSessionFactoryBean для Hibernate, который код вашего приложения может использовать для получения экземпляров Session из Hibernate.

Определение бина DataSource похоже на пример локального JDBC, продемонстрированный ранее, и поэтому не показано в следующем примере.

Если поиск источника данных DataSource (используемого любым не-JTA диспетчером транзакций) осуществляется через JNDI, а управление происходит посредством Java EE, он должен быть нетранзакционным, поскольку Spring Framework (а не контейнер Java EE) управляет транзакциями.

В данном случае бин txManager имеет тип HibernateTransactionManager. Точно так же, как DataSourceTransactionManager необходима ссылка на DataSource, HibernateTransactionManager необходима ссылка на SessionFactory. В следующем примере объявлены бины sessionFactory и txManager:

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
        </value>
    </property>
</bean>
<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

Если вы используете Hibernate и управляемые контейнером Java EE транзакции JTA, следует использовать тот же JtaTransactionManager, что и в предыдущем примере с JTA для JDBC, как это показано в следующем примере. Также рекомендуется сделать так, чтобы Hibernate узнавал о JTA через свой координатор транзакций и, возможно, конфигурацию режима освобождения соединения:

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
            hibernate.transaction.coordinator_class=jta
            hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
        </value>
    </property>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

Или же можно передать JtaTransactionManager в ваш LocalSessionFactoryBean для обеспечения тех же значений по умолчанию:

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
        </value>
    </property>
    <property name="jtaTransactionManager" ref="txManager"/>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>