相信你們必定用過Spring中的註解型事務,配合上Spring Boot,只須要在方法上打一個@Transactional 就能夠完成,真香。spring
可是若是你們對其中的機制只知其一;不知其二的話,可能一不當心就會掉進坑,而後久久沒法爬出來。bash
下面我就分享下 被標記爲事務的方法互相調用的坑。app
首先我寫兩個事務方法:ide
@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;//自殺代碼,便於測試
}
複製代碼
在第二個方法中,用了自殺代碼,便於測試。ui
看上去一點問題都沒有:第一個方法會成功,第二個方法會失敗而且回滾。可是僅僅是看上去,當咱們運行一下,會發現奇怪的事情發生了: this
兩個方法居然都成功了!!Why?爲了排查問題,須要開啓一下 有關事務 的日誌,在 配置文件 中加上下面的配置:spa
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=debug
複製代碼
而後運行,看下控制檯打印的內容: debug
圖片可能有點模糊,你們能夠在新標籤頁中打開這圖片,能夠看到這裏分明只開了一個事務,並且事務的傳播行爲是PROPAGATION_REQUIRED,這是事務的默認傳播行爲,也就是這裏只開啓了insertCodeBear方法的事務,並無開啓insertCodeMonkey的事務。這是什麼緣由?爲了更好的說明問題產生的緣由,我須要手寫一個AOP。3d
在此以前你們要達成一個共識,@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直接調用的。
那麼,這個問題該如何解決呢?在下一篇博客,我會採用幾種方式來解決這個問題(這篇博客已經比較長了,由於加上了不少看上去沒什麼用的「廢話」,由於能夠直接寫出結論,而後再寫解決方案就是了。可是我仍是很詳細的,把「廢話」都寫出來了,就是由於分析問題的思路纔是最重要的 )。