spring中AB类构造器存在循环依赖咋办

Posted 洪宏鸿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring中AB类构造器存在循环依赖咋办相关的知识,希望对你有一定的参考价值。

构造器存在循环依赖🚌

A 和 B 的构造器存在循环依赖

当 A 和 B 的构造器存在循环依赖时,SmartInstantiationAwareBeanPostProcessor 无法解决这种循环依赖问题。在这种情况下,Spring 会抛出一个异常,提示用户存在循环依赖。这是因为 Spring 采用的是构造器注入,而构造器注入无法像属性注入那样提前暴露引用来解决循环依赖。

具体过程如下:

  1. Spring 容器开始实例化 A,发现 A 有一个构造器 A(B b)。

  2. 容器尝试实例化 B 以满足 A 的依赖。然后发现 B 也有一个构造器 B(A a)。

  3. 容器尝试实例化 A 以满足 B 的依赖,此时发现 A 已经在实例化过程中了。这表明存在循环依赖。

  4. Spring 无法解决这种构造器循环依赖,会抛出一个异常,如 BeanCurrentlyInCreationException,提示用户存在循环依赖。

解决

要解决这种问题,可以采用以下方法:

  1. 使用属性注入(setter 注入)或使用 @Autowired 注解在字段上进行注入,这样 Spring 可以通过提前暴露引用的方式解决循环依赖问题。例如,将 A 和 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;
    


  1. 使用懒加载(Lazy Loading)来解决循环依赖。在 Bean 的依赖声明上添加 @Lazy 注解,这样 Spring 容器在初始化 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;
    


  1. 如果可能,可以考虑调整类之间的依赖关系,将循环依赖拆分为线性依赖。这可以通过引入新的类或接口来实现,或者通过修改现有类的依赖关系实现。我们可以将 D 模块设置为调度器或者中介者,而不是直接依赖 A 和 B。我们可以将 A 和 B 作为 D 的依赖项进行注册,从而避免在 A 和 B 中直接依赖 D。以下是一个调整后的示例:
// A 模块
@Component
public class A 
    // A 的其他属性和方法

    // 注册 A 到 D 模块
    @Autowired
    public void registerToD(D d) 
        d.registerA(this);
    


// B 模块
@Component
public class B 
    // B 的其他属性和方法

    // 注册 B 到 D 模块
    @Autowired
    public void registerToD(D d) 
        d.registerB(this);
    


// D 模块,处理 A 和 B 之间的交互
@Component
public class D 
    private A a;
    private B b;

    public void registerA(A a) 
        this.a = a;
    

    public void registerB(B b) 
        this.b = b;


// 在 D 中处理 A 和 B 之间的交互
public void processInteraction() 
    // 使用 a 和 b 的方法来实现交互逻辑


在这个调整后的示例中,我们将 A 和 B 注册到 D 模块,而不是让 A 和 B 直接依赖 D。这样,我们解决了 A 和 B 与 D 之间的循环依赖问题。同时,我们仍然可以在 D 模块中处理 A 和 B 之间的交互逻辑。

这种调整后的模块化设计方法可以有效地解决循环依赖问题,同时保持代码的可维护性和可读性。当然,在实际项目中,我们需要根据具体需求和场景选择合适的解决方案。


需要注意的是,以上提到的懒加载方法在某些情况下可能会引发其他问题,例如在多线程环境下可能会导致 Bean 实例化多次。因此,在使用懒加载时需要对应用的具体需求和场景进行评估,确保其能够正常运行。

总结

总之,对于构造器循环依赖的问题,我们需要调整代码结构,以便让 Spring 容器能够正确处理依赖关系。在实际项目中,应避免产生循环依赖的情况,以提高代码的可维护性和可读性。如果确实需要处理循环依赖,可以采用属性注入、懒加载或调整依赖关系等方法解决。

Spring如何解决循环依赖?

在这里插入图片描述

介绍

先说一下什么是循环依赖,Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成

Spring的循环依赖有两种场景

  1. 构造器的循环依赖
  2. 属性的循环依赖

构造器的循环依赖,可以在构造函数中使用@Lazy注解延迟加载。在注入依赖时,先注入代理对象,当首次使用时再创建对象完成注入

属性的循环依赖主要是通过3个map来解决的

构造器的循环依赖

@Component
public class ConstructorA {

	private ConstructorB constructorB;

	@Autowired
	public ConstructorA(ConstructorB constructorB) {
		this.constructorB = constructorB;
	}
}
@Component
public class ConstructorB {

	private ConstructorA constructorA;

