spring成神之路第四十四篇:詳解 spring 宣告式事務(@Transactional)
spring事務有2種用法:程式設計式事務和宣告式事務。
程式設計式事務上一篇文章中已經介紹了,不熟悉的建議先看一下程式設計式事務的用法。
這篇主要介紹宣告式事務的用法,我們在工作中基本上用的都是宣告式事務,所以這篇文章是比較重要的,建議各位打起精神,正式開始。
什麼是宣告式事務?
所謂宣告式事務,就是通過配置的方式,比如通過配置檔案(xml)或者註解的方式,告訴spring,哪些方法需要spring幫忙管理事務,然後開發者只用關注業務程式碼,而事務的事情spring自動幫我們控制。
比如註解的方式,只需在方法上面加一個@Transaction
註解,那麼方法執行之前spring會自動開啟一個事務,方法執行完畢之後,會自動提交或者回滾事務,而方法內部沒有任何事務相關程式碼,用起來特別的方法。
@Transaction public void insert(String userName){ this.jdbcTemplate.update("insert into t_user (name) values (?)", userName); }
宣告式事務的2種實現方式
- 配置檔案的方式,即在spring xml檔案中進行統一配置,開發者基本上就不用關注事務的事情了,程式碼中無需關心任何和事務相關的程式碼,一切交給spring處理。
- 註解的方式,只需在需要spring來幫忙管理事務的方法上加上@Transaction註解就可以了,註解的方式相對來說更簡潔一些,都需要開發者自己去進行配置,可能有些同學對spring不是太熟悉,所以配置這個有一定的風險,做好程式碼review就可以了。
配置檔案的方式這裡就不講了,用的相對比較少,我們主要掌握註解的方式如何使用,就可以了。
宣告式事務註解方式5個步驟
1、啟用Spring的註釋驅動事務管理功能
在spring配置類上加上@EnableTransactionManagement
註解
@EnableTransactionManagement public class MainConfig4 { }
簡要介紹一下原理:當spring容器啟動的時候,發現有@EnableTransactionManagement註解,此時會攔截所有bean的建立,掃描看一下bean上是否有@Transaction註解(類、或者父類、或者介面、或者方法中有這個註解都可以),如果有這個註解,spring會通過aop的方式給bean生成代理物件,代理物件中會增加一個攔截器,攔截器會攔截bean中public方法執行,會在方法執行之前啟動事務,方法執行完畢之後提交或者回滾事務。稍後會專門有一篇文章帶大家看這塊的原始碼。
如果有興趣的可以自己先去讀一下原始碼,主要是下面這個這方法會
org.springframework.transaction.interceptor.TransactionInterceptor#invoke
再來看看 EnableTransactionManagement 的原始碼
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(TransactionManagementConfigurationSelector.class) public @interface EnableTransactionManagement { /** * spring是通過aop的方式對bean建立代理物件來實現事務管理的 * 建立代理物件有2種方式,jdk動態代理和cglib代理 * proxyTargetClass:為true的時候,就是強制使用cglib來建立代理 */ boolean proxyTargetClass() default false; /** * 用來指定事務攔截器的順序 * 我們知道一個方法上可以新增很多攔截器,攔截器是可以指定順序的 * 比如你可以自定義一些攔截器,放在事務攔截器之前或者之後執行,就可以通過order來控制 */ int order() default Ordered.LOWEST_PRECEDENCE; }
2、定義事務管理器
事務交給spring管理,那麼你肯定要建立一個或者多個事務管理者,有這些管理者來管理具體的事務,比如啟動事務、提交事務、回滾事務,這些都是管理者來負責的。
spring中使用PlatformTransactionManager這個介面來表示事務管理者。
PlatformTransactionManager多個實現類,用來應對不同的環境
JpaTransactionManager:如果你用jpa來操作db,那麼需要用這個管理器來幫你控制事務。
DataSourceTransactionManager:如果你用是指定資料來源的方式,比如操作資料庫用的是:JdbcTemplate、mybatis、ibatis,那麼需要用這個管理器來幫你控制事務。
HibernateTransactionManager:如果你用hibernate來操作db,那麼需要用這個管理器來幫你控制事務。
JtaTransactionManager:如果你用的是java中的jta來操作db,這種通常是分散式事務,此時需要用這種管理器來控制事務。
比如:我們用的是mybatis或者jdbctemplate,那麼通過下面方式定義一個事務管理器。
@Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
3、需使用事務的目標上加@Transaction註解
- @Transaction放在介面上,那麼介面的實現類中所有public都被spring自動加上事務
- @Transaction放在類上,那麼當前類以及其下無限級子類中所有pubilc方法將被spring自動加上事務
- @Transaction放在public方法上,那麼該方法將被spring自動加上事務
- 注意:@Transaction只對public方法有效
下面我們看一下@Transactional原始碼:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { /** * 指定事務管理器的bean名稱,如果容器中有多事務管理器PlatformTransactionManager, * 那麼你得告訴spring,當前配置需要使用哪個事務管理器 */ @AliasFor("transactionManager") String value() default ""; /** * 同value,value和transactionManager選配一個就行,也可以為空,如果為空,預設會從容器中按照型別查詢一個事務管理器bean */ @AliasFor("value") String transactionManager() default ""; /** * 事務的傳播屬性 */ Propagation propagation() default Propagation.REQUIRED; /** * 事務的隔離級別,就是制定資料庫的隔離級別,資料庫隔離級別大家知道麼?不知道的可以去補一下 */ Isolation isolation() default Isolation.DEFAULT; /** * 事務執行的超時時間(秒),執行一個方法,比如有問題,那我不可能等你一天吧,可能最多我只能等你10秒 * 10秒後,還沒有執行完畢,就彈出一個超時異常吧 */ int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; /** * 是否是隻讀事務,比如某個方法中只有查詢操作,我們可以指定事務是隻讀的 * 設定了這個引數,可能資料庫會做一些效能優化,提升查詢速度 */ boolean readOnly() default false; /** * 定義零(0)個或更多異常類,這些異常類必須是Throwable的子類,當方法丟擲這些異常及其子類異常的時候,spring會讓事務回滾 * 如果不配做,那麼預設會在 RuntimeException 或者 Error 情況下,事務才會回滾 */ Class<? extends Throwable>[] rollbackFor() default {}; /** * 和 rollbackFor 作用一樣,只是這個地方使用的是類名 */ String[] rollbackForClassName() default {}; /** * 定義零(0)個或更多異常類,這些異常類必須是Throwable的子類,當方法丟擲這些異常的時候,事務不會回滾 */ Class<? extends Throwable>[] noRollbackFor() default {}; /** * 和 noRollbackFor 作用一樣,只是這個地方使用的是類名 */ String[] noRollbackForClassName() default {}; }
引數介紹
引數 | 描述 |
---|---|
value | 指定事務管理器的bean名稱,如果容器中有多事務管理器PlatformTransactionManager,那麼你得告訴spring,當前配置需要使用哪個事務管理器 |
transactionManager | 同value,value和transactionManager選配一個就行,也可以為空,如果為空,預設會從容器中按照型別查詢一個事務管理器bean |
propagation | 事務的傳播屬性,下篇文章詳細介紹 |
isolation | 事務的隔離級別,就是制定資料庫的隔離級別,資料庫隔離級別大家知道麼?不知道的可以去補一下 |
timeout | 事務執行的超時時間(秒),執行一個方法,比如有問題,那我不可能等你一天吧,可能最多我只能等你10秒 10秒後,還沒有執行完畢,就彈出一個超時異常吧 |
readOnly | 是否是隻讀事務,比如某個方法中只有查詢操作,我們可以指定事務是隻讀的 設定了這個引數,可能資料庫會做一些效能優化,提升查詢速度 |
rollbackFor | 定義零(0)個或更多異常類,這些異常類必須是Throwable的子類,當方法丟擲這些異常及其子類異常的時候,spring會讓事務回滾 如果不配做,那麼預設會在 RuntimeException 或者 Error 情況下,事務才會回滾 |
rollbackForClassName | 同 rollbackFor,只是這個地方使用的是類名 |
noRollbackFor | 定義零(0)個或更多異常類,這些異常類必須是Throwable的子類,當方法丟擲這些異常的時候,事務不會回滾 |
noRollbackForClassName | 同 noRollbackFor,只是這個地方使用的是類名 |
4、執行db業務操作
在@Transaction標註類或者目標方法上執行業務操作,此時這些方法會自動被spring進行事務管理。
如,下面的insertBatch操作,先刪除資料,然後批量插入資料,方法上加上了@Transactional註解,此時這個方法會自動受spring事務控制,要麼都成功,要麼都失敗。
@Component public class UserService { @Autowired private JdbcTemplate jdbcTemplate; //先清空表中資料,然後批量插入資料,要麼都成功要麼都失敗 @Transactional public void insertBatch(String... names) { jdbcTemplate.update("truncate table t_user"); for (String name : names) { jdbcTemplate.update("INSERT INTO t_user(name) VALUES (?)", name); } } }
5、啟動spring容器,使用bean執行業務操作
@Test public void test1() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MainConfig4.class); context.refresh(); UserService userService = context.getBean(UserService.class); userService.insertBatch("java高併發系列", "mysql系列", "maven系列", "mybatis系列"); }
案例1
準備資料庫
DROP DATABASE IF EXISTS javacode2018; CREATE DATABASE if NOT EXISTS javacode2018; USE javacode2018; DROP TABLE IF EXISTS t_user; CREATE TABLE t_user( id int PRIMARY KEY AUTO_INCREMENT, name varchar(256) NOT NULL DEFAULT '' COMMENT '姓名' );
spring配置類
package com.javacode2018.tx.demo4; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; import javax.sql.DataSource; @EnableTransactionManagement //@1 @Configuration @ComponentScan public class MainConfig4 { //定義一個數據源 @Bean public DataSource dataSource() { org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8"); dataSource.setUsername("root"); dataSource.setPassword("root123"); dataSource.setInitialSize(5); return dataSource; } //定義一個JdbcTemplate,用來執行db操作 @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } //定義我一個事物管理器 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { //@2 return new DataSourceTransactionManager(dataSource); } }
@1:使用@EnableTransactionManagement註解開啟spring事務管理
@2:定義事務管理器
來個業務類
package com.javacode2018.tx.demo4; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; @Component public class UserService { @Autowired private JdbcTemplate jdbcTemplate; //先清空表中資料,然後批量插入資料,要麼都成功要麼都失敗 @Transactional //@1 public int insertBatch(String... names) { int result = 0; jdbcTemplate.update("truncate table t_user"); for (String name : names) { result += jdbcTemplate.update("INSERT INTO t_user(name) VALUES (?)", name); } return result; } //獲取所有使用者資訊 public List<Map<String, Object>> userList() { return jdbcTemplate.queryForList("SELECT * FROM t_user"); } }
@1:insertBatch方法上加上了@Transactional註解,讓spring來自動為這個方法加上事務
測試類
package com.javacode2018.tx.demo4; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Demo4Test { @Test public void test1() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MainConfig4.class); context.refresh(); UserService userService = context.getBean(UserService.class); //先執行插入操作 int count = userService.insertBatch( "java高併發系列", "mysql系列", "maven系列", "mybatis系列"); System.out.println("插入成功(條):" + count); //然後查詢一下 System.out.println(userService.userList()); } }
執行輸出
插入成功(條):4
[{id=1, name=java高併發系列}, {id=2, name=mysql系列}, {id=3, name=maven系列}, {id=4, name=mybatis系列}]
有些朋友可能會問,如何知道這個被呼叫的方法有沒有使用事務? 下面我們就來看一下。
如何確定方法有沒有用到spring事務
方式1:斷點除錯
spring事務是由TransactionInterceptor攔截器處理的,最後會呼叫下面這個方法,設定個斷點就可以看到詳細過程了。
org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
方式2:看日誌
spring處理事務的過程,有詳細的日誌輸出,開啟日誌,控制檯就可以看到事務的詳細過程了。
新增maven配置
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
src\main\resources新建logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>[%d{MM-dd HH:mm:ss.SSS}][%thread{20}:${PID:- }][%X{trace_id}][%level][%logger{56}:%line:%method\(\)]:%msg%n##########**********##########%n</pattern> </encoder> </appender> <logger name="org.springframework" level="debug"> <appender-ref ref="STDOUT" /> </logger> </configuration>
再來執行一下案例1
[09-10 11:20:38.830][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:370:getTransaction()]:Creating new transaction with name [com.javacode2018.tx.demo4.UserService.insertBatch]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ##########**********########## [09-10 11:20:39.120][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:265:doBegin()]:Acquired Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@65fe9e33]]] for JDBC transaction ##########**********########## [09-10 11:20:39.125][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:283:doBegin()]:Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@65fe9e33]]] to manual commit ##########**********########## [09-10 11:20:39.139][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:502:update()]:Executing SQL update [truncate table t_user] ##########**********########## [09-10 11:20:39.169][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:860:update()]:Executing prepared SQL update ##########**********########## [09-10 11:20:39.169][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:609:execute()]:Executing prepared SQL statement [INSERT INTO t_user(name) VALUES (?)] ##########**********########## [09-10 11:20:39.234][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:860:update()]:Executing prepared SQL update ##########**********########## [09-10 11:20:39.235][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:609:execute()]:Executing prepared SQL statement [INSERT INTO t_user(name) VALUES (?)] ##########**********########## [09-10 11:20:39.236][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:860:update()]:Executing prepared SQL update ##########**********########## [09-10 11:20:39.237][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:609:execute()]:Executing prepared SQL statement [INSERT INTO t_user(name) VALUES (?)] ##########**********########## [09-10 11:20:39.238][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:860:update()]:Executing prepared SQL update ##########**********########## [09-10 11:20:39.239][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:609:execute()]:Executing prepared SQL statement [INSERT INTO t_user(name) VALUES (?)] ##########**********########## [09-10 11:20:39.241][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:741:processCommit()]:Initiating transaction commit ##########**********########## [09-10 11:20:39.241][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:328:doCommit()]:Committing JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@65fe9e33]]] ##########**********########## [09-10 11:20:39.244][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:387:doCleanupAfterCompletion()]:Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@65fe9e33]]] after transaction ##########**********########## 插入成功(條):4 [09-10 11:20:39.246][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:427:query()]:Executing SQL query [SELECT * FROM t_user] ##########**********########## [09-10 11:20:39.247][main: ][][DEBUG][org.springframework.jdbc.datasource.DataSourceUtils:115:doGetConnection()]:Fetching JDBC Connection from DataSource ##########**********########## [{id=1, name=java高併發系列}, {id=2, name=mysql系列}, {id=3, name=maven系列}, {id=4, name=mybatis系列}]
來理解一下日誌
insertBatch方法上有@Transaction註解,所以會被攔截器攔截,下面是在insertBatch方法呼叫之前,建立了一個事務。
insertBatch方法上@Transaction註解引數都是預設值,@Transaction註解中可以通過value或者transactionManager
來指定事務管理器,但是沒有指定,此時spring會在容器中按照事務管理器型別找一個預設的,剛好我們在spring容器中定義了一個,所以直接拿來用了。事務管理器我們用的是new DataSourceTransactionManager(dataSource)
,從事務管理器的datasource中獲取一個數據庫連線,然後通過連線設定事務為手動提交,然後將(datasource->這個連線)丟到ThreadLocal中了,具體為什麼,可以看上一篇文章。
下面就正是進入insertBatch方法內部了,通過jdbctemplate執行一些db操作,jdbctemplate內部會通過datasource到上面的threadlocal中拿到spring事務那個連線,然後執行db操作。
最後insertBatch方法執行完畢之後,沒有任何異常,那麼spring就開始通過資料庫連線提交事務了。
總結
本文講解了一下spring中程式設計式事務的使用步驟。
主要涉及到了2個註解:
@EnableTransactionManagement:開啟spring事務管理功能
@Transaction:將其加在需要spring管理事務的類、方法、介面上,只會對public方法有效。
大家再消化一下,有問題,歡迎留言交流。
下篇文章將詳細介紹事務的傳播屬性,敬請期待。
案例原始碼
git地址: https://gitee.com/javacode2018/spring-series 本文案例對應原始碼:spring-series\lesson-002-tx\src\main\java\com\javacode2018\tx\demo4
路人甲java所有案例程式碼以後都會放到這個上面,大家watch一下,可以持續關注動態。
來源:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648936892&idx=2&sn=473a156dc141a2efc0580f93567f0630&scene=21#wechat_redirect