相信大家一定用過Spring中的註解型事務,配合上Spring Boot,只需要在方法上打一個@Transactional 就可以完成,真香。
但是如果大家對其中的機制一知半解的話,可能一不小心就會掉進坑,然後久久無法爬出來。
下面我就分享下 被標記為事務的方法互相呼叫的坑。
首先我寫兩個事務方法:
@Autowired
AccountMapper mapper;
@Transactional
@Override
public void insertCodeBear() {
Account account = new Account();
account.setAccount("CodeBear");
account.setPassword("CodeBear");
mapper.insert(account);
}
@Transactional
@Override
public void insertCodeMonkey() {
Account account = new Account();
account.setAccount("CodeMonkey");
account.setPassword("CodeMonkey");
mapper.insert(account);
}
複製程式碼
現在我想在insertCodeBear方法裡面呼叫insertCodeMonkey方法,但是insertCodeMonkey不是很重要,就算失敗,也不能影響到insertCodeBear方法的執行,但是insertCodeMonkey該回滾的還是要回滾,我們很容易寫出如下程式碼:
@Autowired
AccountMapper mapper;
@Transactional
@Override
public void insertCodeBear() {
try {
insertCodeMonkey();
} catch (Exception ex) {
}
Account account = new Account();
account.setAccount("CodeBear");
account.setPassword("CodeBear");
mapper.insert(account);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insertCodeMonkey() {
Account account = new Account();
account.setAccount("CodeMonkey");
account.setPassword("CodeMonkey");
mapper.insert(account);
int a = 1 / 0;//自殺程式碼,便於測試
}
複製程式碼
在第二個方法中,用了自殺程式碼,便於測試。
看上去一點問題都沒有:第一個方法會成功,第二個方法會失敗並且回滾。但是僅僅是看上去,當我們執行一下,會發現奇怪的事情發生了:
兩個方法竟然都成功了!!Why?為了排查問題,需要開啟一下 有關事務 的日誌,在 配置檔案 中加上下面的配置:
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=debug
複製程式碼
然後執行,看下控制檯列印的內容:
圖片可能有點模糊,大家可以在新標籤頁中開啟這圖片,可以看到這裡分明只開了一個事務,而且事務的傳播行為是PROPAGATION_REQUIRED,這是事務的預設傳播行為,也就是這裡只開啟了insertCodeBear方法的事務,並沒有開啟insertCodeMonkey的事務。這是什麼原因?為了更好的說明問題產生的原因,我需要手寫一個AOP。
在此之前大家要達成一個共識,@Transactional 其實也是通過AOP去實現的。
AOP有幾種實現方式,我這裡採用JDK動態代理的方式:
程式碼入口:
public class Main {
public static void main(String[] args) {
BookServiceImpl impl = new BookServiceImpl();
InvocationHandler myInvocationHandler = new MyInvocationHandler(impl);
Object o = Proxy.newProxyInstance(myInvocationHandler.getClass().getClassLoader(),
impl.getClass().getInterfaces(), myInvocationHandler);
((IBookService) o).add();
}
}
複製程式碼
介面:
public interface IBookService {
void add();
void delete();
}
複製程式碼
實現類:
public class BookServiceImpl implements IBookService {
public void add() {
delete();
System.out.println("add");
}
public void delete() {
System.out.println("delete");
}
}
複製程式碼
切面定義:
public class MyInvocationHandler implements InvocationHandler {
private Object obj;
public MyInvocationHandler(Object obj) {
this.obj = obj;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("開始啦,小夥子");
method.invoke(obj, args);
System.out.println("結束啦,小夥子");
return null;
}
}
複製程式碼
在Main入口裡面呼叫了實現類的代理物件,呼叫了add方法,add方法裡面又呼叫了delete的方法。很簡單吧。按照我們的想法,應該是列印出兩次 切面中定義的話,但是事實是 只列印了一次:
讓我們在切面方法中加上這行程式碼: System.out.println("方法是" + method.getName());
複製程式碼
看看是哪個方法進入到了這裡。
執行:
add方法進入到了這裡,但是delete方法卻沒有進來。
讓我們再回到第一個例子,為了讓大家看的清楚一點,我再貼上insertCodeBear被呼叫的程式碼:
@RestController
@RequestMapping("/CodeBear")
public class HelloWorldController {
@Autowired
AccountService service;
@GetMapping("/insert")
public void insert() {
service.insertCodeBear();
}
}
複製程式碼
AccountService 是一個介面,裡面定義了insertCodeBear和insertCodeMonkey虛方法。 我們打一個斷點在
service.insertCodeBear();
複製程式碼
這裡,然後除錯看下service是一個什麼東西:
你會發現,service已經不是簡單的AccountService 的實現類了,而是實現類的代理物件,從這裡也可以看出,其實@Transactional也是通過AOP去實現的。
通過兩個例子,可以得到一個結論:只有呼叫代理物件的方法才能被攔截,所以 在方法A中直接呼叫方法B,方法B是不會被攔截的。
這也就是為什麼insertCodeMonkey的事務沒有被開啟的原因了,因為insertCodeMonkey方法是insertCodeBear直接呼叫的。
那麼,這個問題該如何解決呢?在下一篇部落格,我會採用幾種方式來解決這個問題(這篇部落格已經比較長了,因為加上了很多看上去沒什麼用的“廢話”,因為可以直接寫出結論,然後再寫解決方案就是了。但是我還是很詳細的,把“廢話”都寫出來了,就是因為分析問題的思路才是最重要的 )。