基於注解的聲明式事務的使用方法


使用方法

1. 添加相關jar包或依賴——數據源、數據庫驅動、mysql或spring-jdbc等,這里以spring-jdbc為例;

2. 數據庫連接參數,一般單獨寫在properties或yaml配置文件中;

3. 編寫數據庫訪問層組件(dao)和業務邏輯層組件(service),且在service層需要事務管理的方法上加@Transactional注解;

4. 在容器中注冊數據源、數據庫操作模板、事務管理器,以及步驟3中的組件;

5. 在容器中開啟基於事務的注解,即在容器配置類上加@EnableTransactionManagement注解。

 

下面是一個使用spring-jdbc訪問數據庫的事務demo

pom文件

<!--spring數據庫操作模板-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.1.16.RELEASE</version>
</dependency>
<!--數據源,由於只是測試事務的demo,用的c3p0-->
<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.4</version>
</dependency>
<!--數據庫驅動-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>

 

數據庫配置文件

jdbc.url=jdbc:mysql://127.0.0.1:3306/test
jdbc.username=root
jdbc.password=123456
jdbc.driverClass=com.mysql.jdbc.Driver

 

容器配置類

/**
 * 容器配置類
 * @EnableTransactionalManagement注解的意義是開啟基於注解的事務,即使方法上的@Transactional注解生效
*/ @Configuration @ComponentScan(basePackages = {"cn.monolog.service", "cn.monolog.dao"}) @PropertySource(value = "classpath:/datasource.properties") @EnableTransactionManagement public class TxBeanConfig { //從配置文件中注入屬性 @Value(value = "${jdbc.username}") private String username; @Value(value = "${jdbc.password}") private String password; @Value(value = "${jdbc.driverClass}") private String driverClass; @Value(value = "${jdbc.url}") private String url; //注冊數據源 @Bean(name = "dataSource") public DataSource dataSource() throws PropertyVetoException { //創建數據源實例 ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(); //屬性注入 comboPooledDataSource.setUser(this.username); comboPooledDataSource.setPassword(this.password); comboPooledDataSource.setDriverClass(this.driverClass); comboPooledDataSource.setJdbcUrl(this.url); //返回 return comboPooledDataSource; } //注冊spring的數據庫操作模板 @Bean(name = "jdbcTemplate") public JdbcTemplate jdbcTemplate() throws PropertyVetoException { //從容器中獲取數據源 // --注:spring對配置類(即加了@Configuration的類)有特殊處理, // 當多次調用注冊方法時,並不是每次都會創建新bean,而是會從容器中獲取bean DataSource dataSource = this.dataSource(); //創建模板 JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); //返回模板 return jdbcTemplate; } //注冊事務管理器 @Bean(name = "transactionManager") public PlatformTransactionManager transactionManager() throws PropertyVetoException { //從容器中獲取數據源 DataSource dataSource = this.dataSource(); //創建事務管理器實例 DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource); //返回 return dataSourceTransactionManager; } }

 

數據庫訪問層組件

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
/**
 * 數據庫訪問層組件
 * 用於測試事務
 */
@Repository
public class VariationDao {

    //從容器中自動注入spring數據庫操作模板
    @Autowired
    @Qualifier("jdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    /**
     * 創建一條數據
     */
    public void create(String staffId, Integer alloted) {
        //編寫sql語句
        String sql = "insert into c_variation_config (staff_id, alloted) values (?, ?)";
        //執行sql語句
        this.jdbcTemplate.update(sql, staffId, alloted);
    }
}

 

業務邏輯層組件

import cn.monolog.dao.VariationDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 業務邏輯層組件
 * 用於測試事務
 */
@Service
public class VariationService {

    //從容器中自動注入dao層組件
    @Autowired
    @Qualifier("variationDao")
    private VariationDao variationDao;

    /**
     * 創建一條數據
     */
    @Transactional(rollback =Exception.class)
    public void create(String staffId, Integer alloted) {
        //執行
        this.variationDao.create(staffId, alloted);
        //手動制造異常,由於加了事務,上一條語句產生的數據庫更新會被回滾
        int i = 10 / 0;
    }
}

 

注意事項

1. 在上述demo注冊事務管理器時,需要獲取容器中的另一個組件——數據源,這種從一個注冊方法中需要獲取另一個組件的情況,有兩種處理方案

① 在參數列表中接收

@Configuration
@PropertySource(value = "classpath:/datasource.properties")
@EnableTransactionManagement
public class TxBeanConfig {

    //從配置文件中注入屬性
    @Value(value = "${jdbc.username}")
    private String username;
    @Value(value = "${jdbc.password}")
    private String password;
    @Value(value = "${jdbc.driverClass}")
    private String driverClass;
    @Value(value = "${jdbc.url}")
    private String url;

    //注冊數據源
    @Bean(name = "dataSource")
    public DataSource dataSource() throws PropertyVetoException {
        //創建數據源實例
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();

        //屬性注入
        comboPooledDataSource.setUser(this.username);
        comboPooledDataSource.setPassword(this.password);
        comboPooledDataSource.setDriverClass(this.driverClass);
        comboPooledDataSource.setJdbcUrl(this.url);

        //返回
        return comboPooledDataSource;
    }

    /**
     * 注冊事務管理器
     * @param dataSource 在參數列表中接收容器中的另一個組件
     * @return
     * @throws PropertyVetoException
     */
    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(DataSource dataSource) throws PropertyVetoException {
        //創建事務管理器實例
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
        //返回
        return  dataSourceTransactionManager;
    }
}

② 在方法體中調用另一個注冊方法

@Configuration
@ComponentScan(basePackages = {"cn.monolog.service", "cn.monolog.dao"})
@PropertySource(value = "classpath:/datasource.properties")
@EnableTransactionManagement
public class TxBeanConfig {

    //從配置文件中注入屬性
    @Value(value = "${jdbc.username}")
    private String username;
    @Value(value = "${jdbc.password}")
    private String password;
    @Value(value = "${jdbc.driverClass}")
    private String driverClass;
    @Value(value = "${jdbc.url}")
    private String url;

    //注冊數據源
    @Bean(name = "dataSource")
    public DataSource dataSource() throws PropertyVetoException {
        //創建數據源實例
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();

        //屬性注入
        comboPooledDataSource.setUser(this.username);
        comboPooledDataSource.setPassword(this.password);
        comboPooledDataSource.setDriverClass(this.driverClass);
        comboPooledDataSource.setJdbcUrl(this.url);

        //返回
        return comboPooledDataSource;
    }
    
    
    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager() throws PropertyVetoException {
        //獲取數據源
        DataSource dataSource = this.dataSource(); //創建事務管理器實例
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
        //返回
        return  dataSourceTransactionManager;
    }
}

對於這種方式,我們可能會產生一種疑問:this.dataSource方法中會new一個數據源實例,那么在注冊事務管理器的同時,會不會又new了一個數據源實例,與單例模式違背?

答案是不會。因為spring對於容器配置類(即加了@Configuration)的注冊方法有特殊處理,多次調用並不會反復new實例,如果容器中已經存在實例,再次調用會從容器中直接取出。

 

2. 如果service方法中加了try/catch語句,在捕獲異常之后,之前的數據庫更新操作並不會回滾,例如

@Transactional(rollbackFor = Exception.class)
public void create(String staffId, Integer alloted) {
    try {
        //執行--不回滾!
        this.variationDao.create(staffId, alloted);
        //手動制造異常
        int i = 10 / 0;
    } catch (Exception e) {
        //捕獲異常后,數據庫的更新操作並不會回滾
        e.printStackTrace();
    }
}

原因可能跟下面這段spring源碼有關,但是暫時我沒找到直接原因

public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
/**
     * Handle a throwable, completing the transaction.
     * We may commit or roll back, depending on the configuration.
     * @param txInfo information about the current transaction
     * @param ex throwable encountered
     */
    protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
        if (txInfo != null && txInfo.getTransactionStatus() != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
                        "] after exception: " + ex);
            }
            if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
                try {
                    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
                }
                catch (TransactionSystemException ex2) {
                    logger.error("Application exception overridden by rollback exception", ex);
                    ex2.initApplicationException(ex);
                    throw ex2;
                }
                catch (RuntimeException | Error ex2) {
                    logger.error("Application exception overridden by rollback exception", ex);
                    throw ex2;
                }
            }
            else {
                // We don't roll back on this exception.
                // Will still roll back if TransactionStatus.isRollbackOnly() is true.
                try {
                    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
                }
                catch (TransactionSystemException ex2) {
                    logger.error("Application exception overridden by commit exception", ex);
                    ex2.initApplicationException(ex);
                    throw ex2;
                }
                catch (RuntimeException | Error ex2) {
                    logger.error("Application exception overridden by commit exception", ex);
                    throw ex2;
                }
            }
        }
    }

}

