1. 程式人生 > 實用技巧 >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);
// [email protected]
System.out.println(context.getBean(FieldA.class));
// [email protected]
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