記一次事務失效的問題排查記錄
- 2019 年 12 月 13 日
- 筆記
昨天遇到一個非常奇怪的問題,在一個Service中使用@Transactional
註解的一個方法無論如何都不能開啟事務。項目用的是Springboot和Mybatis Plus,許可權驗證用的是Shiro。Service層的偽程式碼如下:
@Transactional(rollbackFor = Exception.class) public void register(String username, String password) { Member member = new Member(); ... ... this.save(member); MemberMessage memberMessage = new MemberMessage(); ... ... memberMessageService.save(memberMessage); }
當memberMessage插入失敗拋異常時,前面保存的member記錄不會回滾。打斷點發現,只要save(member)這行走完數據就直接插入,此時方法還沒執行完,按道理事務應該還沒提交,但是通過Navicat已經能夠看到新增的記錄了。懷疑是事務壓根沒開啟,遂將logging.level.root
日誌等級改為DEBUG發現壓根就沒開啟事務。
找不到原因,往上層追查,這個方法是在Controller通過@Autowired
注入並調用的。之後我在這個Controller中注入其他Service添加測試方法testSave()
,Controller偽程式碼如下:
@Autowired private MemberService memberService; @Autowired private ConfService confService; @RequestMapping("/register") public JsonResult register(String username, String password) { confService.testSave(); // memberService.register(username, password); return JsonResult.ok(); }
測試發現事務是生效的,且如果發生異常是能夠回滾的,事務正常提交日誌如下:
o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.guitu18.service.base.ConfService$$EnhancerBySpringCGLIB$$82a30421.testSave]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception o.s.j.d.DataSourceTransactionManager : Acquired Connection [com.mysql.jdbc.JDBC4Connection@10d912c1] for JDBC transaction o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [com.mysql.jdbc.JDBC4Connection@10d912c1] to manual commit o.s.j.d.DataSourceTransactionManager : Participating in existing transaction o.s.j.d.DataSourceTransactionManager : Participating in existing transaction org.mybatis.spring.SqlSessionUtils : Creating a new SqlSession org.mybatis.spring.SqlSessionUtils : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@734d6117] o.m.s.t.SpringManagedTransaction : JDBC Connection [com.mysql.jdbc.JDBC4Connection@10d912c1] will be managed by Spring c.g.mapper.base.ClanPlayerMapper.insert : ==> Preparing: INSERT INTO conf ( name, value ... ) VALUES ( ?, ? ) c.g.mapper.base.ClanPlayerMapper.insert : ==> Parameters: 123(String), 45(String) c.g.mapper.base.ClanPlayerMapper.insert : <== Updates: 1 org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@734d6117] org.mybatis.spring.SqlSessionUtils : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@734d6117] org.mybatis.spring.SqlSessionUtils : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@734d6117] org.mybatis.spring.SqlSessionUtils : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@734d6117] o.s.j.d.DataSourceTransactionManager : Initiating transaction commit o.s.j.d.DataSourceTransactionManager : Committing JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@10d912c1] o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@10d912c1] after transaction o.s.jdbc.datasource.DataSourceUtils : Returning JDBC Connection to DataSource
這下子我就納悶了,肯定是有什麼我沒留意到的地方有疏漏,繼續找。先確認了資料庫的表類型是InnoDB能夠支援事務沒錯,接著檢查Spring配置,所在包名,以及是否被Spring掃描等等原因,後面我直接將這兩個Service挪到同一個包下繼續測試,甚至修改了包結構,依然還是ConfService能正常開啟事務,MemberService怎麼也開啟不了事務。
百度也查了,比如@Transaction
註解不生效原因,我每條都確認了沒問題。
- 只對public修飾方法才起作用
@Transaction
默認檢測異常為RuntimeException及其子類 如果有其他異常需要回滾事務的需要自己手動配置,例如:@Transactional(rollbackFor = Exception.class)
- 確保異常沒有被try-catch{},catch以後也不會回滾
- 檢查下自己的資料庫是否支援事務,如mysql的mylsam
- Springboot項目默認已經支援事務,不用配置;其他類型項目需要在xml中配置是否開啟事務
- 如果在同一個類中,一個非
@Transaction
的方法調用有@Transaction
的方法不會生效,因為代理問題
然後昨天為了這個問題折騰的太久,人弄疲了就先放著了。今天接著繼續研究,一路打斷點到TransactionAspectSupport
類中,再到ReflectiveMethodInvocation.proceed()
,invokeJoinpoint()
等方法。
protected Object invokeJoinpoint() throws Throwable { return this.publicMethod ? this.methodProxy.invoke(this.target, this.arguments) : super.invokeJoinpoint(); }
我發現事務生效的情況下,都會一路走到上面這個方法上,這裡判斷如果是public方法,則通過代理對象調用實際業務,至此事務也開啟並加入且生效了。然而那個事務始終不能開啟的MemberService壓根就不會走到這裡來。
這時候我突然想到,該不會是MemberService這個類沒有被代理吧,在Controller中打斷點查看發現MemberService壓根就不是代理對象,@Autowired
注入的是原始對象的實例。

