文章目录
如何解决循环依赖的问题
解决循环依赖的关键在于重构代码、合理使用依赖注入方式以及利用Spring提供的机制(如@Lazy、@PostConstruct、三级缓存等)。设计良好的应用通常不会产生复杂的循环依赖问题,因此在代码结构上进行适当的优化和调整,是解决循环依赖的根本方法。
解决循环依赖问题有多种方法,具体的策略取决于代码结构和应用场景。以下是几种常见的方法:
1. 使用构造器注入的依赖重构
如果两个或多个Bean之间通过构造器注入形成了循环依赖,最简单的解决方法是通过依赖重构来打破循环。可以尝试以下方法:
重构设计:重新设计类之间的依赖关系,减少不必要的依赖。
拆分Bean:将一个复杂的Bean拆分为多个小的Bean,以减少直接的循环依赖。
引入第三方Bean:引入一个中间层或第三方Bean来解耦两个直接依赖的Bean。
2. 使用Setter注入代替构造器注入
Spring容器可以通过三级缓存机制解决Setter注入或字段注入的循环依赖问题。如果当前使用的是构造器注入,且构造器注入导致了循环依赖,可以考虑将其改为Setter注入。
示例:
@Component
public class A {
private B b;
@Autowired
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
@Autowired
public void setA(A a) {
this.a = a;
}
}
通过将构造器注入改为Setter注入,Spring可以使用三级缓存机制来解决循环依赖问题。
3. 使用@Lazy注解
@Lazy注解可以推迟Bean的初始化,直到它第一次被需要时才会创建。这样可以打破循环依赖,因为某些Bean可能在启动时并不需要立即创建。
示例:
@Component
public class A {
private B b;
@Autowired
public A(@Lazy B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
@Autowired
public B(@Lazy A a) {
this.a = a;
}
}
通过使用@Lazy注解,Spring会在需要时才创建依赖的Bean,从而避免循环依赖。
4. 使用ApplicationContext获取Bean
在某些情况下,可以通过Spring的ApplicationContext手动获取Bean,而不是通过依赖注入来避免循环依赖。这种方法适合于少数特殊场景,但应谨慎使用,以免破坏代码的可测试性和灵活性。
示例:
@Component
public class A {
private B b;
@Autowired
private ApplicationContext context;
public void init() {
this.b = context.getBean(B.class);
}
}
@Component
public class B {
private A a;
@Autowired
private ApplicationContext context;
public void init() {
this.a = context.getBean(A.class);
}
}
5. 使用接口和事件机制
通过使用接口或事件机制,可以降低类之间的耦合度,从而避免循环依赖。例如,将依赖关系替换为事件监听器或观察者模式,通过事件的发布和监听来间接实现依赖关系。
6. 依赖倒置原则(DIP)
依赖倒置原则是SOLID设计原则中的一条,它建议依赖于抽象(接口或抽象类)而不是具体实现。通过依赖倒置,可以设计出更加松耦合的系统,减少循环依赖的可能性。
示例:
public interface IService {
void performTask();
}
@Component
public class A implements IService {
@Override
public void performTask() {
// Implementation
}
}
@Component
public class B {
private final IService service;
@Autowired
public B(IService service) {
this.service = service;
}
}
7. 使用@PostConstruct注解
在某些场景下,可以推迟依赖的设置,直到Bean完全初始化之后。可以通过@PostConstruct注解将依赖关系的初始化放在Bean初始化完成之后执行。
示例:
@Component
public class A {
private B b;
@Autowired
public void setB(B b) {
this.b = b;
}
@PostConstruct
public void init() {
// Initialization code that depends on B
}
}
8. 通过外部配置或工厂方法注入
在Spring中,配置类或工厂方法也可以用来管理复杂的依赖关系,特别是在依赖关系不适合通过直接注入来解决时。
@Configuration
public class AppConfig {
@Bean
public A a(B b) {
return new A(b);
}
@Bean
public B b() {
return new B();
}
}
通过在配置类中显式地配置依赖,可以更好地控制Bean的初始化顺序。
Spring是如何解决循环依赖的问题的
Spring 通过三级缓存机制和反射技术,巧妙地解决了通过 setter 注入和字段注入方式引起的循环依赖问题。然而,对于构造函数注入的循环依赖,Spring 无法自动解决,开发者需要调整设计,避免直接的构造函数依赖。理解这些机制有助于开发者更好地设计和调试 Spring 应用
通过将构造器注入改为Setter注入,Spring可以使用三级缓存机制来解决循环依赖问题
Spring提供了多种解决方案:
- 构造函数注入:通过在构造函数参数中使用依赖注入,而不是使用字段注入或setter方法注入,可以避免循环依赖。
- @Autowired注解配合setter方法:将@Autowired注解放在setter方法上,而不是字段上,也可以解决循环依赖问题。
- 使用@PostConstruct注解:使用@PostConstruct注解在Bean创建完成后执行一些初始化操作,可以解决某些循环依赖问题。
- 使用代理对象:对于循环依赖中的其中一个Bean,可以使用代理对象来解决。Spring AOP可以帮助生成代理对象,从而解决循环依赖。
Spring在处理Bean的循环依赖(Circular Dependency)时,通过使用三级缓存机制来解决。以下是Spring如何解决循环依赖问题的详细说明: - 什么是循环依赖
循环依赖是指两个或多个Bean之间相互依赖,导致在创建这些Bean时出现死锁。例如:
● Bean A 依赖 Bean B
● Bean B 依赖 Bean A
在这种情况下,如果没有特殊处理,Spring在创建这些Bean时会出现死锁,无法正常完成Bean的初始化。 - Spring的三级缓存机制
Spring使用三级缓存机制来解决循环依赖问题。三级缓存指的是:
一级缓存:singletonObjects,存储已经完全初始化好的单例Bean。
二级缓存:earlySingletonObjects,存储提前曝光的Bean实例(尚未完全初始化,但可以被其他Bean引用)。
三级缓存:singletonFactories,存储的是一个ObjectFactory,用于生成Bean实例的代理对象。
三级缓存的工作流程:
创建Bean实例:当Spring容器开始创建一个Bean(比如Bean A)时,它会先尝试从一级缓存中获取。如果获取不到,则会创建一个Bean的半成品实例,并将这个实例的ObjectFactory放入三级缓存。
解决依赖注入:在Bean A的依赖注入过程中,Spring发现Bean A依赖Bean B,它会尝试去创建Bean B。如果Bean B也依赖Bean A,那么此时Spring会从三级缓存中获取Bean A的ObjectFactory,并通过这个工厂方法获取Bean A的实例(此时Bean A尚未完全初始化)。
完成Bean的初始化:在依赖注入完成后,Spring会进行Bean A和Bean B的完全初始化过程。当初始化完成时,这些Bean将被放入一级缓存,并从三级缓存和二级缓存中移除。 - 具体示例:
假设有两个相互依赖的Bean A和Bean B:
@Component
public class A {
private B b;
@Autowired
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
@Autowired
public void setA(A a) {
this.a = a;
}
}
处理过程:
创建A实例:
Spring检测到Bean A依赖于Bean B,因此开始创建Bean A的实例,并将其ObjectFactory放入三级缓存。
创建B实例:
在创建Bean A的过程中,Spring检测到它依赖于Bean B,因此开始创建Bean B的实例。
解决循环依赖:
在创建Bean B时,Spring发现Bean B依赖于Bean A。此时,Spring会从三级缓存中获取Bean A的ObjectFactory,通过这个工厂方法获取Bean A的引用,并将这个引用注入到Bean B中。
完成初始化:
Bean B完成初始化后,Spring会继续完成Bean A的初始化。
最终,Bean A和Bean B都被完全初始化,并存放在一级缓存中,循环依赖问题得以解决。
4. 局限性
构造器注入的循环依赖:Spring只能解决通过Setter注入或字段注入的循环依赖问题。如果两个Bean之间通过构造器注入形成循环依赖,Spring无法处理这种情况,因为构造器注入要求在创建Bean实例时立即提供依赖项。
多线程环境下的复杂性:在多线程环境中,如果多个线程同时创建循环依赖的Bean,可能会增加解决循环依赖的复杂性。
5. 结论
Spring通过三级缓存机制有效地解决了大多数情况下的Bean循环依赖问题,确保依赖注入过程中的顺利进行。这种机制允许Spring在Bean未完全初始化时就能提前暴露Bean的引用,从而打破循环依赖带来的死锁局面。
Spring本身只能解决单实例存在的循环引用问题,但是存在以下四种情况需要人为干预:
spring循环依赖无法解决的场景
1.多例Bean通过setter注入的情况,不能解决循环依赖问题
2.构造器注入的Bean的情况,不能解决循环依赖问题
3.单例的代理Bean通过Setter注入的情况,不能解决循环依赖问题
4.设置了@DependsOn的Bean的情况,不能解决循环依赖问题
多实例的Setter注入导致的循环依赖,需要把Bean改成单例。
构造器注入导致的循环依赖,可以通过@Lazy注解
DependsOn导致的循环依赖,找到注解循环依赖的地方,迫使它不循环依赖。
单例的代理对象Setter注入导致的循环依赖,可以使用@Lazy注解,或者使用
在实际开发中,出现循环依赖的根本原因还是在代码设计的时候,因为模块的耦合度较高,
依赖关系复杂导致的,我们应该尽可能的从系统设计角度去考虑模块之间的依赖关系,避免循环依赖的问题。
Spring的三级缓存
三级缓存,提前暴露对象,aop
总:什么是循环依赖问题,A依赖B,B依赖A
分:先说明bean的创建过程,实例化,初始化(填充属性)
1.先创建A对象,实例化A对象,此时A对象中的b属性为空
2.从容器中查找B对象,如果找到了,直接赋值不存在循环依赖问题,找不到直接创建B对象
3.实例化B对象,此时B对象中的a属性为空,填充属性a
4.从容器中查找A对象,找不到,直接创建
此时,会发现A对象是存在的,只不过此时的A对象不是一个完整的状态,只完成了实例化但是未完成初始化,如果在程序调用过程中,拥有了某个对象的引用,能否在后期给他完成赋值操作,可以优先把非完整状态的对象优先赋值,等待后续操作来完成赋值,相当于提前暴露了某个不完整对象的引用,所以解决问题的核心在于实例化和初始化分开操作,这也是解决循环依赖问题的关键,当所有的对象都完成实例化和初始化操作之后,还要把完整对象放到容器中,此时在容器中存在对象的几个状态,完成实例化但未完成初始化,完整状态,因为都在容器中,所以要使用不同的map结构来进行存储,此时就有了一级缓存和二级缓存,如果一级缓存中有了,那么二级缓存中就不会存在同名的对象,因为他们的查找顺序是1,2,3这样的方式来查找的。一级缓存中放的是完整对象,二级缓存中放的是非完整对象
为什么需要三级缓存?三级缓存的value类型是ObjectFactory是一个函数接口,存在的意义是保证在整个容器的运行过程中同名的bean对象只能有一个。
第一级缓存存放完全初始化好的Bean,这个Bean可以直接使用了
单例的Bean被创建后就被存放在一级缓存中,其实就是一个Map,只要这个单例bean被创建后,以后再次获取直接从Map中获取
第二级缓存存放原始的Bean对象,也就是说Bean里面的属性还没有进行赋值
第三级缓存存放Bean工厂对象,用来生成原始Bean对象并放入到二级缓存中
singletonFactories,三级缓存,主要存放半成品单例bean的被包装成的ObjectFactory,后续可以解决循环依赖,
单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖,能处理。
一级缓存放成品对象,二级缓存存放半成品对象
Spring本身也考虑到了这方面的问题,所以它设计了三级缓存来解决部分循环依赖的问题。
所谓三级缓存,其实就是用来存放不同类型的Bean。
第一级缓存存放完全初始化好的Bean,这个Bean可以直接使用了
第二级缓存存放原始的Bean对象,也就是说Bean里面的属性还没有进行赋值
第三级缓存存放Bean工厂对象,用来生成原始Bean对象并放入到二级缓存中
假设BeanA和BeanB存在循环依赖,那么在三级缓存的设计下,我画了这样一个图来描述工作原理。
初始化BeanA,先把BeanA实例化,然后把BeanA包装成ObjectFactory对象保存到三级缓存中。
接着BeanA开始对属性BeanB进行依赖注入,于是开始初始化BeanB,同样做两件事,创建BeanB实例,以及加入到三级缓存。
然后,BeanB也开始进行依赖注入,在三级缓存中找到了BeanA,于是完成BeanA的依赖注入BeanB初始化成功以后保存到一级缓存,于是BeanA可以成功拿到BeanB的实例,从而完成正常的依赖注入。
整个流程看起来很复杂,但是它的核心思想就是把Bean的实例化和Bean中属性的依赖注入这两个过程分离出来
如果整个应用程序中不涉及Aop的存在,那么二级缓存中足以解决循环依赖的问题,
如果aop中存在了循环依赖,那么就必须要使用三级缓存才能解决,因为找不到代理对象了,需要用到三级缓存中的lambda表达式生成代理对象

1万+

被折叠的 条评论
为什么被折叠?



