Spring 事务不生效?可能是内部调用惹的祸!

同一个类里调用事务方法居然无效?Spring 事务失效原因全解析!

在实际开发中,Spring 的事务管理为我们提供了非常便捷的控制手段。然而,由于其基于 AOP 代理机制的实现方式,有时候在同一个
Service 类中调用另一个带有 @Transactional
注解的方法时,会遇到事务不生效的问题。本文将结合实例与案例,详细解析这一问题的原理、常见错误示例以及正确的解决方案。

一、Spring 事务管理原理概述

Spring 通过 AOP(面向切面编程)实现事务管理。当容器扫描到方法上有 @Transactional 注解时,会为目标对象生成一个代理类。在代理类中,在目标方法调用前后分别执行事务的开启、提交或回滚等操作。Spring 事务管理的核心机制包括:

  • 事务传播行为
    Spring 定义了多种事务传播属性(如 REQUIREDREQUIRES_NEWNESTED 等),用于在多个事务方法相互调用时决定事务如何传播。

    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)
    Spring 事务不生效?可能是内部调用惹的祸!_第1张图片

  • AOP 代理机制
    事务管理的生效依赖于对外部方法调用的拦截,当一个方法被代理对象调用时,会先执行事务增强逻辑。但如果在同一个类内部直接调用,调用不会经过代理,事务注解将不会生效。

二、内部方法调用导致事务失效的问题

错误示例

设想在一个订单 Service 中,有两个方法:

  • completeOrder():用于更新订单状态并标记订单完成(带有 @Transactional 注解);
  • sendNotification():用于发送订单完成通知(同样带有 @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 类(推荐)

将内部方法拆分到另外一个 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 代理,事务配置能正常生效;
  • 各业务模块职责更清晰,利于解耦和维护。

方案二:自己@Autowired自己

这种方法是在当前类中通过自动注入自身(也就是注入接口或当前类的代理对象),然后通过这个注入的实例来调用事务方法,从而保证调用是经过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注解解决

方案三:通过Spring上下文获取当前代理对象(推荐)

这种方法是通过实现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,确保方法调用走代理逻辑,进而使事务注解生效。

总结来说,这两种方法是我们之前没有使用到的解决方案,它们可以在同类调用时确保事务切面的正确触发,从而避免事务失效问题。

方案四:使用 AopContext 获取当前代理对象

如果出于业务或架构原因不便拆分 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时,本方式可能失效。

四、总结

  • 核心原理:
    Spring 事务管理依赖于 AOP 代理,只有通过代理对象调用的方法才能触发事务增强逻辑。
  • 常见问题:
    同一 Service 内部方法调用(如 this.sendNotification())不会经过代理,从而导致事务注解失效,可能会引发数据不一致或事务边界不明确的问题。
  • 解决方案:
    1. 拆分 Service,将内部事务方法放到另外一个 Service 类中
    2. 自己@Autowired自己
    3. 通过Spring上下文获取当前代理对象
    4. 使用 AopContext 获取代理对象进行调用(需要开启 exposeProxy 配置)。

通过对上述案例的分析,我们可以更好地理解 Spring 事务的传播行为与内部调用陷阱,进而设计出更加健壮和易于维护的业务逻辑。希望这篇文章能帮助大家在开发过程中避免类似问题,并选择合适的解决方案来保证事务的一致性和完整性。

务方法放到另外一个 Service 类中
2. 自己@Autowired自己
3. 通过Spring上下文获取当前代理对象
4. 使用 AopContext 获取代理对象进行调用(需要开启 exposeProxy 配置)。

通过对上述案例的分析,我们可以更好地理解 Spring 事务的传播行为与内部调用陷阱,进而设计出更加健壮和易于维护的业务逻辑。希望这篇文章能帮助大家在开发过程中避免类似问题,并选择合适的解决方案来保证事务的一致性和完整性。

如有错误欢迎在评论区指正讨论!感谢支持!

你可能感兴趣的:(项目实战,spring,java,数据库)