粉色粗體語句上面的注釋,意思是在遇到這種異常時不回滾,但是如果TransactionStatus.isRollbackOnly==true,仍然會回滾;

繼續跟進源碼,TransactionStatus是一個接口,isRollbackOnly方法在它的實現類中找到了:

public abstract class AbstractTransactionStatus implements TransactionStatus {

    private boolean rollbackOnly = false;

    @Override
    public void setRollbackOnly() {
        this.rollbackOnly = true;
    }

    @Override
    public boolean isRollbackOnly() {
        return (isLocalRollbackOnly() || isGlobalRollbackOnly());
    }

    /**
     * Determine the rollback-only flag via checking this TransactionStatus.
     * <p>Will only return "true" if the application called {@code setRollbackOnly}
     * on this TransactionStatus object.
     */
    public boolean isLocalRollbackOnly() {
        return this.rollbackOnly;
    }

}

從上面截取的源碼可以看出,rollbackOnly的默認值是false,但是這個TransactionStatus的實現類也提供了修改它的方法——setRollbackOnly。

至此,我們得出一個結論,如果在service方法中使用了try/catch字句,只要在捕獲異常的同時,把這個TransactionStatus.rollbackOnly設為ture,就可以實現回滾了,demo代碼如下

    /**
     * 創建一條數據
     */
    @Transactional(rollbackFor = Exception.class)
    public void create(String staffId, Integer alloted) {
        try {
            //執行
            this.variationDao.create(staffId, alloted);
            //手動制造異常
            int i = 10 / 0;
        } catch (Exception e) {
            //捕獲異常后,將Transaction.status設置為true,實現回滾
 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            e.printStackTrace();
        }
    }

 

