先不去管Spring中的回圈依賴,我們先實作一個自定義注解,來模擬@Autowired的功能,
一、自定義注解模擬@Autowired
自定義Load注解,被該注解標識的欄位,將會進行自動注入
/**
* @author qcy
* @create 2021/10/02 13:31:20
*/
//只用在欄位上
@Target(ElementType.FIELD)
//運行時有效,這樣可以通過反射決議注解
@Retention(RetentionPolicy.RUNTIME)
public @interface Load {
}
新建A類與B類,其中A類中需要注入B
public class A {
@Load
private B b;
public B getB() {
return b;
}
}
public class B {
}
測驗類
public class Main {
private static <T> T getBean(Class<T> clazz) throws IllegalAccessException, InstantiationException {
//實體化物件
T instance = clazz.newInstance();
//獲取當前類中的所有欄位
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//允許訪問私有變數
field.setAccessible(true);
//判斷欄位是否被@Load注解修飾
boolean isUseLoad = field.isAnnotationPresent(Load.class);
if (!isUseLoad) {
continue;
}
//獲取需要被注入的欄位的class
Class<?> fieldType = field.getType();
//遞回獲取欄位的實體物件
Object fieldBean = getBean(fieldType);
//將實體物件注入到該欄位中
field.set(instance, fieldBean);
}
return instance;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
A a = getBean(A.class);
System.out.println(a.getB().getClass());
}
}
最終能夠列印出a物件中依賴的b的class型別

二、多例模式下的回圈依賴
現在思考一個問題,如果b物件同時依賴a呢?也就是B類中需要注入A
現在直接把B類的代碼改成以下的樣子
public class B {
@Load
private A a;
public A getA() {
return a;
}
}
直接運行測驗類,會發生什么呢?

出現了堆疊溢位!到底是哪里出問題了呢?

原來是,在實體化A后,屬性注入階段發現需要注入B的實體,于是去實體化B,B又需要依賴A,因此去實體化A,一直依賴下去...

不難觀察出,getBean每呼叫一次,都會回傳一個新的物件,也就是對應于多例模式,
多例模式中出現回圈依賴,直接報出了StackOverflowError,看來解決不了回圈依賴,這也并不難理解,
三、單例模式下使用快取來解決回圈依賴
如果這個時候,對于傳入的同一個class,能夠回傳同一個實體,即單例模式,能否解決回圈依賴呢?
大致的思路是,使用一個快取map,將實體化好且屬性注入完畢的物件快取到該map中,下次直接使用即可,
可現在又遇到難題了,壓根就創建不出來一個完整的A的實體物件啊,無法進行快取,
既然無法直接將完成品放入到快取中,那是否可以將實體物件分為兩個階段
半成品階段
僅完成實體化,并沒有完成屬性注入
成品階段
半成品完成屬性注入
首先實體化A得到半成品a,接著將這個a放入到快取中,然后實體化b時,注入快取中半成品的a,得到成品b,最終再將成品b注入到半成品a中,此時a變為成品,
這個時候,a完成了實體化與屬性注入,b也完成了實體化與屬性注入,回圈依賴好像就能解決了,

改下代碼,直接上線!
public class Main {
//由類名可以獲取到對應的實體物件
private static Map<String, Object> singletonObjects= new HashMap<>();
private static <T> T getBean(Class<T> clazz) throws IllegalAccessException, InstantiationException {
//先從快取中獲取
String className = clazz.getSimpleName();
if (singletonObjects.containsKey(className)) {
return (T) singletonObjects.get(className);
}
//實體化物件
T instance = clazz.newInstance();
//實體化完成后,就將這個半成品放入到快取中
singletonObjects.put(className, instance);
//獲取當前類中的所有欄位
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//允許訪問私有變數
field.setAccessible(true);
//判斷欄位是否被@Load注解修飾
boolean isUseLoad = field.isAnnotationPresent(Load.class);
if (!isUseLoad) {
continue;
}
//獲取需要被注入的欄位的class
Class<?> fieldType = field.getType();
//遞回獲取欄位的實體物件
Object fieldBean = getBean(fieldType);
//將實體物件注入到該欄位中
field.set(instance, fieldBean);
}
return instance;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
A a1 = getBean(A.class);
A a2 = getBean(A.class);
System.out.println(a1 == a2);
System.out.println(a1.getB() == a1.getB());
B b = getBean(B.class);
System.out.println(a1.getB() == b);
}
}
運行后,將回傳三個true,說明單例模式下的回圈依賴是可以解決的,
大致的圖是這樣的

