昨天遇到一個很是奇怪的問題,在一個Service中使用@Transactional
註解的一個方法不管如何都不能開啓事務。項目用的是Springboot和Mybatis Plus,權限驗證用的是Shiro。Service層的僞代碼以下:html
@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發現壓根就沒開啓事務。java
找不到緣由,往上層追查,這個方法是在Controller經過@Autowired
注入並調用的。以後我在這個Controller中注入其餘Service添加測試方法testSave()
,Controller僞代碼以下:mysql
@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(); }
測試發現事務是生效的,且若是發生異常是可以回滾的,事務正常提交日誌以下:spring
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怎麼也開啓不了事務。sql
百度也查了,好比@Transaction
註解不生效緣由,我每條都確認了沒問題。數據庫
@Transaction
默認檢測異常爲RuntimeException及其子類 若是有其餘異常須要回滾事務的須要本身手動配置,例如:@Transactional(rollbackFor = Exception.class)
@Transaction
的方法調用有@Transaction
的方法不會生效,由於代理問題而後昨天爲了這個問題折騰的過久,人弄疲了就先放着了。今天接着繼續研究,一路打斷點到TransactionAspectSupport
類中,再到ReflectiveMethodInvocation.proceed()
,invokeJoinpoint()
等方法。apache
protected Object invokeJoinpoint() throws Throwable { return this.publicMethod ? this.methodProxy.invoke(this.target, this.arguments) : super.invokeJoinpoint(); }
我發現事務生效的狀況下,都會一路走到上面這個方法上,這裏判斷若是是public方法,則經過代理對象調用實際業務,至此事務也開啓並加入且生效了。然而那個事務始終不能開啓的MemberService壓根就不會走到這裏來。session
這時候我忽然想到,該不會是MemberService這個類沒有被代理吧,在Controller中打斷點查看發現MemberService壓根就不是代理對象,@Autowired
注入的是原始對象的實例。mybatis
檢查該Controller中注入的另外一個ConfService,確實是代理對象沒錯了。app
那麼問題來了,爲何這個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中,致使被提早初始化而未能建立代理,因此不能開啓事務。
捋一捋:
@Autowired
注入了本篇的主角MemberService。@Autowired
注入的其餘依賴,那麼會一併初始化,依賴中要是還有依賴會繼續遞歸初始化,這樣下來會致使一系列的實例都是沒有被代理的。解決方案:既然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的做用和實現原理 https://www.guitu18.com/post/2019/04/28/33.html
本次排查記錄總結:
@Configuration
註解的配置類中,經過@Bean
註冊的對象是沒有被建立代理的,若是你的業務須要使用到代理,請不要使用這種方式。@Bean
直接注入,在被@Bean
註冊的對象直接依賴(@Autowired
注入等)也會致使該對象提早初始化,沒有被建立代理。@Bean
註冊的對象中用到代理對象,能夠從ApplicationContext中獲取到。