聊起Spring aop很多人肯定都觉得自己使用的很熟练,比如日志,事务等等,当我问起事务怎么实现的时候,他们都会说加个注解@Transactional就可以了,而我再问你有没想过这中间有坑的时候,都说不知道,甚至工作五六年的人都不知道。这些坑不是来自spring本身,而是来自使用者,因为他们用的不熟且没有验证。最近,看见公司项目中就有人埋了坑而不自知,所以我决定记录一下。之前已经写过spring 事务作用于异步方法或线程池时有坑,今天说说其他的坑。
1.一个类内部方法间调用的时候,可能不生效,如图:
注意,这里只是可能会不生效,因为如果insert()上加了@Transactional的话,结果就不一样了。解决方案及证明如下:
SpringUtils代码如下:
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext ac;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.ac = applicationContext;
}
public static ApplicationContext getAc() {
return ac;
}
}
当然,也可以使用下面这种方式:
@Autowired
private ApplicationContext applicationContext;
StudentServiceImpl impl = applicationContext.getBean(StudentServiceImpl.class);
那么,问题的根本是什么呢?我们都知道aop是通过动态代理实现的,简言之:@Transactional默认只能通过代理拦截调用,第一幅图中直接调用insert2(),相当于this.insert2(),不是通过代理对象调用的,所以事务不生效,而通过从容器中获取serviceImpl再调用insert2(),这时候就用上了代理对象,所以事务生效。
2. 当@Transactional作用于非public方法(如protected、private)时,事务不生效,如下图:
其实,如果insert2()方法用private修饰时,开发工具甚至会有提示,但是很多人对这种提示视而不见,如下图:
这个的解决方案其实很简单,用public修饰就得了(spring官方也提出过其他解决方案)。至于原因,其实官网有过说法,截图如下:
源码中对此做出的处理在org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource#computeTransactionAttribute中,如下图:
3.需要回滚的异常和抛出的异常不匹配,不指定的情况下默认回滚RuntimeException。其实java中异常可以分为受检异常和非受检异常,简单来讲,受检异常就是你写代码的时候编译器提示你需要抛出或者捕捉的异常,下面以抛出FileNotFoundException(不属于RuntimeException),回滚RuntimeException为例:
最终可以看到数据库多了一条数据,这个问题解决起来比较容易,但可能也容易被忽略。
4.加了注解但异常被捕捉后并未抛出:今天神奇的发现有人在方法上加了注解,而方法体中对所有代码进行了try catch且并没有手动抛出,我不知道这位开发同学怎么想的,他这样做相当于没有异常抛出,spring 事务无法感知到此处需要回滚事务。
很多时候,我发现开发人员觉得自己会用一个注解就觉得掌握了某个知识点,特别是像@Transactional这种被广泛应用于项目中的,其实没有深入了解一个知识点没什么,因为一个人不可能掌握那么多知识,但是,这种问题的产生说明他们没有对代码进行过实际验证,只要验证就知道这些写法都是有问题的。问题虽小,可是如果到生产上由于脏数据的产生而导致的问题往往需要花费很长时间才能解决,且可能造成经济损失。注解虽好,且用且珍惜吧。。。
参考文档: https://docs.spring.io/spring/docs/5.2.4.RELEASE/spring-framework-reference/data-access.html#transaction-declarative-annotations