事情似乎到這里應該結束了,好的觀眾朋友們,咱們下期見,

四、多執行緒下隱藏的問題
以上的代碼,在單執行緒的環境下,是沒有問題的,可是放到多執行緒的環境中,可能就會出現空指標問題,
執行緒1剛把半成品a放入到快取中,還未來得及將b注入進去,此時執行緒2直接在快取中獲取到了a,在嘗試呼叫其所依賴的b的任何方法時,就會出現空指標例外,
因此上述代碼,存在執行緒不安全的問題,怎么去解決呢?
很簡單,我直接對getBean方法加鎖不就可以了嗎?

對整個方法加鎖確實可以解決問題,但運行性能會大打折扣,
第一次的創建與讀快取互斥,創建完成后的讀與讀不需要加鎖,
其實問題的本質在于,singletonObjects快取既存放半成品型別,又存放成品型別,導致執行緒根本不清楚拿到的實體的型別,
那能不能再創建一個快取earlySingletonObjects,即早期單例物件,就存放半成品型別,先前的singletonObjects存放成品型別,
直接上代碼
//成品快取
private static final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
//半成品快取
private static final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
//從快取中獲取
private static Object getSingleton(String className) {
//先從成品快取中查找
Object singletonObject = singletonObjects.get(className);
if (singletonObject == null) {
//再從半成品快取中查找
singletonObject = earlySingletonObjects.get(className);
}
return singletonObject;
}
@SuppressWarnings("unchecked")
private static <T> T getBean(Class<T> clazz) throws IllegalAccessException, InstantiationException {
//先從快取中獲取
String className = clazz.getSimpleName();
Object singleton = getSingleton(className);
if (singleton != null) {
return (T) singleton;
}
synchronized (singletonObjects) {
singleton = singletonObjects.get(className);
//這里需要再進行一次檢查
if (singleton != null) {
return (T) singleton;
}
//實體化物件
T instance = clazz.newInstance();
//實體化完成后,就將這個半成品放入到快取中
earlySingletonObjects.put(className, instance);
//獲取當前類中的所有欄位
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//允許訪問私有變數
field.setAccessible(true);
//判斷欄位是否被@Load注解修飾
boolean isUseLoad = field.isAnnotationPresent(Load.class);
if (!isUseLoad) {
continue;
}
//獲取需要被注入的欄位的class
Class<?> fieldType = field.getType();
//遞回獲取欄位的實體物件
Object fieldBean = getBean(fieldType);
//將實體物件注入到該欄位中
field.set(instance, fieldBean);
}
//完成屬性注入后,從半成品快取中移除,加入到成品快取中
earlySingletonObjects.remove(className);
singletonObjects.put(className, instance);
return instance;
}
}
public static void main(String[] args) {
new Thread(() -> {
try {
A a1 = getBean(A.class);
System.out.println("t1.a:" + a1.hashCode());
System.out.println("t1.b:" + a1.getB().hashCode());
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
A a1 = getBean(A.class);
System.out.println("t2.a:" + a1.hashCode());
System.out.println("t2.b:" + a1.getB().hashCode());
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
B b = getBean(B.class);
System.out.println("t3.b:" + b.hashCode());
System.out.println("t3.a:" + b.getA().hashCode());
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}).start();
}
輸出結果:

多次實驗,從輸出結果說明:在多執行緒的場景下,使用兩級快取能夠有效避免出現空指標的問題,在一定程度上也能比整個方法加鎖的效率更高,

五、什么樣的回圈依賴都能解決嗎?
從第二節可以看出,多例模式下就不可以解決回圈依賴,
我們在以上小節中寫的代碼,是默認全部使用反射set注入的,
而對于單例模式,在經過對依賴項自然排序后,構造器注入是不可以優先于任何一個set注入的,

第一種場景:b依賴a,需要使用構造器注入a;a依賴b,需要使用set注入b
經過Spring對Bean的自然排序后,會先去創建a,再去創建b,

這種場景,是可以解決回圈依賴的,在實體化B時,已經存在半成品a,
結論:最后再使用構造器注入時,可以解決回圈依賴,
第二種場景:a依賴b,需要使用構造器注入b;b依賴a,需要使用set注入a

這種場景,在實體化a時,就需要呼叫構造方法,因此去實體b,而b在快取中找不到a,造成注入失敗,
結論:一開始就使用構造器注入,則不能解決回圈依賴,
那么都使用構造器注入時,那肯定也不能解決回圈依賴的,
因此,Spring解決回圈依賴有兩個小前提:
- 回圈依賴中的Bean都是單例
- 在經過自然排序后,構造器注入不能優先于回圈依賴中的任何一個set注入
六、Spring中解決回圈依賴的原理
在Spring中,我們使用getBean方法從容器獲取一個Bean,那么就從getBean方法入手
AbstractApplicationContext類中的getBean
public Object getBean(String name) throws BeansException {
assertBeanFactoryActive();
return getBeanFactory().getBean(name);
}
接著進入AbstractBeanFactory類中的getBean
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}
再進入到doGetBean方法中,該方法比較長,截取其中比較核心的點來說
先說getSingleton方法
//從快取中獲取指定的bean
Object sharedInstance = getSingleton(beanName);
進入到DefaultSingletonBeanRegistry的getSingleton方法中
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//先從一級快取中查找
Object singletonObject = this.singletonObjects.get(beanName);
//如果一級快取中沒有,且當前bean正處于創建的程序中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//從二級快取中查找
singletonObject = this.earlySingletonObjects.get(beanName);
//如果二級快取中也沒有,且允許暴露早期參考時
if (singletonObject == null && allowEarlyReference) {
//從三級快取中查找到bean的工廠
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//呼叫getObject方法生成bean
singletonObject = singletonFactory.getObject();
//放入到二級快取中
this.earlySingletonObjects.put(beanName, singletonObject);
//從三級快取中移除
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
看到這里,似乎和之前我們寫的代碼很像啊,
Spring在解決回圈依賴時,其實也用到了快取,快取宣告及定義如下:
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
singletonObjects,一級快取,存放的是最終的成品,即完成實體化(僅呼叫構造方法)、屬性注入、初始化后的bean
earlySingletonObjects,二級快取,存放的是半成品,或叫早期曝光物件,即只完成實體化,未完成屬性注入及初始化的bean
singletonFactories,三級快取,存放的是能獲取到半成品的工廠
前幾節我們自己解決了回圈依賴,一級快取的存在是為了在單執行緒的情況下解決回圈依賴,而二級快取的存在是為了兼容多執行緒,提升獲取bean的效率,那三級快取存在的意義又是什么呢?

解決回圈依賴,其中的一個核心前提是bean必須是單例的,因此我們接著看doGetBean方法中第二處核心的代碼,即處理單例bean的代碼
/處理單例bean
if (mbd.isSingleton()) {
//sharedInstance就是從快取中獲取的bean,一般來說,這里是null的
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
其中getSingleton方法中的第二個引數是一個函式式介面型別的ObjectFactory,因此這里可以直接使用lambda運算式來傳入一個默認的實作,即createBean方法,
進入到getSingleton(beanName,objectFactory)方法中,精簡后的代碼如下:
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
//如果不在快取中,就會利用getObject方法去創建
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//標志當前bean正在創建中,如果被多次創建,這里也會拋出例外
beforeSingletonCreation(beanName);
boolean newSingleton = false;
//執行getObject,即執行外部傳入的createBean方法
singletonObject = singletonFactory.getObject();
newSingleton = true;
//省略例外處理,出現例外時,newSingleton=false
//取消bean正在創建的標志
afterSingletonCreation(beanName);
if (newSingleton) {
//管理快取,移除三級快取與二級快取,加入到一級快取中
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}
當走到getObject方法時,就會進入到AbstractAutowireCapableBeanFactorycreateBean方法中核心的方法是這一句話
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
接著進入到同類中的doCreateBean方法中,精簡之后的代碼:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// 實體化bean
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
//加入到三級快取中,getEarlyBeanReference會回傳單例工廠
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
Object exposedObject = bean;
//屬性注入
populateBean(beanName, mbd, instanceWrapper);
//初始化
exposedObject = initializeBean(beanName, exposedObject, mbd);
if (earlySingletonExposure) {
//從二級快取中查找
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
//回傳二級快取中的bean,這里就有可能是代理后的物件
exposedObject = earlySingletonReference;
}
}
return exposedObject;
}
看看getEarlyBeanReference到底回傳了什么樣的一個單例工廠(但其實說是工廠,不如說是獲取早期曝光物件的一個邏輯)
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
//從容器中尋找實作InstantiationAwareBeanPostProcessor介面的后置處理器
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
//遍歷找到的所有符合要求的后置處理器
for (BeanPostProcessor bp : getBeanPostProcessors()) {
//如果后置處理器實作了SmartInstantiationAwareBeanPostProcessor介面
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
//呼叫SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
在getEarlyBeanReference方法中,最侄訓回傳一個經過AOP攔截后生成的代理物件,
如果當前沒有任何實作InstantiationAwareBeanPostProcessor介面的后置處理器,即當前bean沒有被任何AOP攔截后,那直接回傳傳進來的bean,
到這里,原始碼已經分析得差不多了,以A與B的回圈依賴,畫一下整個程序
以A沒被代理為例:

A被代理為例:

從上面的分析可以看出,解決回圈依賴的本質是借助于快取,
多例下的回圈依賴,每次注入的都是一個新的物件,完全用不到快取,所以無法解決多例下的回圈依賴,
在經過自然排序后,如果一開始就是構造器注入,也無法利用到快取,set注入之所以能利用到快取,是因為能夠將實體化與屬性賦值分離,給快取利用留下余地,
對三級快取的理解
在單執行緒的情況,也就是getBean僅支持串行操作的話,那么一級快取其實已經夠用了,
二級快取將半成品與成品物件分離,使得多執行緒的情況下,不會拿到不完整的物件實體,而且支持多執行緒同時查詢快取,在一定程度上提升了性能,
三級快取存放的是單例物件工廠,準確的說,是函式式介面實作,是生成物件的一段邏輯,如果該物件被代理,則工廠的getObject回傳代理之后的物件,否則回傳原物件,
這里可能有人有疑問,為什么需要三級快取呢?在實體化階段之后,直接將原物件或代理物件放入二級快取不也行嗎?
理論是可以的,
一般來說,在Bean的生命周期中,創建代理是在初始化完成后再做的,
而如果代理出現在回圈依賴中,不能等到初始化完成后再做,否則B中注入的A就是原物件,不是代理物件,因此,需要在B的屬性注入的前一刻完成對A的代理,而這個階段,也必然是發生在A的初始化之前,
這個時候,大可以在A實體化之后,如果存在特定的后置處理器,也就是說存在代理,那么直接將A的代理物件放入二級快取中,不管以后會不會出現回圈依賴,事實上,在這個時期,也無法去檢測到底之后會不會產生回圈依賴,總不能去預知未來吧,如果出現回圈依賴,則從二級快取中獲取A的代理物件,注入到B中,這個時候是可以的,如果不發生回圈依賴,這個A的代理物件就是無用的,做的是無用功,
這個時候,再加上一層快取呢?在A實體化之后,只往三級快取中存放一個生成物件的邏輯,到底是生成代理物件還是原物件,由發生回圈依賴時再做決定,當發生回圈依賴時,例如當B中需要注入A時,會呼叫三級快取中的工廠邏輯,生成A的代理物件,注入進B中,當沒發生回圈依賴時,A的代理物件還是在初始化完成之后再做,和Bean的生命周期中的處理一致,
所以,三級快取的存在,是由于對代理的考慮,一方面能避免直接在二級快取中存放代理物件而之后沒發生回圈依賴所做的無用功,另一方面也能夠最大化的統一Bean的生命周期,可謂兩全其美,一箭雙雕,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/305434.html
標籤:java
下一篇:Java堆疊和佇列的實作