3. 事務與切面的執行順序問題

https://www.cnblogs.com/dubhlinn/p/10735015.html一文中,通過分析事務源碼,得出結論:事務也是springAOP的一種應用。如果容器中存在多個切面,就涉及到執行順序問題,如果對事務和某些切面的執行順序先后有要求,需要讓切面實現Ordered接口(或加@Order注解)手動規定順序,並且在事務的@EnableTransactional注解中設置order屬性,讓事務的order數字大於上述切面即可。項目中一個典型的案例——數據源切換。如果項目中使用了多數據源,而且利用springAOP在service層做數據源切換,如果service層的方法加了事務,這時就要考慮事務和切面的執行順序問題了。因為事務也是一種切面,且事務管理器會加載固定的數據源,如果事務先執行,數據源就已經加載了,后面的切面無法再切換數據源。

/**
 * 用於切換數據源的切面,應用於service層
 * 執行順序為1,在事務之前,這樣加載事務時,會讀取切換后的數據源
 * created by YinHF on 2019-03-14
 */
@Aspect
@Component
@Order(value = 1) public class DataSourceAOP {

    //使用test1庫的接口
    private static final String TEST1 = "execution(* cn.monolog.biz.service.impl.test1.*(..))";
    //使用test2庫的接口
    private static final String TEST2 = "execution(* cn.monolog.biz.service.impl.test2.*(..))";
    //使用test3庫的接口
    private static final String TEST3 = "execution(* cn.monolog.biz.service.impl.test3.*(..))";

    /**
     * 切換數據源 → test1庫
     */
    @Before(TEST1)
    public void switchToApp() {
        MultipleDataSource.setDataSourceKey(MultipleDataSource.TEST_1);
    }

    /**
     * 切換數據源 → test2庫
     */
    @Before(TEST2)
    public void switchToConsole() {
        MultipleDataSource.setDataSourceKey(MultipleDataSource.TEST_2);
    }/**
     * 切換數據源 → test3庫
     */
    @Before(TEST3)
    public void switchToDing() {
        MultipleDataSource.setDataSourceKey(MultipleDataSource.TEST_3);
    }
}
/**
 * 容器配置類
 */
@Configuration
@EnableTransactionManagement(order = 2)
public class TxBeanConfig {
}

 


免責聲明!

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



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