記一次事務失效的問題排查記錄

  • 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註解不生效原因,我每條都確認了沒問題。

  1. 只對public修飾方法才起作用
  2. @Transaction默認檢測異常為RuntimeException及其子類 如果有其他異常需要回滾事務的需要自己手動配置,例如:@Transactional(rollbackFor = Exception.class)
  3. 確保異常沒有被try-catch{},catch以後也不會回滾
  4. 檢查下自己的資料庫是否支援事務,如mysql的mylsam
  5. Springboot項目默認已經支援事務,不用配置;其他類型項目需要在xml中配置是否開啟事務
  6. 如果在同一個類中,一個非@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中,導致被提前初始化而未能創建代理,所以不能開啟事務。


捋一捋:

  1. 首先我們在項目整合Shiro的時候通過ShiroConfig做了一些配置,其中一項包括Shiro的授權認證器MemberAuthorizingRealm。
  2. 在MemberAuthorizingRealm中我們通過@Autowired注入了本篇的主角MemberService。
  3. Spring啟動的時候,配置相關的都是優先初始化的,在初始化MemberAuthorizingRealm的時候發現需要注入一個MemberService對象,容器里肯定是沒有的,那麼就提前將其初始化了。此時如果在MemberService還有通過@Autowired注入的其他依賴,那麼會一併初始化,依賴中要是還有依賴會繼續遞歸初始化,這樣下來會導致一系列的實例都是沒有被代理的。
  4. 但是這時候Spring中創建代理的處理器是還沒有的,導致MemberService的BeanPostProcessor中沒有AbstractAutoProxyCreator這個對象,後面整個BeanPostProcessor列表執行的時候沒有為其創建代理。
  5. 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的作用和實現原理

本次排查記錄總結:

  1. @Configuration註解的配置類中,通過@Bean註冊的對象是沒有被創建代理的,如果你的業務需要使用到代理,請不要使用這種方式。
  2. 即便沒有直接通過@Bean直接注入,在被@Bean註冊的對象直接依賴(@Autowired注入等)也會導致該對象提前初始化,沒有被創建代理。
  3. 如果必須要在通過@Bean註冊的對象中用到代理對象,可以從ApplicationContext中獲取到。