在Spring中,经常使用@Transactional注解来声明方法需要事务支持。然而,当一个类中的方法自调用(即一个方法调用同类中的另一个方法)时,@Transactional注解可能会失效。这是因为Spring的事务管理是基于代理机制实现的,而自调用无法触发代理逻辑。
示例代码
以下是遇到的一个事务失效场景:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | @Servicepublic class XXServiceImpl implements XXService {
 
 @Override
 public void acquireItem(Long uuid, Long iId, IdptEnum idptEnum, String affairId) {
 String idptKey = getIdptKey(iId, idptEnum, affairId);
 doAcquireItem(uuid, iId, idptKey);
 }
 
 @Transactional
 public void doAcquireItem(Long uuid, Long iId, String idptKey) {
 
 }
 
 private String getIdptKey(Long iId, IdptEnum idptEnum, String affairId) {
 return String.format("%d_%d_%s", iId, idptEnum.getType(), affairId);
 }
 }
 
 | 
在上述代码中,调用了同类中的doAcquireItem方法,但由于是自调用,@Transactional注解没有生效。
原理分析
通过分析,了解到Spring的事务管理是通过AOP(面向切面编程)实现的:
- Spring会为带有@Transactional注解的方法生成代理对象。
- 当外部调用代理对象的方法时,代理会拦截调用并添加事务逻辑。
- 自调用时,调用的是目标对象本身,而不是代理对象,因此事务逻辑不会被触发。
解决方案
方法一:通过代理对象调用
将自调用改为通过代理对象调用,确保事务逻辑生效。
修改后的代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 
 | @Servicepublic class XXServiceImpl implements XXService {
 @Autowired
 private LockService lockService;
 @Autowired
 private XXDao XXDao;
 @Autowired
 @Lazy
 private XXServiceImpl XXServiceImp;
 
 @Override
 public void acquireItem(Long uuid, Long iId, IdptEnum idptEnum, String affairId) {
 String idptKey = getIdptKey(iId, idptEnum, affairId);
 XXServiceImp.doAcquireItem(uuid, iId, idptKey);
 }
 
 @Transactional
 @RedisLock(key = "#idptKey", waitTime = 3000)
 public void doAcquireItem(Long uuid, Long iId, String idptKey) {
 XX XX = XXDao.getByIdpt(idptKey);
 if (Objects.nonNull(XX)) {
 return;
 }
 XX insert = XX.builder()
 .uuid(uuid)
 .iId(iId)
 .status(YesOrNoEnum.NO.getStatus())
 .idpt(idptKey)
 .build();
 XXDao.save(insert);
 }
 
 private String getIdptKey(Long iId, IdptEnum idptEnum, String affairId) {
 return String.format("%d_%d_%s", iId, idptEnum.getType(), affairId);
 }
 }
 
 | 
关键点
- 使用了@Lazy注解延迟注入代理对象,避免循环依赖。
- 通过代理对象调用doAcquireItem方法,确保事务逻辑生效。
方法二:提取方法到另一个类
还尝试将需要事务支持的方法提取到另一个类中,由外部类调用。
修改后的代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 
 | @Servicepublic class XXServiceImpl implements XXService {
 @Autowired
 private LockService lockService;
 @Autowired
 private XXDao XXDao;
 @Autowired
 private TransactionalService transactionalService;
 
 @Override
 public void acquireItem(Long uuid, Long iId, IdptEnum idptEnum, String affairId) {
 String idptKey = getIdptKey(iId, idptEnum, affairId);
 transactionalService.doAcquireItem(uuid, iId, idptKey);
 }
 
 private String getIdptKey(Long iId, IdptEnum idptEnum, String affairId) {
 return String.format("%d_%d_%s", iId, idptEnum.getType(), affairId);
 }
 }
 
 @Service
 public class TransactionalService {
 @Autowired
 private XXDao XXDao;
 
 @Transactional
 @RedisLock(key = "#idptKey", waitTime = 3000)
 public void doAcquireItem(Long uuid, Long iId, String idptKey) {
 XX XX = XXDao.getByIdpt(idptKey);
 if (Objects.nonNull(XX)) {
 return;
 }
 XX insert = XX.builder()
 .uuid(uuid)
 .iId(iId)
 .status(YesOrNoEnum.NO.getStatus())
 .idpt(idptKey)
 .build();
 XXDao.save(insert);
 }
 }
 
 | 
关键点
- 将事务逻辑提取到TransactionalService类中,避免自调用。
- 原类通过@Autowired注入TransactionalService,调用其方法。
方法三:使用AopContext.currentProxy
最后,尝试通过AopContext.currentProxy()获取当前代理对象,并通过代理对象调用方法。
修改后的代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 
 | @Servicepublic class XXServiceImpl implements XXService {
 @Autowired
 private LockService lockService;
 @Autowired
 private XXDao XXDao;
 
 @Override
 public void acquireItem(Long uuid, Long iId, IdptEnum idptEnum, String affairId) {
 String idptKey = getIdptKey(iId, idptEnum, affairId);
 ((XXServiceImpl) AopContext.currentProxy()).doAcquireItem(uuid, iId, idptKey);
 }
 
 @Transactional
 @RedisLock(key = "#idptKey", waitTime = 3000)
 public void doAcquireItem(Long uuid, Long iId, String idptKey) {
 XX XX = XXDao.getByIdpt(idptKey);
 if (Objects.nonNull(XX)) {
 return;
 }
 XX insert = XX.builder()
 .uuid(uuid)
 .iId(iId)
 .status(YesOrNoEnum.NO.getStatus())
 .idpt(idptKey)
 .build();
 XXDao.save(insert);
 }
 
 private String getIdptKey(Long iId, IdptEnum idptEnum, String affairId) {
 return String.format("%d_%d_%s", iId, idptEnum.getType(), affairId);
 }
 }
 
 | 
关键点
- 通过AopContext.currentProxy()获取当前代理对象。
- 确保在Spring配置中启用了exposeProxy属性:
| 12
 3
 
 | <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"><property name="exposeProxy" value="true" />
 </bean>
 
 | 
事务传播行为
在解决问题的过程中,还学习了Spring事务的多种传播行为(Propagation):
- REQUIRED(默认):如果当前存在事务,则加入;否则创建新事务。
- REQUIRES_NEW:总是创建新事务,暂停当前事务。
- NESTED:嵌套事务,支持回滚到子事务。
事务失效的其他场景
- 非public方法:Spring AOP仅支持public方法。
- 异常未被捕获:事务仅在未捕获的RuntimeException或Error时回滚。
- 多线程调用:事务上下文无法跨线程传播。
通过这些方法,成功解决了Spring事务注解自调用失效的问题,并加深了对Spring事务机制的理解。