Spring事务不生效的问题与循环依赖问题

本文深入探讨了在Spring框架中事务失效的原因及其解决方法,分析了动态代理和AOP在事务管理中的作用,同时解释了循环依赖问题及Spring如何处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章来源:https://www.cnblogs.com/V1haoge/p/9476550.html

一篇不错的讲解Spring事务实现原理和事务不生效的解决方法的文章,感谢作者。

一、提出问题

  不知道你是否遇到过这样的情况,在ssm框架中开发web引用,或者使用springboot开发应用,当我们调用一个带有@Transactional注解的方法执行某项事务操作的时候,有时候会发现事务是不生效的。

  你是否考虑过这是为什么,又该如何来修复事务呢?

二、分析问题

  要想弄明白事务不生效的原因,我们首先要弄明白Spring中事务的实现原理,而Spring中的声明式事务是使用AOP来实现的。

  Spring中AOP又是依靠什么实现的呢?动态代理,在Spring中使用的两种动态代理,一种是java原生提供的JDK动态代理,另一种是第三方提供的CGLIB动态代理,前者基于接口实现,后者基于类实现,明显后者的适用范围更加广泛,但是原生的JDK动态代理却是速度要快很多,两者各有特色。

动态代理的目的就是在应用运行时实时生成代理类,这样我们就能在已有实现的基础上对其进行增强,这其实也就是AOP的目的所在,增强类的功能。

  动态代理生成的代理类拥有原生类的所有公有方法,针对指定方法的调用会转移到代理类的同名方法之上,而在这个方法之内会在调用原生类的同名方法之外进行一些其他的操作,比如日志记录,比如安全检查,比如事务操作等。

  当我们在Controller层直接调用service层的一个带有事务注解的方法时,就会执行以上步骤:生成代理类,调用代理类的同名方法,由代理类实现事务功能,再调用原生类的方法进行逻辑执行。

  上面这种情况是没有问题的,有问题的是我们在service层内部的方法调用本类中的带有事务注解的方法时,该事务注解将失效,我们的调用方式无非就是直接调用或者用this调用,这两种情况效果其实是一样的,都是用当前实例调用。

  结合之前的AOP和动态代理的介绍,我们很容易就能理解这里事务失效的原因:那就是我们调用目标事务方法的时候直接调用的原生的方法,而没有调用代理类中的代理方法,也就是说,我们没有调用进行了事务增强的方法,如此一来事务当然会失效了。

  这么来说,我们需要调用代理类中增强之后的代理方法,才能使事务生效。

三、解决问题

  那么我们要如何来修复呢?其实很简单,只要我们不使用this调用即可。this代表的是当前实例,在spring中一般就是单例实例,自己调用自己的方法,事务注解等于摆设。如果我们更改调用方式,在当前类中注入自身单例实例,使用注入的实例来调用该方法,即可使事务生效。

  为什么呢?一般我们的SSM架构中的Service层都是有接口和实现类的,既然存在接口,那么这里使用的必然是JDK动态代理来生成代理类。当我们将当前类的单例实例注入到自身之后,使用这个注入的实例来调用接口中的方法时,如果存在@Transactional之类的AOP增强注解存在,那么就是生成代理类来实现功能增强。(在Springboot中开发的时候我们习惯去掉接口开发,那么代理类就是使用CGLIB动态代理生成的)。

  这样也就要求我们的事务方法需要先在接口中声明,然后在实现类中实现逻辑,并添加事务注解。

  这种方式适用于解决在Service中调用Service中的事务方法时事务失效的问题。这么想想之前从Controller调用Service的时候也是通过注入的Service单例实例来调用的,这也侧面证明我们提供的方法时有效的。

  还有几种解决方案:

    一种就是Spring基础系列-AOP源码分析中的源码6里面所说的通过暴露AOP代理的方式实现。

    一种是将事务注解添加到类上。

    再一种就是就是将被调用的事务方法,放到另一个类中再进行调用。

    这里再添加一种方法:使当前类实现BeanFactoryAware接口,并实现setBeanFactory方法,添加BeanFactory字段,然后通过beanFactory的getBean方法获取当前类的Bean实例来调用目标事务方法,即可实现嵌套之类的事务调用。

四、问题引申

4.1 引申问题:循环依赖

  至于由此引发的另一个问题:当我们在当前类中注入当前类的实例后,在创建这个类的实例的时候是需要注入这个类的实例的,但是这时候这个类有没有创建完成,这该怎么办呢???

  这就是Spring中著名的循环依赖问题。

  更明显的样例是在A中依赖B,B中又依赖A的情况,依赖相互彼此,那么会不会导致两个实例都创建失败呢?

