当前位置: 首页 > Web前端 > JavaScript

springboot声明式事务不生效——警惕springboot声明式事务的陷阱

时间:2023-03-27 15:58:54 JavaScript

文章目录警惕@Transactional的坑标记@Transactional的私有方法——该类中的方法不调用该类中的@Transactional方法——在不生效的代码中手动处理了异常——即使抛出异常也不一定生效。Spring已经实现了JavaTransactionAPI(JTA)、JDBC、Hibernate、JavaPersistenceAPI(JPA)编程模型等事务API的一致性,Spring的声明式事务功能提供了极其方便的事务配置方式。借助SpringBoot的自动配置,大部分SpringBoot项目只需要在方法上标注@Transactional注解,即可一键打开方法。交易配置。在使用@Transactional注解开启声明式事务时,最先被忽视的问题就是事务可能不会生效。在使用@Transactional注解开启声明式事务时,不仅需要注意其是否有效,还有很多其他的陷阱,在此罗列以示警惕。警惕@Transactional的坑标记@Transactional的私有方法——无效当Controller类调用UserService的createUserPrivate方法时,由于createUserPrivate方法是私有的,即使加上@Transactional,声明式事务也不会生效.除非有特殊配置(比如使用AspectJ静态编织实现AOP),否则只有定义在public方法上的@Transactional才能生效。原因是Spring默认通过动态代理实现AOP,增强了目标方法。私有方法不能代理,Spring自然不能动态增强事务处理逻辑。@Service@Slf4jpublicclassUserService{@AutowiredprivateUserRepositoryuserRepository;//标记@Transactional的私有方法@TransactionalprivatevoidcreateUserPrivate(UserEntityentity){userRepository.save(entity);if(entity.getName().contains("test"))thrownewRuntimeException("invalidusername!");}}本类中的方法调用本类中的@Transactional方法——如果不生效,必须通过代理类从外部调用目标方法才能生效。也就是说,声明式事务只有被@Transactional方法标记的类以外的其他类调用时才会生效。publicintcreateUserWrong2(Stringname){this.createUserPublic(newUserEntity(name));returnuserRepository.findByName(name).size();}//标记为@Transactionalpublicmethod@TransactionalpublicvoidcreateUserPublic(UserEntityentity){userRepository.save(entity);if(entity.getName().contains("test"))thrownewRuntimeException("invalidusername!");}但是!下面的自调用方法可以生效,但是不符合分层规范:自注入一个self,然后调用自己的类,实际开发中最好不要这样做。.以下是标准调用方法://@GetMapping("right")publicintright2(@RequestParam("name")Stringname){try{userService.createUserPublic(newUserEntity(name));}catch(Exceptionex){log.error("创建用户失败,因为{}",ex.getMessage());}returnuserService.getUserCount(name);}//@TransactionalpublicvoidcreateUserPublic(UserEntityentity){userRepository.saveinService(entity);if(entity.getName().contains("test"))thrownewRuntimeException("invalidusername!");}下面我们来回顾一下这种自调用、自调用和in-调用UserService三种实现方式的区别在Controller中:通过这个自调用,是没有机会去Spring代理类的;后两种改进方案调用Spring注入的UserService,通过代理调用可以动态增强createUserPublic方法。异常是在代码中手动处理的——它不会生效。只有当异常传播到标有@Transactional注解的方法时,事务才能被回滚。Spring的TransactionAspectSupport中有一个invokeWithinTransaction方法,就是处理事务的逻辑。可见,只有捕获到异常,才能进行后续的事务处理。以下是spring源代码:try{//这是一个aroundadvice:调用链中的下一个拦截器。//这通常会导致调用目标对象。retVal=invocation.proceedWithInvocation();}catch(Throwableex){//目标调用异常completeTransactionAfterThrowing(txInfo,ex);throwex;}finally{cleanupTransactionInfo(txInfo);}手动处理了异常,没有抛出,如下@TransactionalpublicintcreateUserWrong1(Stringname){try{this.createUserPrivate(newUserEntity(name));抛出新的RuntimeException(“错误”);}catch(Exceptionex){log.error("创建用户失败,因为{}",ex.getMessage());}}}解决方案:手动回滚加上TransactionAspectSupport.currentTransactionStatus()。setRollbackOnly();@TransactionalpublicvoidcreateUserRight1(Stringname){try{userRepository.save(newUserEntity(name));抛出新的RuntimeException(“错误”);}catch(Exceptionex){log.error("创建用户失败",ex);事务方面支持。currentTransactionStatus().setRollbackOnly();}}即使抛出异常,也不一定生效。默认情况下,Spring会在RuntimeException(未检查异常)或Error发生时回滚事务打开Spring的DefaultTransactionAttribute类可以看到如下代码块,可以找到相关的证据,也可以通过注释看到Spring这样做的原因,大概意思是checked异常一般是业务异常,或者类似给另一个方法返回值,如果出现这样的异常,业务可能还是完成了,所以不会主动回滚;而Error或RuntimeException表示意外结果,应该回滚:/***默认行为与EJB一样:在未经检查的异常上回滚*({@linkRuntimeException}),假设意外结果超出任何*业务规则。此外,我们还尝试回滚{@linkError},这显然也是一个意想不到的结果。相比之下,检查异常*被视为业务异常,因此是事务性业务方法的常规预期结果,即一种替代返回值,*仍然允许定期完成资源操作。*

这在很大程度上与TransactionTemplate的默认行为一致*除了TransactionTemplate还会回滚未声明的已检查异常*(极端情况)。对于声明式交易,我们期望检查异常ons被*故意声明为业务异常,导致默认提交。*@seeorg.springframework.transaction.support.TransactionTemplate#execute*/@OverridepublicbooleanrollbackOn(Throwableex){return(exinstanceofRuntimeException||exinstanceofError);}以下代码中的事务不会生效:@Service@Slf4jpublicclassUserService{@AutowiredprivateUserRepositoryuserRepository;//即使发生检查异常,事务也不能回滚@TransactionalpublicvoidcreateUserWrong2(Stringname)throwsIOException{userRepository.save(newUserEntity(name));其他任务();}//因为文件不存在,所以必须抛出IOExceptionprivatevoidotherTask()throwsIOException{Files.readAllLines(Paths.get("file-that-not-exist"));}}注解声明的解决方案,希望在遇到所有Exception时回滚事务(突破默认不回滚checkedexception的限制):添加@Transactional注解rollbackFor和noRollbackFor属性覆盖它们的默认设置@Transactional(rollbackFor=Exception.class)publicvoidcreateUserRight2(Stringname)throwsIOException{userRepository.save(newUserEntity(name));其他任务();}