個人認爲, spring的聲明式事務是spring讓人感覺用的最爽的功能之一. 可是在有些時候, 我們使用spring的聲明式事務時卻並沒有效果. 是spring的問題嗎? 下面我們先大致說明一下spring聲明式事務的原理, 然後再分析在什麼情況下, spring的聲明式事務會失效.
代理模式
我們知道, spring的聲明式事務是基於代理模式的. 那麼說事務之前我們還是大致的介紹一下代理模式吧. 其實代理模式相當簡單, 就是將另一個類包裹在我們的類外面, 在調用我們創建的方法之前, 先經過外面的方法, 進行一些處理, 返回之前, 再進行一些操作. 比如:
public class UserService{
...
public User getUserByName(String name) {
return userDao.getUserByName(name);
}
...
}
那麼如果配置了事務, 就相當於又創建了一個類:
public class UserServiceProxy extends UserService{
private UserService userService;
...
public User getUserByName(String name){
User user = null;
try{
// 在這裏開啓事務
user = userService.getUserByName(name);
// 在這裏提交事務
}
catch(Exception e){
// 在這裏回滾事務
// 這塊應該需要向外拋異常, 否則我們就無法獲取異常信息了.
// 至於方法聲明沒有添加異常聲明, 是因爲覆寫方法, 異常必須和父類聲明的異常"兼容".
// 這塊應該是利用的java虛擬機並不區分普通異常和運行時異常的特點.
throw e;
}
return user;
}
...
}
然後我們使用的是 UserServiceProxy 類, 所以就可以"免費"得到事務的支持:
@Autowired
private UserService userService; // 這裏spring注入的實際上是UserServiceProxy的對象
private void test(){
// 由於userService是UserServiceProxy的對象, 所以擁有了事務管理的能力
userService.getUserByName("aa");
}
哪些情況下spring的事務管理會失效
private 方法, final 方法 和 static 方法不能添加事務
上面的東西並不難. 那麼我們可以從上面知道些什麼呢? 首先, 由於java繼承時, 不能重寫 private , final , static 修飾的方法. 所以, 所有的 private 方法, final 方法 和 static 方法 都無法 直接 添加spring的事務管理功能. 比如下面的代碼(完整代碼點擊這裏下載):
/**
* 保存兩個user對象. 添加了spring事務註解
*/
@Transactional
public final void saveErrorFinal(User user1, User user2) {
UserDao userDao = getUserDao(); // 此處需要使用getUserDao方法. 不能直接使用userDao
userDao.save(user1);
System.out.println(10 / 0); // 引發異常
userDao.save(user2);
}
/**
* 靜態方法. 添加了spring事務註解
*/
@Transactional
public static void saveErrorStatic(UserDao userDao, User user1, User user2) {
userDao.save(user1);
System.out.println(10 / 0); // 引發異常
userDao.save(user2);
}
/**
* 私有方法保存方法. 添加了spring事務註解
*/
@Transactional
private void saveErrorPrivate(User user1, User user2) {
userDao.save(user1);
System.out.println(10 / 0); // 引發異常
userDao.save(user2);
}
測試代碼如下:
/**
* 檢驗user1是否插入成功, 並嘗試刪除數據. 如果沒有插入, 則報錯, 並退出.
*/
private void validateInsertSuccess() {
User user = userService.getUserByName(user1.getName());
userService.deleteByName(user1.getName()); // 刪除用戶數據
assertNotEquals("插入失敗!", -1, user.getId().intValue());
}
/**
* 檢驗user1是否插入失敗. 如果插入成功, 則刪除數據, 並報錯.
*/
private void validateInsertFail() {
User user = userService.getUserByName(user1.getName());
userService.deleteByName(user1.getName()); // 刪除用戶數據
assertEquals("插入成功, 事務沒有生效!" + user, -1, user.getId().intValue());
}
@Test
public void testSaveErrorFinal() {
try {
userService.saveErrorFinal(user1, user2);
}
catch (ArithmeticException e) {} // 除零異常, 直接忽略
validateInsertFail(); // 我們假設有事務支持, user1添加失敗
}
@Test
public void testSaveErrorStatic() {
try {
UserService.saveErrorStatic(userDao, user1, user2);
}
catch (ArithmeticException e) {}
validateInsertFail(); // 我們假設有事務支持, user1添加失敗
}
由於 saveErrorPrivate 方法外面是無法調用的, 就暫時不去討論了. 我們直接看 testSaveErrorFinal 和 testSaveErrorStatic 方法的運行結果:
很明顯, 事務並沒有生效. 也就是說 private 方法, final 方法 和 static 方法都沒有事務支持.
沒有通過代理對象調用添加事務的方法
仔細看看代理模式中的代碼, 就會發現不通過代理對象調用方法也會導致spring事務管理失效. 繞過代理對象最直接的方法就是自己 new 一個對象, 雖然這種可能性非常小:
new UserService().save(user);
當然, 前面也說了, 這種可能性非常小. 那麼我們看看第二種情況, 這種情況的可能性也不大:
/**
* 保存兩個user對象, 中間產生異常. 驗證spring的事務是否可以正常工作
*/
@Transactional
public void saveError(User user1, User user2) {
userDao.save(user1);
System.out.println(10 / 0); // 引發異常
userDao.save(user2);
}
/**
* 通過{@link #saveError(User, User)}保存數據, 該方法本身並沒有添加事務註解.
* 而是通過{@link #saveError(User, User)}方法使用事務
*/
public void saveByCallMethod(User user1, User user2) {
//saveErrorPrivate(user1, user2); // 或者調用saveErrorPrivate方法
saveError(user1, user2);
}
由於測試的代碼基本上和上面一樣, 所以這裏我們就不貼測試的代碼了. 再說一次, 點擊這裏下載完整代碼). 實際上, 上面的 saveByCallMethod 方法還是無法獲得spring的事務支持. 因爲它的調用堆棧如下圖所示(從下向上):最終結果就是spring的事務管理沒有生效. 這是或許你會想了, 那爲啥不直接給 saveByCallMethod 方法添加事務支持呢? 對啊, 所以我說這種情況的可能性也不大. 下面我們再看看事務管理和多線程纏在一起時的情況:
/**
* 通過創建一個新的線程, 調用{@link #saveError(User, User)}方法來保存用戶, 該方法和
* {@link #saveError(User, User)}方法都添加了事務註解
*/
@Transactional
public void saveByThread(User user1, User user2) {
new Thread(() -> {
try {
Thread.sleep(1000); //耗時操作
saveError(user1, user2);
}
catch (Exception e) {
e.printStackTrace();
}
System.out.println("保存完成");
}).start();
}
測試代碼請參見我提供的完整代碼. 這樣的代碼已經有可能了吧? 那麼事務管理會生效嗎? 我們再看看調用堆棧就知道了.結果和我們想的一樣, spring的事務管理並沒有生效.
總結
好了, 現在我們來回顧一下, 在那些情況下spring的事務管理會失效:
- private 方法無法添加事務管理.
- final 方法無法添加事務管理.
- static 方法無法添加事務管理.
- 當繞過代理對象, 直接調用添加事務管理的方法時, 事務管理將無法生效.