從源碼分析 Spring 基於注解的事務


在spring引入基於注解的事務(@Transactional)之前,我們一般都是如下這樣進行攔截事務的配置:

    <!-- 攔截器方式配置事務 -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED" />
            <tx:method name="append*" propagation="REQUIRED" />
            <tx:method name="insert*" propagation="REQUIRED" />
            <tx:method name="save*" propagation="REQUIRED" />
            <tx:method name="update*" propagation="REQUIRED" />
            <tx:method name="modify*" propagation="REQUIRED" />
            <tx:method name="edit*" propagation="REQUIRED" />
            <tx:method name="delete*" propagation="REQUIRED" />
            <tx:method name="remove*" propagation="REQUIRED" />
            <tx:method name="repair" propagation="REQUIRED" />
            <tx:method name="delAndRepair" propagation="REQUIRED" />

            <tx:method name="get*" propagation="SUPPORTS" />
            <tx:method name="find*" propagation="SUPPORTS" />
            <tx:method name="load*" propagation="SUPPORTS" />
            <tx:method name="search*" propagation="SUPPORTS" />
            <tx:method name="datagrid*" propagation="SUPPORTS" />

            <tx:method name="*" propagation="SUPPORTS" />
        </tx:attributes>
    </tx:advice>
    <aop:config>
        <aop:pointcut id="transactionPointcut" expression="execution(* net.aazj.service..*Impl.*(..))" />
        <aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
    </aop:config>

這種方式明顯的缺點是,不太容易理解,並且限定了service層的方法名稱的前綴,沒有模板的話寫起來也很難,很容易寫錯。

因此在spring中引入了基於注解的事務配置方法之后,我們應該拋棄這種事務配置方法了。基於注解 @Transactional 的事務配置具有簡單,靈活的優點。下面看一個例子:

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper userMapper;
    
    @Transactional (propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,readOnly=true)
    public User getUser(int userId) {
        return userMapper.getUser(userId);
    }
    
    @Transactional
    public void addUser(String username){
        userMapper.addUser(username);
//      int i = 1/0;    // 測試事務的回滾
    }
    
    @Transactional (rollbackFor = BaseBusinessException.class)
    public void addAndDeleteUser(String username, int id) throws BaseBusinessException{
        userMapper.addUser(username);        
        this.m1();        
        userMapper.deleteUserById(id);
    }
    
    private void m1() throws BaseBusinessException {
        throw new BaseBusinessException("xxx");
    }
}

首先在service類上聲明了@Transactional,表明類中的所有方法都需要運行在事務中,然后在方法中可以指定具體的事務特性,方法中的@Transactional會覆蓋類上的@Transactional。

下面我們從源碼的角度(從源碼的學習可以給我們實打實的比較深入理解,而且不會出錯,二手資料總是會有時延的)來探究一下它們:

public @interface Transactional {

    /**
     * A qualifier value for the specified transaction.
     * <p>May be used to determine the target transaction manager,
     * matching the qualifier value (or the bean name) of a specific
     * {@link org.springframework.transaction.PlatformTransactionManager}
     * bean definition.
     */
    String value() default "";

    /**
     * The transaction propagation type.
     * Defaults to {@link Propagation#REQUIRED}.
     * @see org.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior()
     */
    Propagation propagation() default Propagation.REQUIRED;

    /**
     * The transaction isolation level.
     * Defaults to {@link Isolation#DEFAULT}.
     * @see org.springframework.transaction.interceptor.TransactionAttribute#getIsolationLevel()
     */
    Isolation isolation() default Isolation.DEFAULT;

    /**
     * The timeout for this transaction.
     * Defaults to the default timeout of the underlying transaction system.
     * @see org.springframework.transaction.interceptor.TransactionAttribute#getTimeout()
     */
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    /**
     * {@code true} if the transaction is read-only.
     * Defaults to {@code false}.
     * <p>This just serves as a hint for the actual transaction subsystem;
     * it will <i>not necessarily</i> cause failure of write access attempts.
     * A transaction manager which cannot interpret the read-only hint will
     * <i>not</i> throw an exception when asked for a read-only transaction.
     * @see org.springframework.transaction.interceptor.TransactionAttribute#isReadOnly()
     */
    boolean readOnly() default false;

    /**
     * Defines zero (0) or more exception {@link Class classes}, which must be a
     * subclass of {@link Throwable}, indicating which exception types must cause
     * a transaction rollback.
     * <p>This is the preferred way to construct a rollback rule, matching the
     * exception class and subclasses.
     * <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class clazz)}
     */
    Class<? extends Throwable>[] rollbackFor() default {};

