• springboot声明式事务没有生效——警惕springboot声明式事务的坑


    写在前面

    Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。

    在使用 @Transactional 注解开启声明式事务时, 第一个最容易忽略的问题是,很可能事务并没有生效。

    而使用@Transactional 注解开启声明式事务时,不光需要注意其有没有生效,其他的坑也非常多,在这里列举一下以示警惕。

    警惕@Transactional 的坑

    标记了@Transactional的private方法——不生效

    Controller类调用UserService的createUserPrivate方法时,由于createUserPrivate方法时private的,即使加上了@Transactional,声明式事务也不会生效。

    除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则只有定义在 public 方法上的 @Transactional 才能生效。
    原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。

    @Service
    @Slf4j
    public class UserService {
        @Autowired
        private UserRepository userRepository;
    
        //标记了@Transactional的private方法
        @Transactional
        private void createUserPrivate(UserEntity entity) {
            userRepository.save(entity);
            if (entity.getName().contains("test"))
                throw new RuntimeException("invalid username!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    本类中的方法调用本类中@Transactional方法——不生效

    必须通过代理过的类从外部调用目标方法才能生效。
    也就是说,只有除了标注了@Transactional方法存在的类 的其他类,调用该方法,声明式事务才会生效。

    public int createUserWrong2(String name) {
      this.createUserPublic(new UserEntity(name));
      return userRepository.findByName(name).size();
    }
    
    //标记了@Transactional的public方法
    @Transactional
    public void createUserPublic(UserEntity entity) {
        userRepository.save(entity);
        if (entity.getName().contains("test"))
            throw new RuntimeException("invalid username!");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    但是!以下自己调用自己的方法是可以生效的,但是不符合分层规范:
    自身注入一个自身,然后调用自身的类,实际开发最好不要这样搞。。
    在这里插入图片描述
    或者调用方法的时候,用工具类获取被代理的类:

    // 获取当前被代理的类
    ((UserService)(AopContext.currentProxy())).createUserPublic();
    
    • 1
    • 2

    以下才是标准的调用方式:

    // Controller中
    @GetMapping("right")
    public int right2(@RequestParam("name") String name) {
        try {
            userService.createUserPublic(new UserEntity(name));
        } catch (Exception ex) {
            log.error("create user failed because {}", ex.getMessage());
        }
        return userService.getUserCount(name);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    // Service中
    @Transactional
    public void createUserPublic(UserEntity entity) {
        userRepository.save(entity);
        if (entity.getName().contains("test"))
            throw new RuntimeException("invalid username!");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们再通过一张图来回顾下 this 自调用、通过 self 调用,以及在 Controller 中调用 UserService 三种实现的区别:
    通过 this 自调用,没有机会走到 Spring 的代理类;后两种改进方案调用的是 Spring 注入的 UserService,通过代理调用才有机会对 createUserPublic 方法进行动态增强。
    在这里插入图片描述

    代码中手动处理了异常——不生效

    只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。在 Spring 的 TransactionAspectSupport 里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。可以看到,只有捕获到异常才能进行后续事务处理,以下是spring源码:

    try {
       // This is an around advice: Invoke the next interceptor in the chain.
       // This will normally result in a target object being invoked.
       retVal = invocation.proceedWithInvocation();
    }
    catch (Throwable ex) {
       // target invocation exception
       completeTransactionAfterThrowing(txInfo, ex);
       throw ex;
    }
    finally {
       cleanupTransactionInfo(txInfo);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    手动处理了异常,并没有抛出,以下代码中声明式事务是不会生效的:

    @Service
    @Slf4j
    public class UserService {
        @Autowired
        private UserRepository userRepository;
    
        @Transactional
        public int createUserWrong1(String name) {
            try {
                this.createUserPrivate(new UserEntity(name));
                throw new RuntimeException("error");
            } catch (Exception ex) {
                log.error("create user failed because {}", ex.getMessage());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    解决方案:手动回滚
    加上TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

    
    @Transactional
    public void createUserRight1(String name) {
        try {
            userRepository.save(new UserEntity(name));
            throw new RuntimeException("error");
        } catch (Exception ex) {
            log.error("create user failed", ex);
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    即使抛出异常,也有可能不生效

    默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。

    打开 Spring 的 DefaultTransactionAttribute 类能看到如下代码块,可以发现相关证据,通过注释也能看到 Spring 这么做的原因,大概的意思是受检异常一般是业务异常,或者说是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而 Error 或 RuntimeException 代表了非预期的结果,应该回滚:

    
    /**
     * The default behavior is as with EJB: rollback on unchecked exception
     * ({@link RuntimeException}), assuming an unexpected outcome outside of any
     * business rules. Additionally, we also attempt to rollback on {@link Error} which
     * is clearly an unexpected outcome as well. By contrast, a checked exception is
     * considered a business exception and therefore a regular expected outcome of the
     * transactional business method, i.e. a kind of alternative return value which
     * still allows for regular completion of resource operations.
     * 

    This is largely consistent with TransactionTemplate's default behavior, * except that TransactionTemplate also rolls back on undeclared checked exceptions * (a corner case). For declarative transactions, we expect checked exceptions to be * intentionally declared as business exceptions, leading to a commit by default. * @see org.springframework.transaction.support.TransactionTemplate#execute */ @Override public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    以下代码中事务是不会生效的:

    
    @Service
    @Slf4j
    public class UserService {
        @Autowired
        private UserRepository userRepository;
        
        //即使出了受检异常也无法让事务回滚
        @Transactional
        public void createUserWrong2(String name) throws IOException {
            userRepository.save(new UserEntity(name));
            otherTask();
        }
    
        //因为文件不存在,一定会抛出一个IOException
        private void otherTask() throws IOException {
            Files.readAllLines(Paths.get("file-that-not-exist"));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    解决方案,在注解中声明,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制):
    加上@Transactional 注解的 rollbackFor 和 noRollbackFor 属性来覆盖其默认设置。

    @Transactional(rollbackFor = Exception.class)
    public void createUserRight2(String name) throws IOException {
        userRepository.save(new UserEntity(name));
        otherTask();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    参考资料

    本文参考了朱晔(贝壳金融资深架构师)老师对于声明式事务的讲解。

  • 相关阅读:
    WebGPU-初识各名词概念Adapters与Device
    Windows AD共享权限管理工具
    QT+OSG/osgEarth编译之四十九:osgSim+Qt编译(一套代码、一套框架,跨平台编译,版本:OSG-3.6.5核心库osgSim)
    【机器学习】逻辑回归和线性回归的区别?(面试回答)
    【HTML5】弹性盒子实现导航栏和留言框
    科技云报道:推进工业新质生产力机器人有望成为AI下一个新引擎?
    Java Thread.interrupt()方法具有什么功能呢?
    matlab simulink汽车优化设计遍历法
    帅地这些年看过的书
    竞赛选题 深度学习的视频多目标跟踪实现
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/126378965