昨天遇到一個非常奇怪的問題,在一個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的作用和實現原理 https://www.guitu18.com/post/2019/04/28/33.html
本次排查記錄總結:
- 在
@Configuration
註解的配置類中,通過@Bean
註冊的對象是沒有被創建代理的,如果你的業務需要使用到代理,請不要使用這種方式。 - 即便沒有直接通過
@Bean
直接注入,在被@Bean
註冊的對象直接依賴(@Autowired
注入等)也會導致該對象提前初始化,沒有被創建代理。 - 如果必須要在通過
@Bean
註冊的對象中用到代理對象,可以從ApplicationContext中獲取到。