Spring系列-事務管理

牛覓發表於2019-02-17

Spring Framework為事務管理提供了一致的抽象,具有以下優勢:

  • 跨不同事務API的一致程式設計模型,例如Java Transaction API(JTA),JDBC,Hibernate,Java Persistence API(JPA)和Java Data Objects(JDO)。
  • 支援宣告事務管理支援宣告式事務管理。
  • 與複雜的事務API(如JTA)相比,用於程式設計事務管理的API更簡單。
  • 與Spring的資料訪問抽象整合。

Spring Framework的事務模型的優點

全域性事務和本地事務

傳統上,Java EE開發人員有兩種事務管理選擇:全域性事務或本地事務,兩者都有明顯的侷限性。

  • 全域性事務:JTA、EJB 優點:可以多資源使用; 缺點:JTA API笨重、通過JNDI獲取資源。

  • 本地事務:本地事務是資源專用,比如:JDBC連線。 優點:簡單易用; 缺點:不能多資源使用。

Spring Framework的一致程式設計模型

Spring解決了全域性和本地事務的缺點。它使應用程式開發人員能夠在任何環境中使用一致的程式設計模型。Spring Framework提供了宣告式和程式設計式事務管理。大多數使用者更喜歡宣告式事務管理,在大多數情況下建議使用。大多數使用者更喜歡宣告式事務管理,在大多數情況下建議使用。

通過程式設計式事務管理,開發人員可以使用Spring Framework事務抽象,它可以在任何底層事務基礎結構上執行。使用首選的宣告式模型,開發人員通常很少或根本不編寫與事務管理相關的程式碼,因此不依賴於Spring Framework事務API或任何其他事務API。

理解Spring Framework事務抽象

Spring事務抽象的關鍵是事務策略的概念。事務策略由org.springframework.transaction.PlatformTransactionManager介面定義:

public interface PlatformTransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition)
                                                throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}
複製程式碼

getTransaction(..)方法返回TransactionStatus物件,具體取決於TransactionDefinition引數。 返回的TransactionStatus可能表示新事務,或者如果當前呼叫堆疊中存在匹配的事務,則可以表示現有事務。 後一種情況的含義是,與Java EE事務上下文一樣,TransactionStatus與執行執行緒相關聯。

TransactionDefinition

TransactionDefinition用於描述事務的隔離級別、超時時間、是否只讀事務和事務傳播規則等控制事務具體行為的事務屬性。

public interface TransactionDefinition {

    // 事務傳播
    int getPropagationBehavior();

    // 事務隔離級別
    int getIsolationLevel();

    // 事務超時事務
    int getTimeout();

    // 是否只讀
    boolean isReadOnly();

    // 事務名稱
    String getName();

}
複製程式碼

TransactionStatus

TransactionStatus介面為事務程式碼提供了一種控制事務執行和查詢事務狀態的簡單方法。這些概念應該是熟悉的,因為它們對於所有事務API都是通用的:

public interface TransactionStatus extends SavepointManager {

    boolean isNewTransaction();

    boolean hasSavepoint();

    void setRollbackOnly();

    boolean isRollbackOnly();

    void flush();

    boolean isCompleted();

}
複製程式碼

PlatformTransactionManager

無論您是在Spring中選擇宣告式還是程式化事務管理,定義正確的PlatformTransactionManager實現都是絕對必要的。 您通常通過依賴注入來定義此實現。

PlatformTransactionManager實現通常需要了解它們工作的環境:JDBC,JTA,Hibernate等。以下示例顯示瞭如何定義本地PlatformTransactionManager實現。 (此示例適用於普通JDBC。)

首先,定義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 bean定義將引用DataSource定義。

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
複製程式碼

如果在Java EE容器中使用JTA,則使用通過JNDI獲得的容器DataSource以及Spring的JtaTransactionManager。則配置如下:

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee
        http://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" />

    <!-- other <bean/> definitions here -->

</beans>
複製程式碼