檢查該Controller中注入的另一個ConfService,確實是代理對象沒錯了。

那麼問題來了,為什麼這個MemberService沒有被代理。之前已經做過各種檢查了,甚至將這兩個類放到同一個包下,肯定不是Spring掃描產生的問題。問題出在哪裡呢?繼續找。
從MemberService被引用的地方入手,一路找Shiro的授權認證器AuthorizingRealm這裡。
@Component public class MemberAuthorizingRealm extends AuthorizingRealm { @Autowired private MemberService memberService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { ... ... } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { Member member = memberService.getById(token.getUsername()); ... ... } }
這裡乍一看也沒什麼不對是吧,但是經程式碼過測試問題就出在這裡。這裡我如果不注入MemberService,那麼在其他地方通過@Autowired
注入的就是被代理的實例。What?為什麼會這樣?
不知道原因,看來還是要向上追溯,那麼這個AuthorizingRealm又是在哪裡引用的呢,繼續順著線索往上找。這個類在ShiroConfig中以@Bean
的方式注入到SecurityManager中了。
@Bean("securityManager") public SecurityManager securityManager(MemberAuthorizingRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm); securityManager.setRememberMeManager(null); return securityManager; }
既然是跟配置有關係,那麼我聯想可能是跟初始化順序有關係,配置相關的東西一般都是被優先載入的。找到這裡我想到了Spring的生命周期,隱約感覺真相已經呼之欲出了,趕緊去Spring的Bean初始化流程瞧一瞧,答案肯定是在那裡。
Spring的初始化流程很複雜,這裡只截取重要的部分記錄一下,有興趣的請自行查看Spring初始化相關源碼。首先我們找到代理被創建的地方AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization()
@Override public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException { Object result = existingBean; // 這裡通過getBeanPostProcessors()拿到所有的Bean後置處理器並執行 for (BeanPostProcessor processor : getBeanPostProcessors()) { Object current = processor.postProcessAfterInitialization(result, beanName); if (current == null) { return result; } result = current; } return result; }
在這裡會拿到並執行所有的Bean後置處理器,先找到那個可以開啟事務的ConfService,加個斷點看看他的beanPostProcessors中都有些什麼。

框起來的這兩個DefaultAdvisorAutoProxyCreator就是創建代理對象的處理器,至於為什麼會有兩個現在還不知道,先解決我眼前的問題先。這裡執行完所有的BeanPostProcessor之後,得到的就是代理對象了。

上面創建代理的程式碼在AbstractAutoProxyCreator中,分別是postProcessAfterInitialization()和wrapIfNecessary(),程式碼如下:
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException { if (bean != null) { Object cacheKey = this.getCacheKey(bean.getClass(), beanName); if (!this.earlyProxyReferences.contains(cacheKey)) { return this.wrapIfNecessary(bean, beanName, cacheKey); } } return bean; } // 代理就是在這個方法中創建的,當然創建之前做了各種if判斷 protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { return bean; } else if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { return bean; } else if (!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) { Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null); if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); // 創建代理對象 Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } else { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } } else { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } }
再回頭找到那個MemberService,他的beanPostProcessors列表中可沒有那麼多東西,可以看在他的processor列表中創建代理的處理器DefaultAdvisorAutoProxyCreator確實是沒有的。

這個方法執行完之後,返回的就普通的對象了。我們都知道在Spring中,資料庫事務都是通過AOP實現的,想要支援事務這個類必須被代理才行。至此本篇開頭提到的MemberService中無法開啟事務的真相找到了,因為Controller中注入的MemberService以@Bean
的方式配置到Spring中,導致被提前初始化而未能創建代理,所以不能開啟事務。
捋一捋:
- 首先我們在項目整合Shiro的時候通過ShiroConfig做了一些配置,其中一項包括Shiro的授權認證器MemberAuthorizingRealm。
- 在MemberAuthorizingRealm中我們通過
@Autowired
注入了本篇的主角MemberService。 - Spring啟動的時候,配置相關的都是優先初始化的,在初始化MemberAuthorizingRealm的時候發現需要注入一個MemberService對象,容器里肯定是沒有的,那麼就提前將其初始化了。此時如果在MemberService還有通過
@Autowired
注入的其他依賴,那麼會一併初始化,依賴中要是還有依賴會繼續遞歸初始化,這樣下來會導致一系列的實例都是沒有被代理的。 - 但是這時候Spring中創建代理的處理器是還沒有的,導致MemberService的BeanPostProcessor中沒有AbstractAutoProxyCreator這個對象,後面整個BeanPostProcessor列表執行的時候沒有為其創建代理。
- Spring中的資料庫事務都是需要代理支援的,所以MemberService中不能開啟事務。
解決方案:既然MemberAuthorizingRealm中不能通過@Autowired
注入MemberService,那我們變通一下,不用第一時間注入,等需要用到的時候再向Spring索取就好了。
這裡第一個想到的肯定就是ApplicationContext了,這好辦,寫一個ApplicationContext工具類:
@Component public class ApplicationContextUtils implements ApplicationContextAware { public static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { ApplicationContextUtils.applicationContext = applicationContext; } public static Object getBean(String beanName) { return applicationContext.getBean(beanName); } public static <T> T getBean(Class<T> type) { return applicationContext.getBean(type); } }
通過實現ApplicationContextAware介面拿到ApplicationContext,後面就可以隨心所以了,MemberAuthorizingRealm中需要用到MemberService的時候我們可以這麼寫:
MemberService memberService = ApplicationContextUtils.getBean(MemberService.class);
在其他類似的地方,如果何需要支援事務或者用到代理對象的地方,都可以通過這種方式獲取。另外順帶提一下,如果需要用到對象原始的實例(非代理對象),我們可以通過在Bean名稱前面加一個&
獲取,還是以MemberService舉栗:
MemberService memberService = ApplicationContextUtils.getBean("&memberService");
這樣拿到的就是常規實例對象了,相關知識點:FactoryBean,之前寫過一篇,請參考:
Spring中FactoryBean的作用和實現原理
本次排查記錄總結:
- 在
@Configuration
註解的配置類中,通過@Bean
註冊的對象是沒有被創建代理的,如果你的業務需要使用到代理,請不要使用這種方式。 - 即便沒有直接通過
@Bean
直接注入,在被@Bean
註冊的對象直接依賴(@Autowired
注入等)也會導致該對象提前初始化,沒有被創建代理。 - 如果必須要在通過
@Bean
註冊的對象中用到代理對象,可以從ApplicationContext中獲取到。