使用方法
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 { }