    /**
     * Defines zero (0) or more exception names (for exceptions which must be a
     * subclass of {@link Throwable}), indicating which exception types must cause
     * a transaction rollback.
     * <p>This can be a substring, with no wildcard support at present.
     * A value of "ServletException" would match
     * {@link javax.servlet.ServletException} and subclasses, for example.
     * <p><b>NB: </b>Consider carefully how specific the pattern is, and whether
     * to include package information (which isn't mandatory). For example,
     * "Exception" will match nearly anything, and will probably hide other rules.
     * "java.lang.Exception" would be correct if "Exception" was meant to define
     * a rule for all checked exceptions. With more unusual {@link Exception}
     * names such as "BaseBusinessException" there is no need to use a FQN.
     * <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(String exceptionName)}
     */
    String[] rollbackForClassName() default {};

    /**
     * Defines zero (0) or more exception {@link Class Classes}, which must be a
     * subclass of {@link Throwable}, indicating which exception types must <b>not</b>
     * cause a transaction rollback.
     * <p>This is the preferred way to construct a rollback rule, matching the
     * exception class and subclasses.
     * <p>Similar to {@link org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(Class clazz)}
     */
    Class<? extends Throwable>[] noRollbackFor() default {};

    /**
     * Defines zero (0) or more exception names (for exceptions which must be a
     * subclass of {@link Throwable}) indicating which exception types must <b>not</b>
     * cause a transaction rollback.
     * <p>See the description of {@link #rollbackForClassName()} for more info on how
     * the specified names are treated.
     * <p>Similar to {@link org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(String exceptionName)}
     */
    String[] noRollbackForClassName() default {};

}
public @interface Transactional

注解@Transactional的屬性有:propagation, isolation, timeout, readOnly, rollbackFor, rollbackForClassName, noRollbackFor, noRollbackForClassName

propagation, isolation, timeout, readOnly都有默認值,而rollbackFor, rollbackForClassName, noRollbackFor, noRollbackForClassName默認值都是空的。

我們具體看下我們可能會用到的屬性:propagation, isolation, readOnly, rollbackFor

1)propagation指定事務的傳播屬性

public enum Propagation {

    /**
     * Support a current transaction, create a new one if none exists.
     * Analogous to EJB transaction attribute of the same name.
     * <p>This is the default setting of a transaction annotation.
     */
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

    /**
     * Support a current transaction, execute non-transactionally if none exists.
     * Analogous to EJB transaction attribute of the same name.
     * <p>Note: For transaction managers with transaction synchronization,
     * PROPAGATION_SUPPORTS is slightly different from no transaction at all,
     * as it defines a transaction scope that synchronization will apply for.
     * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
     * will be shared for the entire specified scope. Note that this depends on
     * the actual synchronization configuration of the transaction manager.
     * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
     */
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

    /**
     * Support a current transaction, throw an exception if none exists.
     * Analogous to EJB transaction attribute of the same name.
     */
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

    /**
     * Create a new transaction, and suspend the current transaction if one exists.
     * Analogous to the EJB transaction attribute of the same name.
     * <p>Note: Actual transaction suspension will not work out-of-the-box on
     * all transaction managers. This in particular applies to JtaTransactionManager,
     * which requires the {@code javax.transaction.TransactionManager} to be
     * made available it to it (which is server-specific in standard J2EE).
     * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
     */
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

    /**
     * Execute non-transactionally, suspend the current transaction if one exists.
     * Analogous to EJB transaction attribute of the same name.
     * <p>Note: Actual transaction suspension will not work on out-of-the-box
     * on all transaction managers. This in particular applies to JtaTransactionManager,
     * which requires the {@code javax.transaction.TransactionManager} to be
     * made available it to it (which is server-specific in standard J2EE).
     * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
     */
    NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

    /**
     * Execute non-transactionally, throw an exception if a transaction exists.
     * Analogous to EJB transaction attribute of the same name.
     */
    NEVER(TransactionDefinition.PROPAGATION_NEVER),

    /**
     * Execute within a nested transaction if a current transaction exists,
     * behave like PROPAGATION_REQUIRED else. There is no analogous feature in EJB.
     * <p>Note: Actual creation of a nested transaction will only work on specific
     * transaction managers. Out of the box, this only applies to the JDBC
     * DataSourceTransactionManager when working on a JDBC 3.0 driver.
     * Some JTA providers might support nested transactions as well.
     * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
     */
    NESTED(TransactionDefinition.PROPAGATION_NESTED);


    private final int value;


    Propagation(int value) { this.value = value; }

    public int value() { return this.value; }

}
public enum Propagation

propagation可以取如下的值:REQUIRED, SUPPORTS, MANDATORY, REQUIRES_NEW, NOT_SUPPORTED, NEVER, NESTED

我們一般會只用:REQUIRED(默認值), SUPPORTS,其它的取值基本上不使用(具體可以參考上面源碼中的注釋,已經很詳細了)。