4.2 循环依赖的解决方案

  有必要简单说下Spring中针对这个问题的解决方案。为什么是简单介绍呢,因为我也只是简单理解,但是这种简单理解更加适用于不明白的朋友,不至于一来就懵逼。

  我们都知道在Spring中Bean有多种生命周期范围,主要就是单例和原型(当然还有request、Session等范围),单例表示在整个应用上下文中只会存在一个Bean实例,而原型正好相反,可以存在多个Bean实例,每次调用getBean的时候都会新建一个新的bean实例。

  我们要强调,在Spring中原型范围的Bean实例如果发生循环依赖,只有一种下场:抛异常。

  而针对单例bean,Spring内部提供了一种有效的提前暴露的机制解决了循环依赖的问题。当然这里仅仅解决的是使用setter方式实现依赖注入的情况,如果是使用构造器依赖注入的情况还是那种下场:抛异常。

  抛异常代表,Spring无能力解决此问题,程序出错。

  为什么呢?难道Spring不想解决吗?肯定不是,而是无能为力罢了。

  我们先简单了解下setter方式实现依赖注入的单例Bean的循环依赖的解决方法:

    先介绍下Spring中的那几个缓存池:

      singletonObjects:单例缓存池,用于保存创建完成的单例Bean,是Map,凡是创建完毕的Bean实例全部保存在该缓存池中,不存在循环依赖的Bean会直接在创建完之后保存到该缓存中,而存在循环依赖的bean则会在其创建完成后由earlySingletonObjects转移到此缓存中。

      singletonFactories:单例工厂缓存池,用于保存提前暴露的ObjectFactory,是Map。

      earlySingletonObjects:早期单例缓存池,用于保存尚未创建完成的用于早期暴露的单例Bean,是Map,它与singletonObjects是互斥的,就是不可能同时保存于两者之中,只能择一而存,保存在该缓存池中的是尚未完成创建,而被注入到其他Bean中的Bean实例,可以说该缓存就是一个中间缓存(或者叫过程缓存),只在当将该BeanName对应的原生Bean(处于创建中池)注入到另一个bean实例中后,将其添加到该缓存中,这个缓存中保存的永远是半成品的bean实例,当Bean实例最终完成创建后会从此缓存中移除,转移到singletonObjects缓存中保存。

      registeredSingletons:已注册的单例缓存池,用于保存已完成创建的Bean实例的beanName,是Set(此缓存未涉及)。

      singletonsCurrentlyInCreation:创建中池,保存处于创建中的单例bean的BeanName,是Set,在这个bean实例开始创建时添加到池中,而来Bean实例创建完成之后从池中移除。

    当存在循环依赖的情况时,比如之前的情况:A依赖B,B又依赖A的情况,这种情况下,首先要创建A实例,将其beanName添加到singletonsCurrentlyInCreation池,然后调用A的构造器创建A的原生实例,并将其ObjectFactory添加到singletonFactories缓存中,然后处理依赖注入(B实例),发现B实例不存在且也不在singletonsCurrentlyInCreation池中,表示Bean实例尚未进行创建,那么下一步开始创建B实例,将其beanName添加到singletonsCurrentlyInCreation池,然后调用B的构造器创建A的原生实例,并将其ObjectFactory添加到singletonFactories缓存中,再然后处理依赖注入(A实例),发现A实例尚未创建完成,但在singletonsCurrentlyInCreation池中发现了A实例的beanName,说明A实例正处于创建中,这时表示出现循环依赖,Spring会将singletonFactories缓存中获取对应A的beanName的ObjectFactory中getObject方法返回的Bean实例注入到B中,来完成B实例的创建步骤,同时也会将A的Bean实例添加到earlySingletonObjects缓存中,表示A实例是一个提前暴露的Bean实例,B实例创建完毕之后需要将B的原生实例从singletonFactories缓存中移除,并将完整实例添加到SingletonObjects缓存中(当然earlySingletonObjects中也不能存在),并且将其beanName从singletonsCurrentlyInCreation池中移除(表示B实例完全创建完毕)。然后将B实例注入到A实例中来完成A实例的创建,最后同样将A的原生实例从earlySingletonObjects中移除,完整实例添加到SingletonObjects中,并将A的beanName从创建中池中移除。到此完成A和B两个单例实例的创建。

  了解了上面所述的解决方案之后,我们可以明白针对构造器实现依赖注入的Bean发生循环依赖的情况下为什么无法解决。那就是因为,之前提前暴露的前提是创建好原生的Bean实例,原生的Bean实例就是依靠构造器创建的,如果在构造器创建Bean的时候就需要注入依赖,而依赖又正处于创建中的话,由于无法暴露ObjectFactory,而无法解决循环依赖问题。

  另外原型bean的情况,Spring根本就不会对原型的Bean添加缓存,因为添加缓存的目的是为了保证单例Bean的唯一性,但是对于原型,就不能缓存了,如果从缓存获取的Bean实例,那还是原型模式吗?不存在缓存当然也就无法实现上面描述的那一系列操作,也就无法解决循环依赖的问题了。

五、总结

  Spring中的事务问题归结为注入问题,循环依赖问题也是注入问题,有关注入的问题以后再讨论。

  Spring之中所有的增强都是依靠AOP实现的,而AOP又是依靠动态代理实现的,JDK的动态代理依靠反射技术实现,而CGLIB动态代理依靠字节码技术实现。

