在实际开发中,Spring 的事务管理为我们提供了非常便捷的控制手段。然而,由于其基于 AOP 代理机制的实现方式,有时候在同一个
Service 类中调用另一个带有@Transactional
注解的方法时,会遇到事务不生效的问题。本文将结合实例与案例,详细解析这一问题的原理、常见错误示例以及正确的解决方案。
Spring 通过 AOP(面向切面编程)实现事务管理。当容器扫描到方法上有 @Transactional
注解时,会为目标对象生成一个代理类。在代理类中,在目标方法调用前后分别执行事务的开启、提交或回滚等操作。Spring 事务管理的核心机制包括:
事务传播行为
Spring 定义了多种事务传播属性(如 REQUIRED
、REQUIRES_NEW
、NESTED
等),用于在多个事务方法相互调用时决定事务如何传播。
Spring事务传播属性:
1.propagation-required: 支持当前事务,如果有就加入当前事务中;如果当前方法没有事务,就新建一个事务;
2.propagation-supports: 支持当前事务,如果有就加入当前事务中;如果当前方法没有事务,就以非事务的方式执行;
3.propagation-mandatory: 支持当前事务,如果有就加入当前事务中;如果当前没有事务,就抛出异常;
4.propagation-requires_new: 新建事务,如果当前存在事务,就把当前事务挂起;如果当前方法没有事务,就新建事务;
5.propagation-not-supported: 以非事务方式执行,如果当前方法存在事务就挂起当前事务;如果当前方法不存在事务,就以非事务方式执行;
6.propagation-never: 以非事务方式执行,如果当前方法存在事务就抛出异常;如果当前方法不存在事务,就以非事务方式执行;
7.propagation-nested: 如果当前方法有事务,则在嵌套事务内执行;如果当前方法没有事务,则与required操作类似;
前六个策略类似于EJB CMT,第七个(PROPAGATION_NESTED)是Spring所提供的一个特殊变量。
它要求事务管理器或者使用JDBC 3.0 Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionManager)
AOP 代理机制
事务管理的生效依赖于对外部方法调用的拦截,当一个方法被代理对象调用时,会先执行事务增强逻辑。但如果在同一个类内部直接调用,调用不会经过代理,事务注解将不会生效。
设想在一个订单 Service 中,有两个方法:
@Transactional
注解);@Transactional
注解)。如果在 completeOrder()
中直接调用 sendNotification()
,代码示例如下:
@Service
public class OrderService {
@Transactional
public void completeOrder(Long orderId) {
// 更新订单状态
updateOrderStatus(orderId, "COMPLETED");
// 内部调用 sendNotification() —— 事务注解不会生效
sendNotification(orderId);
}
@Transactional
public void sendNotification(Long orderId) {
// 发送通知逻辑,比如调用邮件服务
// 这里的事务并不会单独开启
}
private void updateOrderStatus(Long orderId, String status) {
// 数据库操作,更新订单状态
}
}
问题解析:
spring 在扫描bean的时候会扫描方法上是否包含@Transactional注解,如果包含,spring会为这个bean动态地生成一个子类(即
代理类
,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用之前就会启动transaction
。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean
,所以就不会启动transaction,我们看到的现象就是@Transactional注解无效
。
在上面的例子中,
completeOrder()
和sendNotification()
都被标注了@Transactional
。但是当completeOrder()
内部调用sendNotification()
时,由于这是一种内部调用,实际调用的是OrderService
类的内部方法,而没有经过 Spring AOP 生成的代理对象,因此sendNotification()
中的事务配置不会被拦截和处理。这样一来,整个调用实际上只是在completeOrder()
开启的事务中执行,导致sendNotification()
的事务配置失效。
将内部方法拆分到另外一个 Service 类中,可以确保调用时通过 Spring 容器管理的代理对象进行,从而使 @Transactional
注解生效。示例如下:
@Service
public class OrderService {
@Autowired
private NotificationService notificationService;
@Transactional
public void completeOrder(Long orderId) {
// 1. 更新订单状态
updateOrderStatus(orderId, "COMPLETED");
// 2. 调用另外一个 Service 的通知方法,此调用会走代理,从而生效事务
notificationService.sendNotification(orderId);
}
private void updateOrderStatus(Long orderId, String status) {
// 数据库操作,更新订单状态
}
}
@Service
public class NotificationService {
@Transactional
public void sendNotification(Long orderId) {
// 执行发送通知的业务逻辑
}
}
优点:
这种方法是在当前类中通过自动注入自身(也就是注入接口或当前类的代理对象),然后通过这个注入的实例来调用事务方法,从而保证调用是经过Spring代理的。例如:
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentMapper studentMapper;
// 自己注入自己(代理对象)
@Autowired
private StudentService studentService;
@Override
public void insertStudent(){
// 通过代理对象调用事务方法
studentService.insert();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void insert() {
// 业务逻辑
}
}
可能有人会担心这样会有循环依赖的问题,事实上,spring通过三级缓存解决了循环依赖的问题,所以上面的写法不会有循环依赖问题。
但是!!!,这不代表spring永远没有循环依赖的问题(@Async导致循环依赖了解下)
如果仍然出现循环依赖问题可采用其他方法或者使用
@Lazy
注解解决
这种方法是通过实现ApplicationContextAware
接口,获取全局ApplicationContext
,然后在调用时从上下文中获取当前的代理bean。例如:
@Component
public class SpringBeanUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringBeanUtil.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentMapper studentMapper;
@Override
public void insertStudent(){
// 从Spring上下文中获取当前代理对象来调用事务方法
StudentService bean = SpringBeanUtil.getBean(StudentService.class);
bean.insert();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void insert() {
// 业务逻辑
}
}
这种方式通过Spring上下文获取代理bean,确保方法调用走代理逻辑,进而使事务注解生效。
总结来说,这两种方法是我们之前没有使用到的解决方案,它们可以在同类调用时确保事务切面的正确触发,从而避免事务失效问题。
如果出于业务或架构原因不便拆分 Service 类,也可以采用 AopContext
获取当前代理对象来调用内部方法。示例如下:
@Service
public class OrderService {
@Transactional
public void completeOrder(Long orderId) {
updateOrderStatus(orderId, "COMPLETED");
// 使用 AopContext 获取当前代理对象调用 sendNotification
((OrderService) AopContext.currentProxy()).sendNotification(orderId);
}
@Transactional
public void sendNotification(Long orderId) {
// 发送通知逻辑
}
private void updateOrderStatus(Long orderId, String status) {
// 数据库操作,更新订单状态
}
}
注意:
使用这种方式需要在 Spring 配置中启用 exposeProxy
,例如在 XML 配置或 Java Config 中加入:
@EnableAspectJAutoProxy(exposeProxy = true)
这种方法虽然可以解决内部调用不走代理的问题,但相对来说不如拆分 Service 的方式直观,因此在设计时应根据实际情况权衡选择。
exposeProxy = true用于控制AOP框架公开代理,公开后才可以通过AopContext获取到当前代理类。(默认情况下不会公开代理,因为会降低性能)
注意:不能保证这种方式一定有效,使用@Async时,本方式可能失效。
代理对象
调用的方法才能触发事务
增强逻辑。this.sendNotification()
)不会经过代理,从而导致事务注解失效,可能会引发数据不一致或事务边界不明确的问题。通过对上述案例的分析,我们可以更好地理解 Spring 事务的传播行为与内部调用陷阱,进而设计出更加健壮和易于维护的业务逻辑。希望这篇文章能帮助大家在开发过程中避免类似问题,并选择合适的解决方案来保证事务的一致性和完整性。
务方法放到另外一个 Service 类中
2. 自己@Autowired自己
3. 通过Spring上下文获取当前代理对象
4. 使用 AopContext 获取代理对象进行调用(需要开启 exposeProxy 配置)。
通过对上述案例的分析,我们可以更好地理解 Spring 事务的传播行为与内部调用陷阱,进而设计出更加健壮和易于维护的业务逻辑。希望这篇文章能帮助大家在开发过程中避免类似问题,并选择合适的解决方案来保证事务的一致性和完整性。
如有错误欢迎在评论区指正讨论!感谢支持!