記一次:事務失效的排查過程。更重要的排查問題的思路和方法值得學習

昨天遇到一個非常奇怪的問題,在一個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注入的是原始對象的實例。

img

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

img

那麼問題來了,爲什麼這個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中都有些什麼。

img

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

img

上面創建代理的代碼在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確實是沒有的。

img

這個方法執行完之後,返回的就普通的對象了。我們都知道在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的作用和實現原理 https://www.guitu18.com/post/2019/04/28/33.html

本次排查記錄總結:

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

首發地址:https://www.guitu18.com/post/2019/10/30/56.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章