<think>嗯,用户问为什么Spring不推荐使用循环依赖。首先,我需要回忆一下Spring框架中循环依赖的处理机制。Spring在启动时会创建Bean,如果Bean A依赖Bean B,而Bean B又反过来依赖Bean A,这时候如果没有正确的处理,就会导致循环依赖问题。 用户之前已经问过如何检测循环依赖,现在可能遇到了相关问题,想了解更深层次的原因。Spring官方文档里提到过循环依赖的风险,所以需要解释清楚为什么不推荐使用。可能的原因包括启动失败、运行时问题、设计缺陷等。 首先,启动时的问题,如果循环依赖无法解决,应用就无法启动,抛出BeanCurrentlyInCreationException。这时候用户需要调整Bean的依赖关系。其次,即使Spring通过三级缓存解决了某些循环依赖(比如构造器注入无法解决,但setter或字段注入可以),但这样可能导致Bean处于未完全初始化的状态,引发不可预见的错误。 然后是设计上的问题循环依赖通常意味着代码结构不合理,职责不清晰。比如两个Service互相依赖,可能说明这两个类承担了过多的职责,应该拆分或引入第三个类来处理公共逻辑。 还有测试和维护的困难。循环依赖会让单元测试变得复杂,因为mock依赖的时候可能需要更多的设置。维护时修改一个类可能会影响到其他依赖它的类,增加了代码的脆弱性。 另外,用户可能在实际项目中遇到了循环依赖导致的问题,比如内存泄漏或者性能下降,因为Bean的创建和初始化过程变得复杂。Spring虽然提供了allow-circular-references的配置选项,但官方不推荐使用,因为这可能掩盖设计问题,而不是真正解决问题。 需要举例说明,比如两个Service相互注入的情况,这样会导致紧耦合,难以单独测试。正确的做法可能是通过接口、事件驱动或者引入第三方Bean来解耦。 最后,要总结Spring不推荐的原因,强调良好的设计实践,避免循环依赖带来的隐患。可能还需要提到一些解决方案,比如使用@Lazy延迟加载,或者重新设计代码结构。</think>在Spring框架中,循环依赖(Circular Dependency)是指两个或多个Bean相互直接或间接引用,形成依赖闭环。尽管Spring通过**三级缓存机制**(Singleton Bean的提前暴露)部分支持了循环依赖(仅限字段注入或Setter注入),但官方仍明确不推荐使用循环依赖,主要原因如下: --- ### 一、破坏Bean生命周期的完整性 1. **初始化顺序不可控** 循环依赖会导致Bean在未完全初始化时就被注入其他Bean中。例如: ```java @Service public class ServiceA { @Autowired private ServiceB serviceB; // ServiceB可能尚未完成初始化 } @Service public class ServiceB { @Autowired private ServiceA serviceA; // ServiceA可能处于半初始化状态 } ``` 此时若调用半初始化Bean的方法,可能引发空指针或逻辑错误。 2. **构造器注入无法解决循环依赖** Spring无法通过构造器注入解决循环依赖(直接抛异常),因为Bean的实例化和属性填充阶段被强制分离: ```java // 以下代码启动必定失败 public class ServiceA { private final ServiceB serviceB; public ServiceA(ServiceB serviceB) { ... } // 构造器依赖 } public class ServiceB { private final ServiceA serviceA; public ServiceB(ServiceA serviceA) { ... } // 构造器依赖 } ``` --- ### 二、隐含设计缺陷 1. **违反单一职责原则** 循环依赖通常意味着代码存在**高耦合**,例如: - `UserService` 依赖 `OrderService`,同时 `OrderService` 又依赖 `UserService` - 表明两个类可能承担了本应由第三方模块处理的职责(如应拆分出 `UserOrderValidator`)。 2. **代码可维护性下降** 修改一个Bean可能引发连锁问题,调试和测试成本显著增加。 --- ### 三、潜在运行时风险 1. **代理对象异常** 若循环依赖的Bean涉及AOP代理(如事务管理、@Async),可能因代理对象生成时机不当导致以下问题: - 方法拦截失效 - 事务不生效 - 异步调用阻塞 2. **内存泄漏风险** 循环依赖可能导致Bean无法被垃圾回收(GC),尤其在非单例作用域(如`prototype`)下更易发生。 --- ### 四、Spring官方态度 - **Spring Framework文档**明确指出: > *"Circular dependencies are a code smell. Refactor your code to break the cycle."* (循环依赖是代码异味,应通过重构消除闭环) - **Spring Boot 2.6+默认禁止循环依赖**: 若未显式配置`spring.main.allow-circular-references=true`,启动时将直接报错。 --- ### 五、替代解决方案 | 场景 | 推荐方案 | |------|----------| | 服务层互相调用 | 提取公共逻辑到工具类或领域服务 | | 需要双向通信 | 使用事件驱动模式(`ApplicationEvent`) | | 必须跨模块依赖 | 通过接口隔离(DIP依赖倒置原则) | --- ### 总结 Spring容忍循环依赖的有限支持是为了框架灵活性,但本质上**循环依赖是设计缺陷的体现**,会引发代码腐化、维护困难和运行时隐患。建议通过**分层设计**、**依赖倒置**(如面向接口编程)和**职责拆分**彻底消除循环依赖
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值