	@Autowired
	public ConstructorB(ConstructorA constructorA) {
		this.constructorA = constructorA;
	}
}
@Configuration
@ComponentScan("com.javashitang.dependency.constructor")
public class ConstructorConfig {
}
public class ConstructorMain {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext context =
				new AnnotationConfigApplicationContext(ConstructorConfig.class);
		System.out.println(context.getBean(ConstructorA.class));
		System.out.println(context.getBean(ConstructorB.class));
	}
}

运行ConstructorMain的main方法的时候会在第一行就报异常,说明Spring没办法初始化所有的Bean,即上面这种形式的循环依赖Spring无法解决。

我们可以在ConstructorA或者ConstructorB构造函数的参数上加上@Lazy注解就可以解决

@Autowired
public ConstructorB(@Lazy ConstructorA constructorA) {
	this.constructorA = constructorA;
}

因为我们主要关注属性的循环依赖,构造器的循环依赖就不做过多分析了

属性的循环依赖

先演示一下什么是属性的循环依赖

@Component
public class FieldA {

	@Autowired
	private FieldB fieldB;
}
@Component
public class FieldB {

	@Autowired
	private FieldA fieldA;
}
@Configuration
@ComponentScan("com.javashitang.dependency.field")
public class FieldConfig {
}
public class FieldMain {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext context =
				new AnnotationConfigApplicationContext(FieldConfig.class);
		// com.javashitang.dependency.field.FieldA@3aa9e816
		System.out.println(context.getBean(FieldA.class));
		// com.javashitang.dependency.field.FieldB@17d99928
		System.out.println(context.getBean(FieldB.class));
	}
}

Spring容器正常启动,能获取到FieldA和FieldB这2个Bean

属性的循环依赖在面试中还是经常被问到的。总体来说也不复杂,但是涉及到Spring Bean的初始化过程,所以感觉比较复杂,我写个demo演示一下整个过程

Spring的Bean的初始化过程其实比较复杂,为了方便理解Demo,我就把Spring Bean的初始化过程分为2部分

  1. bean的实例化过程,即调用构造函数将对象创建出来
  2. bean的初始化过程,即填充bean的各种属性

bean初始化过程完毕,则bean就能被正常创建出来了

下面开始写Demo,ObjectFactory接口用来生产Bean,和Spring中定义的接口一样

public interface ObjectFactory<T> {
	T getObject();
}
public class DependencyDemo {

	// 初始化完毕的Bean
	private final Map<String, Object> singletonObjects =
			new ConcurrentHashMap<>(256);

	// 正在初始化的Bean对应的工厂,此时对象已经被实例化
	private final Map<String, ObjectFactory<?>> singletonFactories =
			new HashMap<>(16);

	// 存放正在初始化的Bean,对象还没有被实例化之前就放进来了
	private final Set<String> singletonsCurrentlyInCreation =
			Collections.newSetFromMap(new ConcurrentHashMap<>(16));

	public  <T> T getBean(Class<T> beanClass) throws Exception {
		// 类名为Bean的名字
		String beanName = beanClass.getSimpleName();
		// 已经初始化好了,或者正在初始化
		Object initObj = getSingleton(beanName, true);
		if (initObj != null) {
			return (T) initObj;
		}
		// bean正在被初始化
		singletonsCurrentlyInCreation.add(beanName);
		// 实例化bean
		Object object = beanClass.getDeclaredConstructor().newInstance();
		singletonFactories.put(beanName, () -> {
			return object;
		});
		// 开始初始化bean,即填充属性
		Field[] fields = object.getClass().getDeclaredFields();
		for (Field field : fields) {
			field.setAccessible(true);
			// 获取需要注入字段的class
			Class<?> fieldClass = field.getType();
			field.set(object, getBean(fieldClass));
		}
		// 初始化完毕
		singletonObjects.put(beanName, object);
		singletonsCurrentlyInCreation.remove(beanName);
		return (T) object;
	}

	/**
	 * allowEarlyReference参数的含义是Spring是否允许循环依赖,默认为true
	 * 所以当allowEarlyReference设置为false的时候,当项目存在循环依赖,会启动失败
	 */
	public Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null 
				&& isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory =
							this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
					}
				}
			}
		}
		return singletonObject;
	}

	/**
	 * 判断bean是否正在被初始化
	 */
	public boolean isSingletonCurrentlyInCreation(String beanName) {
		return this.singletonsCurrentlyInCreation.contains(beanName);
	}

}

测试一波