propagation=Propagation.REQUIRED:表示該方法或類必須要事務的支持,如果已經是在一個事務中被調用,那么就使用該事務,如果沒有在一個事務中,那么就新建一個事務。

propagation=Propagation.SUPPORTS:表示該方法或類支持事務,如果已經是在一個事務中被調用,那么就使用該事務,如果沒有在一個事務中,也可以。

2)isolation指定事務的隔離級別

public enum Isolation {

    /**
     * Use the default isolation level of the underlying datastore.
     * All other levels correspond to the JDBC isolation levels.
     * @see java.sql.Connection
     */
    DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

    /**
     * A constant indicating that dirty reads, non-repeatable reads and phantom reads
     * can occur. This level allows a row changed by one transaction to be read by
     * another transaction before any changes in that row have been committed
     * (a "dirty read"). If any of the changes are rolled back, the second
     * transaction will have retrieved an invalid row.
     * @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED
     */
    READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

    /**
     * A constant indicating that dirty reads are prevented; non-repeatable reads
     * and phantom reads can occur. This level only prohibits a transaction
     * from reading a row with uncommitted changes in it.
     * @see java.sql.Connection#TRANSACTION_READ_COMMITTED
     */
    READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

    /**
     * A constant indicating that dirty reads and non-repeatable reads are
     * prevented; phantom reads can occur. This level prohibits a transaction
     * from reading a row with uncommitted changes in it, and it also prohibits
     * the situation where one transaction reads a row, a second transaction
     * alters the row, and the first transaction rereads the row, getting
     * different values the second time (a "non-repeatable read").
     * @see java.sql.Connection#TRANSACTION_REPEATABLE_READ
     */
    REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

    /**
     * A constant indicating that dirty reads, non-repeatable reads and phantom
     * reads are prevented. This level includes the prohibitions in
     * {@code ISOLATION_REPEATABLE_READ} and further prohibits the situation
     * where one transaction reads all rows that satisfy a {@code WHERE}
     * condition, a second transaction inserts a row that satisfies that
     * {@code WHERE} condition, and the first transaction rereads for the
     * same condition, retrieving the additional "phantom" row in the second read.
     * @see java.sql.Connection#TRANSACTION_SERIALIZABLE
     */
    SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);


    private final int value;


    Isolation(int value) { this.value = value; }

    public int value() { return this.value; }

}
public enum Isolation

isolation可以取如下的值:DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE

我們一般只使用:DEFAULT(默認值),因為我們一般是直接在數據庫的層面上來設置事務的隔離級別,很少會在應用層來設置隔離基本。

isolation=Isolation.DEFAULT:表示使用下層數據庫指定的隔離級別

3)readOnly為只讀,利於數據庫優化器進行優化。默認值為 false。所以對於只對數據庫進行讀取的方法,我們可以如下指定:

    @Transactional (propagation=Propagation.SUPPORTS)
    public User getUser(int userId) {
        return userMapper.getUser(userId);
    }

表示:有事務則使用當前事務,沒有事務則不使用事務。最大限度的利於數據庫的優化器進行優化。

如果一定要使用事務的話,也可以這樣使用readOnly=true來優化:

    @Transactional (propagation=Propagation.REQUIRED,readOnly=true)
    public User getUser(int userId) {
        return userMapper.getUser(userId);
    }

這就是 readOnly 的作用。表示有事務則使用當前事務,如果沒有事務,則新建一個只讀事務。其實 上面的 propagation=Propagation.REQUIRED也是可以去掉的,因為他是默認值

    @Transactional (readOnly=true)
    public User getUser(int userId) {
        return userMapper.getUser(userId);
    }

表示強制事務,並且是 只讀事務。readOnly 要注意有點的是,readOnly只有在 有事務時,才會生效,如果沒有事務,那么 readOnly 是會被忽略的。比如:

@Transactional(propagation=Propagation.SUPPORTS, readonly=true)

表示的就是 沒有事務 也是可以的,有事務的話,就一定是 只讀事務。

4)rollbackFor,其實該屬性也很少使用,而且經常被誤用。表示拋出什么異常時,會回滾事務。異常分為受檢異常(必須進行處理或者重新拋出)和非受檢異常(可以不進行處理)。在遇到非受檢異常時,事務是一定會進行回滾的。rollbackFor用於指定對於何種受檢異常發生時,進行回滾。因為受檢異常,我們必須進行處理或者重新拋出,所以只有一種情況下我們要使用rollbackFor來指定,就是我們不處理異常,直接拋出受檢異常,並且我們需要方法在拋出該異常時,進行回滾,如上面的例子:

    @Transactional (rollbackFor = BaseBusinessException.class)
    public void addAndDeleteUser(String username, int id) throws BaseBusinessException{
        userMapper.addUser(username);
        this.m1();
        userMapper.deleteUserById(id);
    }
    private void m1() throws BaseBusinessException {
        throw new BaseBusinessException("xxx");
    }

