使用springboot時,只要引入spring-jdbc/jpa相關的依賴后,在想要啟用事務的方法上加上@Transactional注解就能開啟事務,碰到異常就能自動回滾。大大的提高了編碼的便捷性性,同時也不侵入代碼,保持了代碼的簡潔性。
默認情況下,Spring時使用的Spring AOP (mode=Mode.Proxy, proxyTargetClass=false)方式啟動數據庫事務攔截。只有了解清楚了具體背景,才能清除知道事務為什么在碰到異常時沒有能夠正確回滾。下面是一些常用場景分析:
場景1、未正確配置TransactionManager
使用springboot開發時,引入以下依賴后通常會自動啟用TransactionManager。
-
spring-boot-starter-jdbc
?是 Spring Boot 提供的用于簡化 JDBC(Java Database Connectivity)開發的啟動器,引入該依賴后,Spring Boot 會自動配置?DataSourceTransactionManager
。 -
spring-boot-starter-data-jpa
?是 Spring Boot 提供的用于簡化 JPA(Java Persistence API)開發的啟動器,它集成了 Hibernate 等 JPA 實現框架,方便開發者進行數據庫操作。引入該依賴后,Spring Boot 會自動配置?JpaTransactionManager
。
通過下面代碼中的printTransactionManager(TransactionManager transactionManager) 方法可以檢查是否配置正常。
@SpringBootApplication
public class MybatisApplication {public static void main(String[] args) {org.springframework.boot.SpringApplication.run(MybatisApplication.class, args);}@BeanObject printTransactionManager(TransactionManager transactionManager) {System.out.println("transactionManager: " + transactionManager);return null;}
}
transactionManager: org.springframework.jdbc.support.JdbcTransactionManager@590765c4?
通過打印語句,可以看到spring中的TransactionManager是否正確配置。
場景2、@Transaction注解不在public方法上
默認情況下,事務是在proxy模式下(即Spring AOP負責攔截事務),proxyTargetClass=false ,有接口的時候使用JDK動態代理實現。沒有接口時使用CGLIB進行代理。
JDK代理接口時,都是public方法。
CGLIB代理時,在public方法上能生效。在Spring 6.0 以后,除public方法外,可以代理protected, package-visable修飾的方法。
當@Transactional注解位于private/final修飾的方法上時,事務碰到異常不能正常回滾。
詳情參考文檔:
Method visibility and?
@Transactional
?in proxy modeThe?
@Transactional
?annotation is typically used on methods with?public
?visibility. As of 6.0,?protected
?or package-visible methods can also be made transactional for class-based proxies by default. Note that transactional methods in interface-based proxies must always be?public
?and defined in the proxied interface. For both kinds of proxies, only external method calls coming in through the proxy are intercepted.
Using @Transactional :: Spring Framework
示例代碼:
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Transactionalprivate void createUser(User user) {// 插入用戶數據userRepository.save(user);// 可能會拋出異常if (user.getName().equals("error")) {throw new RuntimeException("創建用戶失敗");}}
}
因為方法的可見性,private修飾的方法不能被代理攔截
場景3、調用內部方法
@Transactional
?注解使用 AOP 實現事務管理,而 AOP 是基于代理模式的。當在同一個類內部一個沒有事務注解的方法調用有?@Transactional
?注解的方法時,事務注解會失效,因為這種調用沒有經過代理對象。
示例代碼如下:
// 定義 UserService 類,處理用戶相關業務邏輯
@Service
class UserService {@Autowiredprivate UserRepository userRepository;// 無事務注解的方法,內部調用有事務注解的方法public void outerMethod() {try {innerMethod();} catch (Exception e) {System.out.println("Exception caught: " + e.getMessage());}}// 有事務注解的方法@Transactionalpublic void innerMethod() {User user = new User("John");userRepository.save(user);// 模擬拋出異常throw new RuntimeException("Simulated exception");}
}
示例代碼中,雖然Spring AOP代理了innerMethod方法,但是原始事務不是通過代理的innerMethod進入,而是通過原始類的outerMethod進入,這樣就調用的是原始類的innerMethod方法,導致不能進入代理類的innerMethod方法,事務攔截不能生效。
場景4、方法內部Catch了異常
在 Spring 中使用?@Transactional
?注解時,如果在方法內部捕獲了異常且沒有重新拋出,會導致事務無法正常回滾,從而使?@Transactional
?注解失效。
@Transactional
?注解的事務管理是基于 AOP 實現的,它會在目標方法拋出異常時進行事務回滾。默認情況下,@Transactional
?注解只對未被捕獲的?RuntimeException
?及其子類異常進行回滾操作。如果在方法內部捕獲了異常,Spring 就無法感知到異常的拋出,從而不會觸發事務回滾邏輯,導致事務繼續提交,@Transactional
?注解的功能失效。
示例代碼
// 定義 UserService 類,處理用戶相關業務邏輯
@Service
class UserService {@Autowiredprivate UserRepository userRepository;@Transactionalpublic void createUser() {User user = new User("John");userRepository.save(user);try {// 模擬拋出異常throw new RuntimeException("Simulated exception");} catch (Exception e) {// 捕獲異常但未重新拋出System.out.println("Exception caught: " + e.getMessage());}}
}
createUser()
?方法:使用?@Transactional
?注解標記,在方法內部保存用戶信息后拋出一個?RuntimeException
?異常,并在?catch
?塊中捕獲該異常,但沒有重新拋出。
解決辦法:重新拋出異常
在?catch
?塊中重新拋出異常,讓 Spring 能夠感知到異常的發生,從而觸發事務回滾邏輯。
@Transactional
public void createUser() {User user = new User("John");userRepository.save(user);try {throw new RuntimeException("Simulated exception");} catch (Exception e) {System.out.println("Exception caught: " + e.getMessage());// 重新拋出異常throw e;}
}
場景5、?rollbackFor/rollbackForClassName屬性未正確配置
-
默認回滾規則:
@Transactional
注解默認只對RuntimeException
及其子類和Error
進行回滾。若拋出的是受檢查異常(如IOException
、SQLException
),默認不會觸發回滾。
@Service
class UserService {@Autowiredprivate UserRepository userRepository;@Transactionalpublic void createUser() throws IOException {User user = new User("John");userRepository.save(user);// 拋出受檢查異常throw new IOException("Simulated IOException");}
}
IOException是一個CheckedException的子類,不在默認的回滾體系內,所以不能自動回滾。需要使用rollbackFor屬性顯式指定才能生效。
- rollbackFor=BaseException.class, 針對BaseException和它的子類回滾。若拋出的異常不在繼承體系內,則不能自動回滾。
@Service public class OrderService {@Autowiredprivate OrderRepository orderRepository;@Transactional(rollbackFor = BaseException.class)public void createOrder(Order order) throws Exception {orderRepository.createOrder(order);// will rollback// throw new SubException("出現未知錯誤");// will not rollbackthrow new OtherException("出現未知錯誤");} }class BaseException extends Exception {public BaseException(String message) {super(message);} }class SubException extends BaseException {public SubException(String message) {super(message);} }class OtherException extends Exception {public OtherException(String message) {super(message);} }
rollbackForClassName=exceptionPattern, exceptionPattern可以包含異常名字全部或者部分字符。注意這里不是正則表達式,而是基于String.contains(exceptionPattern)來判斷的。
匹配規則的源碼如下:
private int getDepth(Class<?> exceptionType, int depth) {if (this.exceptionType != null) {if (this.exceptionType.equals(exceptionType)) {// Found it!return depth;}}else if (exceptionType.getName().contains(this.exceptionPattern)) {// Found it!return depth;}// If we've gone as far as we can go and haven't found it...if (exceptionType == Throwable.class) {return -1;}return getDepth(exceptionType.getSuperclass(), depth + 1);}
不能匹配的示例代碼如下:
@Slf4j
@Service
public class OrderService {@Autowiredprivate OrderRepository orderRepository;@Transactional(rollbackForClassName = "Base*Exception")public void createOrder(Order order) throws Exception {orderRepository.createOrder(order);// will not rollbackthrow new Base1Exception("出現未知錯誤");}
}
?因為exception.typeName="Base1Exception" contains("Base*Exception") 結果未false,所以不匹配,導致不能回滾。
正確的用法如下:
@Service
public class OrderService {@Autowiredprivate OrderRepository orderRepository;@Transactional(rollbackForClassName = "Base")public void createOrder(Order order) throws Exception {orderRepository.createOrder(order);// will rollbackthrow new Base1Exception("出現未知錯誤");}
}
以上是我過去經常碰到的@Transactional碰到異常不能正常回滾的案例總結,若有遺漏歡迎下方留言。
參考文檔:
Declarative Transaction Management :: Spring Framework