Spring為不同的持久化框架提供了PlatformTransactionManager介面的實現類。如下表所示:

  • org.springframework.orm.jpa.JpaTransactionManager:使用JPA進行持久化,使用該事務管理器
  • org.springframework.orm.hibernateX.HibernateTransactionManager:使用Hibernate X.0(X可為3,4,5)版本進行持久化時,使用該事務管理器
  • org.springframework.jdbc.datasource.DataSourceTransactionManager:使用Spring JDBC或MyBatis等基於DataSource資料來源技術的持久化技術時,使用該事務管理器
  • org.springframework.orm.jdo.JdoTransactionManager:使用JDO進行持久化時,使用該事務管理器
  • org.springframework.transaction.jta.JtaTransactionManager:具有多個資料來源的全域性事務使用該事務管理器(不管採用何種持久化技術)

Synchronizing resources with transactions

本節描述直接或間接使用永續性API(如JDBC,Hibernate或JDO)的應用程式程式碼如何確保正確建立,重用和清理這些資源。

High-level synchronization approach

首選方法是使用Spring基於最高階別模板的永續性整合API,或者將ORM API與事務感知工廠bean或代理一起使用,以管理本地資源工廠。這些事務感知解決方案在內部處理資源建立和重用,清理,資源的可選事務同步以及異常對映。

因此,使用者資料訪問程式碼不必解決這些任務,但可以完全專注於非樣板永續性邏輯。 通常,您使用本機ORM API或使用模板方法通過使用JdbcTemplate進行JDBC訪問。

Low-level synchronization approach

Classes such as DataSourceUtils (for JDBC), EntityManagerFactoryUtils (for JPA), SessionFactoryUtils (for Hibernate), PersistenceManagerFactoryUtils (for JDO), and so on exist at a lower level. When you want the application code to deal directly with the resource types of the native persistence APIs, you use these classes to ensure that proper Spring Framework-managed instances are obtained, transactions are (optionally) synchronized, and exceptions that occur in the process are properly mapped to a consistent API.

For example, in the case of JDBC, instead of the traditional JDBC approach of calling the getConnection() method on the DataSource, you instead use Spring’s org.springframework.jdbc.datasource.DataSourceUtils class as follows:

Connection conn = DataSourceUtils.getConnection(dataSource);
複製程式碼

If an existing transaction already has a connection synchronized (linked) to it, that instance is returned. Otherwise, the method call triggers the creation of a new connection, which is (optionally) synchronized to any existing transaction, and made available for subsequent reuse in that same transaction. As mentioned, any SQLException is wrapped in a Spring Framework CannotGetJdbcConnectionException, one of the Spring Framework’s hierarchy of unchecked DataAccessExceptions. This approach gives you more information than can be obtained easily from the SQLException, and ensures portability across databases, even across different persistence technologies.

This approach also works without Spring transaction management (transaction synchronization is optional), so you can use it whether or not you are using Spring for transaction management.

宣告式事務管理

大多數Spring Framework使用者選擇宣告式事務管理。此選項對應用程式程式碼的影響最小,因此最符合非侵入式輕量級容器的理想。

關於Spring Framework的宣告式事務支援,最重要的概念是通過AOP代理啟用此支援,並且事務切面由後設資料(當前基於XML或基於註解)驅動。 AOP與事務後設資料的組合產生一個AOP代理,該代理使用TransactionInterceptor和適當的PlatformTransactionManager實現來驅動圍繞方法呼叫的事務。

從概念上講,在事務代理上呼叫方法如下圖所示:

calling a method on a transactional proxy

基於XML使用示例

// the service interface that we want to make transactional

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);

}
複製程式碼
// an implementation of the above interface

package x.y.service;

public class DefaultFooService implements FooService {

    public Foo getFoo(String fooName) {
        throw new UnsupportedOperationException();
    }

    public Foo getFoo(String fooName, String barName) {
        throw new UnsupportedOperationException();
    }

    public void insertFoo(Foo foo) {
        throw new UnsupportedOperationException();
    }

    public void updateFoo(Foo foo) {
        throw new UnsupportedOperationException();
    }

}
複製程式碼

假設FooService介面的前兩個方法getFoo(String)getFoo(String,String)必須在具有隻讀語義的事務的上下文中執行,並且其他方法,insertFoo(Foo)updateFoo( Foo),必須在具有讀寫語義的事務的上下文中執行。則配置檔案如下:

<!-- from the file '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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- all methods starting with 'get' are read-only -->
            <tx:method name="get*" read-only="true"/>
            <!-- other methods use the default transaction settings (see below) -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- ensure that the above transactional advice runs for any execution
        of an operation defined by the FooService interface -->
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>

    <!-- don't forget the 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>

    <!-- similarly, don't forget the PlatformTransactionManager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- other <bean/> definitions here -->

</beans>
複製程式碼

<tx:advice/>標記的transaction-manager屬性設定為將驅動事務的PlatformTransactionManager bean的名稱,在本例中為txManager bean。

<aop:config/>定義確保txAdvice bean定義的事務切面在程式中的適當位置執行。 首先,定義一個切入點,該切入點與FooService介面(fooServiceOperation)中定義的任何操作的執行相匹配。 然後使用事務切面將切入點與txAdvice相關聯。 結果表明,在執行fooServiceOperation時,將執行txAdvice定義的切面。

驗證程式碼如下:

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml", Boot.class);
        FooService fooService = (FooService) ctx.getBean("fooService");
        fooService.insertFoo (new Foo());
    }
}
複製程式碼

日誌資訊如下:

<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors

<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo

<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction

<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[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]

<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[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)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)
複製程式碼

回滾宣告式事務

向Spring Framework的事務基礎結構指示事務的工作將被回滾的推薦方法是從當前在事務上下文中執行的程式碼中丟擲異常。 Spring Framework的事務基礎結構程式碼將捕獲任何未處理的異常,因為它會使呼叫堆疊冒泡,並確定是否將事務標記為回滾。

預設配置中,Spring Framework的事務基礎結構程式碼僅在執行時未經檢查的異常情況下標記用於回滾的事務; 也就是說,丟擲的異常是RuntimeException的例項或子類。從事務方法丟擲的已檢查異常不會導致在預設配置中回滾。

您可以準確配置哪些Exception型別標記用於回滾的事務,包括已檢查的異常。以下XML程式碼段演示瞭如何為已檢查的特定於應用程式的Exception型別配置回滾。

<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的事務基礎結構中:

public void resolvePosition() {
    try {
        // some business logic...
    } catch (NoProductInStockException ex) {
        // trigger rollback programmatically
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}
複製程式碼

不同的bean配置不同的事務語義

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://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>

    <!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this bean will also be transactional, but with totally different transactional settings -->
    <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>

    <!-- other transaction infrastructure beans such as a PlatformTransactionManager omitted... -->

</beans>
複製程式碼

Using @Transactional

除了基於XML的事務配置宣告方法之外,您還可以使用基於註解的方法。

// the service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);
}
複製程式碼

當上面的POJO被定義為Spring IoC容器中的bean時,可以通過僅新增一行XML配置來使bean例項成為事務性的:

<!-- from the file '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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- enable the configuration of transactional behavior based on annotations -->
    <!-- a PlatformTransactionManager is still required -->
    <tx:annotation-driven transaction-manager="txManager"/>
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- (this dependency is defined somewhere else) -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- other <bean/> definitions here -->

</beans>
複製程式碼

如果要連線的PlatformTransactionManager的bean名稱具有名稱transactionManager,則可以省略<tx:annotation-driven />標記中的transaction-manager屬性。 如果要依賴注入的PlatformTransactionManager bean具有任何其他名稱,則必須顯式使用transaction-manager屬性,如前面的示例所示。

如果使用基於Java的配置,則@EnableTransactionManagement批註提供等效支援。 只需將註解新增到@Configuration類即可。

Method visibility and @Transactional

使用代理時,應僅將@Transactional註解應用於具有公共可見性的方法。 如果使用@Transactional註解對帶保護的,私有的或包可見的方法進行註解,則不會引發錯誤,但帶註解的方法不會顯示已配置的事務設定。 如果需要註解非公共方法,請考慮使用AspectJ。

可以在介面定義,介面上的方法,類定義或類的公共方法之前放置@Transactional註解。 但是,僅僅存在@Transactional註解不足以啟用事務行為。 @Transactional註解只是後設資料,可由@Transactional-aware的某些執行時基礎結構使用,並且可以使用後設資料來配置具有事務行為的適當bean。

如果您希望自我呼叫也包含在事務中,請考慮使用AspectJ模式。 在這種情況下,首先不會有代理; 相反,目標類將被編織(即,它的位元組程式碼將被修改),以便在任何型別的方法上將@Transactional轉換為執行時行為。

Multiple Transaction Managers with @Transactional

大多數Spring應用程式只需要一個事務管理器,但在某些情況下,您可能需要在單個應用程式中使用多個獨立的事務管理器。 @Transactional註解的value屬性可用於選擇性地指定要使用的PlatformTransactionManager的標識。

public class TransactionalService {

    @Transactional("order")
    public void setSomething(String name) { ... }

    @Transactional("account")
    public void doSomething() { ... }
}
複製程式碼
<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>
複製程式碼

如果您發現許多不同方法在@Transactional上重複使用相同的屬性,那麼Spring的元註解支援允許您為特定用例定義自定義快捷方式註解。例如,定義以下註解:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("order")
public @interface OrderTx {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
}
複製程式碼

應用自定義註解到TransactionalService,如下:

public class TransactionalService {

    @OrderTx
    public void setSomething(String name) { ... }

    @AccountTx
    public void doSomething() { ... }
}
複製程式碼

事務傳播

Spring通過事務傳播行為控制當前的事務如何傳播到被巢狀呼叫的目標服務介面方法中。TransactionDefinition介面中規定了7種型別的事務傳播行為,如下:

  • PROPAGATION_REQUIRED 如果當前沒有事務,則新建一個事務;如果已經存在一個事務,則加入到這個事務中。這是最常見的選擇。
  • PROPAGATION_SUPPORTS 支援當前事務。如果當前沒有事務,則以非事務方式執行。
  • PROPAGATION_MANDATORY 使用當前事務。如果當前沒有事務,則丟擲異常。
  • PROPAGATION_REQUIRES_NEW 新建事務。如果當前存在事務,則把當前事務掛起。
  • PROPAGATION_NOT_SUPPORTED 以非事務方式執行。如果當前存在事務,則丟擲異常。
  • PROPAGATION_NEVER 以非事務方式執行。如果當前存在事務,則丟擲異常。
  • PROPAGATION_NESTED 如果當前存在事務,則在巢狀事務內執行;如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。

在Spring管理的事務中,請注意物理和邏輯事務之間的區別,以及傳播設定如何應用於此差異。

Required

PROPAGATION_REQUIRED

PROPAGATION_REQUIRED

當傳播設定為PROPAGATION_REQUIRED時,將為應用該設定的每個方法建立邏輯事務範圍。每個這樣的邏輯事務範圍可以單獨設定僅回滾狀態,外部事務範圍在邏輯上獨立於內部事務範圍。當然,在標準PROPAGATION_REQUIRED行為的情況下,所有這些範圍將對映到同一物理事務。因此,內部事務範圍中的回滾標記確實會影響外部事務實際提交的機會。

但是,在內部事務作用域設定僅回滾標記的情況下,外部事務尚未決定回滾本身,因此回滾(由內部事務作用域靜默觸發)是意外的。此時丟擲相應的UnexpectedRollbackException。這是預期的行為,因此事務的呼叫者永遠不會被誤導,假設在實際上沒有執行提交。因此,如果內部事務(外部呼叫者不知道)以靜默方式將事務標記為僅回滾,則外部呼叫者仍會呼叫commit。外部呼叫者需要接收UnexpectedRollbackException以清楚地指示已執行回滾。

RequiresNew

PROPAGATION_REQUIRES_NEW

PROPAGATION_REQUIRED相比,PROPAGATION_REQUIRES_NEW對每個受影響的事務範圍使用完全獨立的事務。 在這種情況下,底層物理事務是不同的,因此可以獨立提交或回滾,外部事務不受內部事務的回滾狀態的影響。

Nested