因為 m1 方法拋出受檢異常,我們在 addAndDeleteUser 方法中不對該異常進行處理,而是直接拋出,如果我們希望 userMapper.addUser(username) 和 userMapper.deleteUserById(id) 要么都成功,要么都失敗,此時我們則應該指定:rollbackFor = BaseBusinessException.class ,進行回滾。

所以只有我們的方法聲明要拋出一個受檢異常時,我們才應該使用 rollbackFor 屬性來進行處理。如果我們在 addAndDeleteUser  方法中對 m1 方法的受檢異常進行了處理,那么就沒有必要使用 rollbackFor 了:

    public void addAndDeleteUser(String username, int id){
        userMapper.addUser(username);
        try{
            this.m1();
        }catch(BaseBusinessException e){
            // 處理異常,比如記錄進日志文件等
        }
        userMapper.deleteUserById(id);
    }

因為我們處理了 m1 方法的異常,那么就不會有受檢異常導致 userMapper.addUser(username) 和 userMapper.deleteUserById(id) 這兩個方法一個執行成功,一個沒有執行。而非受檢異常默認就會回滾。

受檢異常是必須進行處理或者重新聲明拋出的。只有聲明重新拋出受檢異常時,才會需要使用 rollbackFor 屬性。所以下面的方式就屬於濫用 rollbackFor 了:

    @Transactional (rollbackFor=Exception.class)
    public void addAndDeleteUser2(String username, int id){
        userMapper.addUser(username);
        userMapper.deleteUserById(id);
    }

因為:受檢異常是必須進行處理或者重新聲明拋出,而我們既沒有進行處理,也沒有重新拋出,就說明他絕對不可能會拋出受檢異常了。而只會拋出未受檢異常,而未受檢異常,默認就會回滾,所以上面的 @Transactional (rollbackFor=Exception.class) 完全是多余的。

總結

1)@Transactional 的默認值為:@Transactional (propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,readOnly=false,

timeout=TransactionDefinition.TIMEOUT_DEFAULT),默認值已經適合絕大多數情況,所以我們一般使用 @Transactional 進行注解就夠了。

2)只有當默認值不符合我們的需要時才給@Transactional的屬性指定值,一般也就指定:propagation=Propagation.SUPPORTS 和 readOnly=true,其它的屬性和值一般很少使用非默認值。所以我們前面的UserServiceImpl類可以重構如下:

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper userMapper;
    
    @Transactional (readOnly=true)
    public User getUser(int userId) {
        return userMapper.getUser(userId);
    }
    
    public void addUser(String username){
        userMapper.addUser(username);
        int i = 1/0;    // 測試事務的回滾
    }
    
    public void deleteUser(int id){
        userMapper.deleteUserById(id);
//      int i = 1/0;    // 測試事務的回滾
    }
    
    @Transactional (rollbackFor = BaseBusinessException.class)
    public void addAndDeleteUser(String username, int id) throws BaseBusinessException{
        userMapper.addUser(username);
        this.m1();
        userMapper.deleteUserById(id);
    }
    
    private void m1() throws BaseBusinessException {
        throw new BaseBusinessException("xxx");
    }
}

addUser 和 deleteUser 因為會繼承上的 @Transactional ,所以無需另外指定了,只有當類上指定的 @Transactional 不適合時,才需要另外在方法上進行指定。

3)所以我們只實際情況中,我們只需要使用下面三者來進行注解事務的配置:

@Transactional,

@Transactional (readOnly=true),

@Transactional (propagation=Propagation.SUPPORTS, readOnly=true),

@Transactional (rollbackFor = xxException.class),其它都可以保持默認值,其它的非默認值極少使用。

 

另外

要使用@Transactional來進行注解事務配置,必須要在spring的配置文件中加入下面的配置說明,啟用基於注解的配置:

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    
    <!-- 使用annotation定義事務 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

補充說明

      也許你會覺得奇怪,userMapper.getUser(userId); 這些涉及到數據庫訪問的方法,為什么不會拋出 SQLException 受檢異常呢?如果手動注冊驅動,然后獲取鏈接,進行數據庫操作時,是會有許多的受檢異常要處理的:

        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            // ...
        }
        String url="jdbc:mysql://localhost:3306/databasename";
         try {
            Connection conn = (Connection) DriverManager.getConnection(url,"username","password");
        } catch (SQLException e) {
            // ...
        } 

哪為什么在這里userMapper.getUser(userId); 就不需要處理受檢異常呢?其實這是spring的功能,spring為了將我們從那些十分麻煩的受檢異常比如SQLException中解救處理,將所有的數據庫訪問層的受檢異常轉嫁到 Spring 的 非受檢異常RuntimeException 體系中來——DataAccessException

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM