通過上一章的學習,我們了解了 Spring Data 操作數據庫的一些常見問題。這一章我們聊一聊數據庫操作中的一個非常重要的話題——事務管理。
Spring 事務管理包含兩種配置方式,第一種是使用 XML 進行模糊匹配,綁定事務管理;第二種是使用注解,這種方式可以對每個需要進行事務處理的方法進行單獨配置,你只需要添加上 @Transactional,然后在注解內添加屬性配置即可。在我們的錯誤案例示范中,我們統一使用更為方便的注解式方式。
另外,補充一點,Spring 在初始化時,會通過掃描攔截對事務的方法進行增強。如果目標方法存在事務,Spring 就會創建一個 Bean 對應的代理(Proxy)對象,并進行相關的事務處理操作。
在正式開始講解事務之前,我們需要搭建一個簡單的 Spring 數據庫的環境。這里我選擇了當下最為流行的 MySQL + Mybatis 作為數據庫操作的基本環境。為了正常使用,我們還需要引入一些配置文件和類,簡單列舉一下。
1.數據庫配置文件 jdbc.properties,配置了數據連接信息。
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
jdbc.username=root
jdbc.password=pass
?2.JDBC 的配置類,從上述 jdbc.properties 加載相關配置項,并創建 JdbcTemplate、DataSource、TransactionManager 相關的 Bean 等。
public class JdbcConfig {@Value("${jdbc.driver}")private String driver;@Value("${jdbc.url}")private String url;@Value("${jdbc.username}")private String username;@Value("${jdbc.password}")private String password;@Bean(name = "jdbcTemplate")public JdbcTemplate createJdbcTemplate(DataSource dataSource) {return new JdbcTemplate(dataSource);}@Bean(name = "dataSource")public DataSource createDataSource() {DriverManagerDataSource ds = new DriverManagerDataSource();ds.setDriverClassName(driver);ds.setUrl(url);ds.setUsername(username);ds.setPassword(password);return ds;}@Bean(name = "transactionManager")public PlatformTransactionManager createTransactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}
?3.應用配置類,通過注解的方式,配置了數據源、MyBatis Mapper 的掃描路徑以及事務等。
@Configuration
@ComponentScan
@Import({JdbcConfig.class})
@PropertySource("classpath:jdbc.properties")
@MapperScan("com.spring.puzzle.others.transaction.example1")
@EnableTransactionManagement
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class AppConfig {public static void main(String[] args) throws Exception {ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);}
}
完成了上述基礎配置和代碼后,我們開始進行案例的講解。
案例 1:unchecked 異常與事務回滾
在系統中,我們需要增加一個學生管理的功能,每一位新生入學后,都會往數據庫里存入學生的信息。我們引入了一個學生類 Student 和與之相關的 Mapper。
其中,Student 定義如下:
public class Student implements Serializable {private Integer id;private String realname;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getRealname() {return realname;}public void setRealname(String realname) {this.realname = realname;}
}
Student 對應的 Mapper 類定義如下:
@Mapper
public interface StudentMapper {@Insert("INSERT INTO `student`(`realname`) VALUES (#{realname})")void saveStudent(Student student);
}
對應數據庫表的 Schema 如下:
CREATE TABLE `student` (`id` int(11) NOT NULL AUTO_INCREMENT,`realname` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
業務類 StudentService,其中包括一個保存的方法 saveStudent。執行一下保存,一切正常。
接下來,我們想要測試一下這個事務會不會回滾,于是就寫了這樣一段邏輯:如果發現用戶名是小明,就直接拋出異常,觸發事務的回滾操作。
@Service
public class StudentService {@Autowiredprivate StudentMapper studentMapper;@Transactionalpublic void saveStudent(String realname) throws Exception {Student student = new Student();student.setRealname(realname);studentMapper.saveStudent(student);if (student.getRealname().equals("小明")) {throw new Exception("該學生已存在");}}
}
然后使用下面的代碼來測試一下,保存一個叫小明的學生,看會不會觸發事務的回滾。
public class AppConfig {public static void main(String[] args) throws Exception {ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);StudentService studentService = (StudentService) context.getBean("studentService");studentService.saveStudent("小明");}
}
?執行結果打印出了這樣的信息:
Exception in thread "main" java.lang.Exception: 該學生已存在
at?com.spring.puzzle.others.transaction.example1.StudentService.saveStudent(StudentService.java:23)
可以看到,異常確實被拋出來,但是檢查數據庫,你會發現數據庫里插入了一條新的記錄。
但是我們的常規思維可能是:在 Spring 里,拋出異常,就會導致事務回滾,而回滾以后,是不應該有數據存入數據庫才對啊。而在這個案例中,異常也拋了,回滾卻沒有如期而至,這是什么原因呢?我們需要研究一下 Spring 的源碼,來找找答案。
案例解析
我們通過 debug 沿著 saveStudent 繼續往下跟,得到了一個這樣的調用棧:
?從這個調用棧中我們看到了熟悉的 CglibAopProxy,另外事務本質上也是一種特殊的切面,在創建的過程中,被 CglibAopProxy 代理。事務處理的攔截器是 TransactionInterceptor,它支撐著整個事務功能的架構,我們來分析下這個攔截器是如何實現事務特性的。
首先,TransactionInterceptor 繼承類 TransactionAspectSupport,實現了接口 MethodInterceptor。當執行代理類的目標方法時,會觸發 invoke()。由于我們的關注重點是在異常處理上,所以直奔主題,跳到異常處理相關的部分。當它 catch 到異常時,會調用 completeTransactionAfterThrowing 方法做進一步處理。
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,final InvocationCallback invocation) throws Throwable {//省略非關鍵代碼Object retVal;try {retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {completeTransactionAfterThrowing(txInfo, ex);throw ex;}finally {cleanupTransactionInfo(txInfo);}//省略非關鍵代碼
}
?在 completeTransactionAfterThrowing 的代碼中,有這樣一個方法 rollbackOn(),這是事務的回滾的關鍵判斷條件。當這個條件滿足時,會觸發 rollback 操作,事務回滾。
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {//省略非關鍵代碼//判斷是否需要回滾if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {try {//執行回滾txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());}catch (TransactionSystemException ex2) {ex2.initApplicationException(ex);throw ex2;}catch (RuntimeException | Error ex2) {throw ex2;}}//省略非關鍵代碼
}
rollbackOn() 其實包括了兩個層級,具體可參考如下代碼:
public boolean rollbackOn(Throwable ex) {// 層級 1:根據"rollbackRules"及當前捕獲異常來判斷是否需要回滾RollbackRuleAttribute winner = null;int deepest = Integer.MAX_VALUE;if (this.rollbackRules != null) {for (RollbackRuleAttribute rule : this.rollbackRules) {// 當前捕獲的異常可能是回滾“異常”的繼承體系中的“一員”int depth = rule.getDepth(ex);if (depth >= 0 && depth < deepest) {deepest = depth;winner = rule;}}}// 層級 2:調用父類的 rollbackOn 方法來決策是否需要 rollbackif (winner == null) {return super.rollbackOn(ex);}return !(winner instanceof NoRollbackRuleAttribute);
}
1. RuleBasedTransactionAttribute 自身的 rollbackOn()
當我們在 @Transactional 中配置了 rollbackFor,這個方法就會用捕獲到的異常和 rollbackFor 中配置的異常做比較。如果捕獲到的異常是 rollbackFor 配置的異常或其子類,就會直接 rollback。在我們的案例中,由于在事務的注解中沒有加任何規則,所以這段邏輯處理其實找不到規則(即 winner == null),進而走到下一步。
2. RuleBasedTransactionAttribute 父類 DefaultTransactionAttribute 的 rollbackOn()
如果沒有在@Transactional 中配置 rollback 屬性,或是捕獲到的異常和所配置異常的類型不一致,就會繼續調用父類的 rollbackOn() 進行處理。
而在父類的 rollbackOn() 中,我們發現了一個重要的線索,只有在異常類型為 RuntimeException 或者 Error 的時候才會返回 true,此時,會觸發 completeTransactionAfterThrowing 方法中的 rollback 操作,事務被回滾。
public boolean rollbackOn(Throwable ex) {return (ex instanceof RuntimeException || ex instanceof Error);
}
查到這里,真相大白,Spring 處理事務的時候,如果沒有在 @Transactional 中配置 rollback 屬性,那么只有捕獲到 RuntimeException 或者 Error 的時候才會觸發回滾操作。而我們案例拋出的異常是 Exception,又沒有指定與之匹配的回滾規則,所以我們不能觸發回滾。
問題修正
從上述案例解析中,我們了解到,Spring 在處理事務過程中,并不會對 Exception 進行回滾,而會對 RuntimeException 或者 Error 進行回滾。
這么看來,修改方法也可以很簡單,只需要把拋出的異常類型改成 RuntimeException 就可以了。于是這部分代碼就可以修改如下:
@Service
public class StudentService {@Autowiredprivate StudentMapper studentMapper;@Transactionalpublic void saveStudent(String realname) throws Exception {Student student = new Student();student.setRealname(realname);studentMapper.saveStudent(student);if (student.getRealname().equals("小明")) {throw new RuntimeException("該用戶已存在");}}
再執行一下,這時候異常會正常拋出,數據庫里不會有新數據產生,表示這時候 Spring 已經對這個異常進行了處理,并將事務回滾。
但是很明顯,這種修改方法看起來不夠優美,畢竟我們的異常有時候是固定死不能隨意修改的。所以結合前面的案例分析,我們還有一個更好的修改方式。
具體而言,我們在解析 RuleBasedTransactionAttribute.rollbackOn的代碼時提到過 rollbackFor 屬性的處理規則。也就是我們在@Transactional 的 rollbackFor 加入需要支持的異常類型(在這里是 Exception)就可以匹配上我們拋出的異常,進而在異常拋出時進行回滾。
于是我們可以完善下案例中的注解,修改后代碼如下:
@Transactional(rollbackFor = Exception.class)
再次測試運行,你會發現一切符合預期了。
案例 2:試圖給 private 方法添加事務
接著上一個案例,我們已經實現了保存學生信息的功能。接下來,我們來優化一下邏輯,讓學生的創建和保存邏輯分離,于是我就對代碼做了一些重構,把 Student 的實例創建和保存邏輯拆到兩個方法中分別進行。然后,把事務的注解 @Transactional 加在了保存數據庫的方法上。
@Service
public class StudentService {@Autowiredprivate StudentMapper studentMapper;@Autowiredprivate StudentService studentService;public void saveStudent(String realname) throws Exception {Student student = new Student();student.setRealname(realname);studentService.doSaveStudent(student);}@Transactionalprivate void doSaveStudent(Student student) throws Exception {studentMapper.saveStudent(student);if (student.getRealname().equals("小明")) {throw new RuntimeException("該用戶已存在");}}
}
執行的時候,繼續傳入參數“小明”,看看執行結果是什么樣子?
異常正常拋出,事務卻沒有回滾。明明是在方法上加上了事務的注解啊,為什么沒有生效呢?我們還是從 Spring 源碼中找答案。
案例解析
通過 debug,我們一步步尋找到了問題的根源,得到了以下調用棧。我們通過 Spring 的源碼來解析一下完整的過程。
前一段是 Spring 創建 Bean 的過程。當 Bean 初始化之后,開始嘗試代理操作,這個過程是從 AbstractAutoProxyCreator 里的 postProcessAfterInitialization 方法開始處理的:
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (this.earlyProxyReferences.remove(cacheKey) != bean) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;
}
?我們一路往下找,暫且略過那些非關鍵要素的代碼,直到到了 AopUtils 的 canApply 方法。這個方法就是針對切面定義里的條件,確定這個方法是否可以被應用創建成代理。其中有一段 methodMatcher.matches(method, targetClass) 是用來判斷這個方法是否符合這樣的條件:
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {//省略非關鍵代碼for (Class<?> clazz : classes) {Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);for (Method method : methods) {if (introductionAwareMethodMatcher != null ?introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :methodMatcher.matches(method, targetClass)) {return true;}}}return false;
}
從 matches() 調用到了 AbstractFallbackTransactionAttributeSource 的 getTransactionAttribute:
public boolean matches(Method method, Class<?> targetClass) {//省略非關鍵代碼TransactionAttributeSource tas = getTransactionAttributeSource();return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
}
其中,getTransactionAttribute 這個方法是用來獲取注解中的事務屬性,根據屬性確定事務采用什么樣的策略。
public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {//省略非關鍵代碼TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);//省略非關鍵代碼}
}
接著調用到 computeTransactionAttribute 這個方法,其主要功能是根據方法和類的類型確定是否返回事務屬性,執行代碼如下:
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {//省略非關鍵代碼if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {return null;}//省略非關鍵代碼
}
這里有這樣一個判斷 allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers()) ,當這個判斷結果為 true 的時候返回 null,也就意味著這個方法不會被代理,從而導致事務的注解不會生效。那此處的判斷值到底是不是 true 呢?我們可以分別看一下。
條件 1:allowPublicMethodsOnly()
allowPublicMethodsOnly 返回了 AnnotationTransactionAttributeSource 的 publicMethodsOnly 屬性。
protected boolean allowPublicMethodsOnly() {return this.publicMethodsOnly;
}
而這個 publicMethodsOnly 屬性是通過 AnnotationTransactionAttributeSource 的構造方法初始化的,默認為 true。
public AnnotationTransactionAttributeSource() {this(true);
}
條件 2:Modifier.isPublic()
這個方法根據傳入的 method.getModifiers() 獲取方法的修飾符。該修飾符是 java.lang.reflect.Modifier 的靜態屬性,對應的幾類修飾符分別是:PUBLIC: 1,PRIVATE: 2,PROTECTED: 4。這里面做了一個位運算,只有當傳入的方法修飾符是 public 類型的時候,才返回 true。
public static boolean isPublic(int mod) {return (mod & PUBLIC) != 0;
}
綜合上述兩個條件,你會發現,只有當注解為事務的方法被聲明為 public 的時候,才會被 Spring 處理。
問題修正
了解了問題的根源以后,解決它就變得很簡單了,我們只需要把它的修飾符從 private 改成 public 就可以了。
不過需要額外補充的是,我們調用這個加了事務注解的方法,必須是調用被 Spring AOP 代理過的方法,也就是不能通過類的內部調用或者通過 this 的方式調用。所以我們的案例的 StudentService,它含有一個自動裝配(Autowired)了自身(StudentService)的實例來完成代理方法的調用。這個問題我們在之前 Spring AOP 的代碼解析中重點強調過,此處就不再詳述了。
@Service
public class StudentService {@Autowiredprivate StudentMapper studentMapper;@Autowiredprivate StudentService studentService;public void saveStudent(String realname) throws Exception {Student student = new Student();student.setRealname(realname);studentService.doSaveStudent(student);}@Transactionalpublic void doSaveStudent(Student student) throws Exception {studentMapper.saveStudent(student);if (student.getRealname().equals("小明")) {throw new RuntimeException("該學生已存在");}}
}
重新運行一下,異常正常拋出,數據庫也沒有新數據產生,事務生效了,問題解決。
Exception in thread "main" java.lang.RuntimeException:該學生已存在?
at com.spring.puzzle.others.transaction.example2.StudentService.doSaveStudent(StudentService.java:27)
?
重點回顧
通過以上兩個案例,相信你對 Spring 的聲明式事務機制已經有了進一步的了解,最后總結下重點:
- Spring 支持聲明式事務機制,它通過在方法上加上 @Transactional,表明該方法需要事務支持。于是,在加載的時候,根據 @Transactional 中的屬性,決定對該事務采取什么樣的策略;
- @Transactional 對 private 方法不生效,所以我們應該把需要支持事務的方法聲明為 public 類型;
- Spring 處理事務的時候,默認只對 RuntimeException 和 Error 回滾,不會對 Exception 回滾,如果有特殊需要,需要額外聲明,例如指明 Transactional 的屬性 rollbackFor 為 Exception.class
?