PROPAGATION_NESTED使用具有多個儲存點的單個物理事務,它可以回滾到該事務。 這種部分回滾允許內部事務作用域觸發其作用域的回滾,外部事務能夠繼續物理事務,儘管已經回滾了一些操作。 此設定通常對映到JDBC儲存點,因此僅適用於JDBC資源事務。

Advising transactional operations

Suppose you want to execute both transactional and some basic profiling advice. How do you effect this in the context of <tx:annotation-driven/>?

When you invoke the updateFoo(Foo) method, you want to see the following actions:

  • Configured profiling aspect starts up.
  • Transactional advice executes.
  • Method on the advised object executes.
  • Transaction commits.
  • Profiling aspect reports exact duration of the whole transactional method invocation.

Here is the code for a simple profiling aspect discussed above. The ordering of advice is controlled through the Ordered interface.

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;
    }

    // this method is the around 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;
    }
}
複製程式碼
<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this is the aspect -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- execute before the transactional advice (hence the lower order number) -->
        <property name="order" __value="1"__/>
    </bean>

    <tx:annotation-driven transaction-manager="txManager" __order="200"__/>

    <aop:config>
            <!-- this advice will execute around the transactional 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>
複製程式碼

The following example effects the same setup as above, but uses the purely XML declarative approach.

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- the profiling advice -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- execute before the transactional advice (hence the lower order number) -->
        <property name="order" value="1__"/>
    </bean>

    <aop:config>
        <aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
        <!-- will execute after the profiling advice (c.f. the order attribute) -->

        <aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" __order="2__"/>
        <!-- order value is higher than the profiling aspect -->

        <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>

    <!-- other <bean/> definitions such as a DataSource and a PlatformTransactionManager here -->

</beans>
複製程式碼

Transaction bound event

從Spring 4.2開始,事件的監聽器可以繫結到事務的一個階段。 典型的例子是在事務成功完成時處理事件:當 當前事務的結果 對於監聽器實際上很重要時,這允許更靈活地使用事件。

註冊常規事件偵聽器是通過@EventListener註解完成的。如果需要將其繫結到事務,請使用@TransactionalEventListener。執行此操作時,預設情況下,偵聽器將繫結到事務的提交階段。

@Service
public class DefaultFooService implements FooService {

    public Foo getFoo(String fooName) {
        return jdbcTemplate.queryForObject("SELECT * FROM  foo where name = ?", 
        new Object[]{fooName}, Foo.class);
    }

    @Transactional
    public void insertFoo(Foo foo) {
        jdbcTemplate.update("INSERT INTO foo(name) VALUES (?)",
                new Object[]{foo.getName()},
                new int[]{Types.VARCHAR});

        // 如果Foo沒有繼承ApplicationEvent, 則內部會包裝為PayloadApplicationEvent。
        // 釋出事件, 事務提交後, 記錄日誌, 或傳送訊息等操作。
        applicationEventPublisher.publishEvent(foo);
        //當事務提交後, 才會真正的執行@TransactionalEventListener配置的Listener, 因此Listener拋異常, 方法返回失敗, 但事務不會回滾.
    }

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private ApplicationEventPublisher applicationEventPublisher;
}
複製程式碼
@Component
public class FooServiceListener {

    @TransactionalEventListener
    public void handler(PayloadApplicationEvent<FooService.Foo> creationEvent) {
        FooService.Foo foo =  creationEvent.getPayload();
        System.out.println("======"+foo.getName());
        System.out.println(1/0);
    }
}
複製程式碼

TransactionalEventListener註解提供了一個階段屬性,該屬性允許自定義偵聽器應繫結到的事務的哪個階段。 有效階段是BEFORE_COMMITAFTER_COMMIT(預設值),AFTER_ROLLBACK和AFTER_COMPLETION,它們聚合事務完成(無論是提交還是回滾)。 如果沒有正在執行的事務,則根本不呼叫偵聽器,因為我們無法遵守所需的語義。 但是,可以通過將註解的fallbackExecution屬性設定為true來覆蓋該行為。

參考資料


qrcode_for_gh_868a560c8305_430.jpg

相關文章