public static void main(String[] args) throws Exception {
	DependencyDemo dependencyDemo = new DependencyDemo();
	// 假装扫描出来的对象
	Class[] classes = {A.class, B.class};
	// 假装项目初始化所有bean
	for (Class aClass : classes) {
		dependencyDemo.getBean(aClass);
	}
	// true
	System.out.println(
			dependencyDemo.getBean(B.class).getA() == dependencyDemo.getBean(A.class));
	// true
	System.out.println(
			dependencyDemo.getBean(A.class).getB() == dependencyDemo.getBean(B.class));
}

是不是很简单?我们只用了2个map就搞定了Spring的循环依赖

2个Map就能搞定循环依赖,那为什么Spring要用3个Map呢?

原因其实也很简单,当我们从singletonFactories中根据BeanName获取相应的ObjectFactory,然后调用getObject()这个方法返回对应的Bean。在我们的例子中
ObjectFactory的实现很简单哈,就是将实例化好的对象直接返回,但是在Spring中就没有这么简单了,执行过程比较复杂,为了避免每次拿到ObjectFactory然后调用getObject(),我们直接把ObjectFactory创建的对象缓存起来不就行了,这样就能提高效率了

比如A依赖B和C,B和C又依赖A,如果不做缓存那么初始化B和C都会调用A对应的ObjectFactory的getObject()方法。如果做缓存只需要B或者C调用一次即可。

知道了思路,我们把上面的代码改一波,加个缓存。

public class DependencyDemo {

	// 初始化完毕的Bean
	private final Map<String, Object> singletonObjects =
			new ConcurrentHashMap<>(256);

	// 正在初始化的Bean对应的工厂,此时对象已经被实例化
	private final Map<String, ObjectFactory<?>> singletonFactories =
			new HashMap<>(16);

	// 缓存Bean对应的工厂生产好的Bean
	private final Map<String, Object> earlySingletonObjects =
			new HashMap<>(16);

	// 存放正在初始化的Bean,对象还没有被实例化之前就放进来了
	private final Set<String> singletonsCurrentlyInCreation =
			Collections.newSetFromMap(new ConcurrentHashMap<>(16));

	public  <T> T getBean(Class<T> beanClass) throws Exception {
		// 类名为Bean的名字
		String beanName = beanClass.getSimpleName();
		// 已经初始化好了,或者正在初始化
		Object initObj = getSingleton(beanName, true);
		if (initObj != null) {
			return (T) initObj;
		}
		// bean正在被初始化
		singletonsCurrentlyInCreation.add(beanName);
		// 实例化bean
		Object object = beanClass.getDeclaredConstructor().newInstance();
		singletonFactories.put(beanName, () -> {
			return object;
		});
		// 开始初始化bean,即填充属性
		Field[] fields = object.getClass().getDeclaredFields();
		for (Field field : fields) {
			field.setAccessible(true);
			// 获取需要注入字段的class
			Class<?> fieldClass = field.getType();
			field.set(object, getBean(fieldClass));
		}
		singletonObjects.put(beanName, object);
		singletonsCurrentlyInCreation.remove(beanName);
		earlySingletonObjects.remove(beanName);
		return (T) object;
	}

	/**
	 * allowEarlyReference参数的含义是Spring是否允许循环依赖,默认为true
	 */
	public Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null
				&& isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				singletonObject = this.earlySingletonObjects.get(beanName);
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory =
							this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}
}

我们写的getSingleton的实现和org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)的实现一模一样,这个方法几乎所有分析Spring循环依赖的文章都会提到,这次你明白工作原理是什么了把

总结一波

  1. 拿bean的时候先从singletonObjects(一级缓存)中获取
  2. 如果获取不到,并且对象正在创建中,就从earlySingletonObjects(二级缓存)中获取
  3. 如果还是获取不到就从singletonFactories(三级缓存)中获取,然后将获取到的对象放到earlySingletonObjects(二级缓存)中,并且将bean对应的singletonFactories(三级缓存)清除
  4. bean初始化完毕,放到singletonObjects(一级缓存)中,将bean对应的earlySingletonObjects(二级缓存)清除

欢迎关注

在这里插入图片描述

参考博客

[1]https://mp.weixin.qq.com/s/gBr3UfC1HRcw4U-ZMmtRaQ
[2]https://mp.weixin.qq.com/s/5mwkgJB7GyLdKDgzijyvXw
比较详细
[1]https://zhuanlan.zhihu.com/p/84267654
[2]https://juejin.im/post/5c98a7b4f265da60ee12e9b2

以上是关于spring中AB类构造器存在循环依赖咋办的主要内容,如果未能解决你的问题,请参考以下文章

Spring的循环依赖问题

spring-循环依赖

[跟我学spring学习笔记][DI循环依赖]

[跟我学spring学习笔记][DI循环依赖]

Spring-bean的循环依赖以及解决方式

Spring系列五:Spring怎么解决循环依赖