這個其實是一個特別高頻的面試題,小編也一直很想和大家仔細來聊一聊這個話題,網上關於這塊的文章很多,但是我一直覺得要把這個問題講清楚還有點難度,今天我來試一試,看能不能和小夥伴們把這個問題梳理清楚。
一 迴圈依賴
1.1 什麼是迴圈依賴
首先,什麼是迴圈依賴?這個其實好理解,就是兩個 Bean 互相依賴,類似下面這樣:
@Service public class AService { @Autowired BService bService; } @Service public class BService { @Autowired AService aService; }
AService 和 BService 互相依賴:
這個應該很好理解。
1.2 迴圈依賴的型別
一般來說,迴圈依賴有三種不同的形態,上面 1.1 小節是其中一種。
另外兩種分別是三者依賴,如下圖:
這種迴圈依賴一般隱藏比較深,不易發覺。
還有自我依賴,如下圖:
一般來說,如果我們的程式碼中出現了迴圈依賴,則說明我們的程式碼在設計的過程中可能存在問題,我們應該儘量避免迴圈依賴的發生。不過一旦發生了迴圈依賴,Spring 預設也幫我們處理好了,當然這並不能說明迴圈依賴這種程式碼就沒問題。實際上在目前最新版的 Spring 中,迴圈依賴是要額外開啟的,如果不額外配置,發生了迴圈依賴就直接報錯了。
另外,Spring 並不能處理所有的迴圈依賴,後面小編會和大家進行分析。
二 迴圈依賴解決思路
2.1 解決思路
那麼對於迴圈依賴該如何解決呢?其實很簡單,加入一個快取就可以了,小夥伴們來看下面這張圖:
我們在這裏引入了一個快取池。
當我們需要建立 AService 的例項的時候,會首先透過 Java 反射建立出來一個原始的 AService,這個原始 AService 可以簡單理解為剛剛 new 出來(實際是剛剛透過反射建立出來)還沒設定任何屬性的 AService,此時,我們把這個 AService 先存入到一個快取池中。
接下來我們就需要給 AService 的屬性設定值了,同時還要處理 AService 的依賴,這時我們發現 AService 依賴 BService,那麼就去建立 BService 物件,結果建立 BService 的時候,發現 BService 依賴 AService,那麼此時就先從快取池中取出來 AService 先用著,然後繼續 BService 建立的後續流程,直到 BService 建立完成後,將之賦值給 AService,此時 AService 和 BService 就都建立完成了。
可能有小夥伴會說,BService 從快取池中拿到的 AService 是一個半成品,並不是真正的最終的 AService,但是小夥伴們要知道,咱們 Java 是引用傳遞(也可以認為是值傳遞,只不過這個值是記憶體地址),BService 當時拿到的是 AService 的引用,說白了就是一塊記憶體地址而已,根據這個地址找到的就是 AService,所以,後續如果 AService 建立完成後,BService 所拿到的 AService 就是完整的 AService 了。
那麼上面提到的這個快取池,在 Spring 容器中有一個專門的名字,就叫做 earlySingletonObjects,這是 Spring 三級快取中的二級快取,這裏儲存的是剛剛透過反射建立出來的 Bean,這些 Bean 還沒有經歷過完整生命週期,Bean 的屬性可能都還沒有設定,Bean 需要的依賴都還沒有注入進來。另外兩級快取分別是:
singletonObjects:這是一級快取,一級快取中儲存的是所有經歷了完整生命週期的 Bean,即一個 Bean 從建立、到屬性賦值、到各種處理器的執行等等,都經歷過了,就存到 singletonObjects 中,當我們需要獲取一個 Bean 的時候,首先會去一級快取中查詢,當一級快取中沒有的時候,纔會考慮去二級快取。
singletonFactories:這是三級快取。在一級快取和二級快取中,快取的 key 是 beanName,快取的 value 則是一個 Bean 物件,但是在三級快取中,快取的 value 是一個 Lambda 表示式,透過這個 Lambda 表示式可以建立出來目標物件的一個代理物件。
有的小夥伴可能會覺得奇怪,按照上文的介紹,一級快取和二級快取就足以解決迴圈依賴了,為什麼還冒出來一個三級快取?那就得考慮 AOP 的情況了!
2.2 存在 AOP 怎麼辦
上面小編給大家介紹的是普通的 Bean 建立,那確實沒有問題。但是 Spring 中還有一個非常重要的能力,那就是 AOP。
說到這裏,我得先和小夥伴麼說一說 Spring 中 AOP 的建立流程。
正常來說是我們首先透過反射獲取到一個 Bean 的例項,然後就是給這個 Bean 填充屬性,屬性填充完畢之後,接下來就是執行各種 BeanPostProcessor 了,如果這個 Bean 中有需要代理的方法,那麼系統就會自動配置對應的後置處理器,小編舉一個簡單例子,假設我有如下一個 Service:
@Service public class UserService { @Async public void hello() { System.out.println("hello>>>"+Thread.currentThread().getName()); } }
那麼系統就會自動提供一個名為 AsyncAnnotationBeanPostProcessor
的處理器,在這個處理器中,系統會生成一個代理的 UserService 物件,並用這個物件代替原本的 UserService。
那麼小夥伴們要搞清楚的是,原本的 UserService 和新生成的代理 UserService 是兩個不同的物件,佔兩塊不同的記憶體地址!!!
我們再來回顧下面這張圖:
如果 AService 最終是要生成一個代理物件的話,那麼 AService 存到快取池的其實還是原本的 AService,因為此時還沒到處理 AOP 那一步(要先給各個屬性賦值,然後纔是 AOP 處理),這就導致 BService 從快取池裏拿到的 AService 是原本的 AService,等到 BService 建立完畢之後,AService 的屬性賦值才完成,接下來在 AService 後續的建立流程中,AService 會變成了一個代理物件了,不是快取池裏的 AService 了,最終就導致 BService 所依賴的 AService 和最終建立出來的 AService 不是同一個。
爲了解決這個問題,Spring 引入了三級快取 singletonFactories。
singletonFactories 的工作機制是這樣的(假設 AService 最終是一個代理物件):
當我們建立一個 AService 的時候,透過反射把原始的 AService 建立出來之後,先去判斷當前一級快取中是否存在當前 Bean,如果不存在,則:
首先向三級快取中新增一條記錄,記錄的 key 就是當前 Bean 的 beanName,value 則是一個 Lambda 表示式 ObjectFactory,透過執行這個 Lambda 可以給當前 AService 生成代理物件。
然後如果二級快取中存在當前 AService Bean,則移除掉。
現在繼續去給 AService 各個屬性賦值,結果發現 AService 需要 BService,然後就去建立 BService,建立 BService 的時候,發現 BService 又需要用到 AService,於是就先去一級快取中查詢是否有 AService,如果有,就使用,如果沒有,則去二級快取中查詢是否有 AService,如果有,就使用,如果沒有,則去三級快取中找出來那個 ObjectFactory,然後執行這裏的 getObject 方法,這個方法在執行的過程中,會去判斷是否需要生成一個代理物件,如果需要就生成代理物件返回,如果不需要生成代理物件,則將原始物件返回即可。最後,把拿到手的物件存入到二級快取中以備下次使用,同時刪除掉三級快取中對應的資料。這樣 AService 所依賴的 BService 就建立好了。
接下來繼續去完善 AService,去執行各種後置的處理器,此時,有的後置處理器想給 AService 生成代理物件,發現 AService 已經是代理物件了,就不用生成了,直接用已有的代理物件去代替 AService 即可。
至此,AService 和 BService 都搞定。
本質上,singletonFactories 是把 AOP 的過程提前了。
2.3 小結
總的來說,Spring 解決迴圈依賴把握住兩個關鍵點:
提前暴露:剛剛建立好的物件還沒有進行任何賦值的時候,將之暴露出來放到快取中,供其他 Bean 提前引用(二級快取)。
提前 AOP:A 依賴 B 的時候,去檢查是否發生了迴圈依賴(檢查的方式就是將正在建立的 A 標記出來,然後 B 需要 A,B 去建立 A 的時候,發現 A 正在建立,就說明發生了迴圈依賴),如果發生了迴圈依賴,就提前進行 AOP 處理,處理完成後再使用(三級快取)。
原本 AOP 這個過程是屬性賦完值之後,再由各種後置處理器去處理 AOP 的(
AbstractAutoProxyCreator
),但是如果發生了迴圈依賴,就先 AOP,然後屬性賦值,最後等到後置處理器執行的時候,就不再做 AOP 的處理了。
不過需要注意,三級快取並不能解決所有的迴圈依賴。
嚴格來說,其實也不是解決不了,所有問題都有辦法解決,只是還需要額外配置。
三 特殊情況
根據前面介紹的思路,以下一些迴圈依賴場景無法解決。
3.1 基於構造器注入
如果依賴的物件是基於構造器注入的,那麼執行的時候就會報錯,程式碼如下:
@Service public class AService { BService bService; public AService(BService bService) { this.bService = bService; } } @Service public class BService { AService aService; public BService(AService aService) { this.aService = aService; } }
執行時報錯如下:
原因分析:
前面我們說解決迴圈依賴的思路是加入快取,如下圖:
我們說先把 AService 原始物件建立出來,存入到快取池中,然後再處理 AService 中需要注入的外部 Bean 等等,但是,如果 AService 依賴的 BService 是透過構造器注入的,那就會導致在建立 AService 原始物件的時候就需要用到 BService,去建立 BService 時候又需要 AService,這樣就陷入到死迴圈了,對於這樣的迴圈依賴執行時候就會出錯。
更進一步,如果我們在 AService 中是透過 @Autowired 來注入 BService 的,那麼應該是可以執行的,程式碼如下:
@Service public class AService { @Autowired BService bService; } @Service public class BService { AService aService; public BService(AService aService) { this.aService = aService; } }
上面這段程式碼,AService 的原始物件就可以順利建立出來放到快取池中,BService 建立所需的 AService 也就能從快取中獲取到,所以就可以執行了。
3.2 prototype 物件
迴圈依賴雙方 scope 都是 prototype 的話,也會迴圈依賴失敗,程式碼如下:
@Service @Scope("prototype") public class AService { @Autowired BService bService; } @Service @Scope("prototype") public class BService { @Autowired AService aService; }
這種迴圈依賴執行時也會報錯,報錯資訊如下(跟前面報錯資訊一樣):
原因分析:
scope 為 prototype 意思就是說這個 Bean 每次需要的時候都現場建立,不用快取裡的。那麼 AService 需要 BService,所以就去現場建立 BService,結果 BService 又需要 AService,繼續現場建立,AService 又需要 BService...,所以最終就陷入到死迴圈了。
3.3 @Async
帶有 @Async 註解的 Bean 產生迴圈依賴,程式碼如下:
@Service public class AService { @Autowired BService bService; @Async public void hello() { } } @Service public class BService { @Autowired AService aService; }
報錯資訊如下:
其實大家從這段報錯資訊中也能看出來個七七八八:在 BService 中注入了 AService 的原始物件,但是 AService 在後續的處理流程中被 AOP 代理了,產生了新的物件,導致 BService 中的 AService 並不是最終的 AService,所以就出錯了!
那有小夥伴要問了,前面我們不是說了三級快取就是爲了解決 AOP 問題嗎,為什麼這裏發生了 AOP 卻無法解決?
如下兩個前置知識大家先理解一下:
第一:
其實大部分的 AOP 迴圈依賴是沒有問題的,這個 @Async 只是一個特例,特別在哪裏呢?一般的 AOP 都是由 AbstractAutoProxyCreator 這個後置處理器來處理的,透過這個後置處理器生成代理物件,AbstractAutoProxyCreator 後置處理器是 SmartInstantiationAwareBeanPostProcessor 介面的子類,並且 AbstractAutoProxyCreator 後置處理器重寫了 SmartInstantiationAwareBeanPostProcessor 介面的 getEarlyBeanReference 方法;而 @Async 是由 AsyncAnnotationBeanPostProcessor 來生成代理物件的,AsyncAnnotationBeanPostProcessor 也是 SmartInstantiationAwareBeanPostProcessor 的子類,但是卻沒有重寫 getEarlyBeanReference 方法,預設情況下,getEarlyBeanReference 方法就是將傳進來的 Bean 原封不動的返回去。
第二:
在 Bean 初始化的時候,Bean 建立完成後,後面會執行兩個方法:
populateBean:這個方法是用來做屬性填充的。
initializeBean:這個方法是用來初始化 Bean 的例項,執行工廠回撥、init 方法以及各種 BeanPostProcessor。
大家先把這兩點搞清楚,然後我來跟大家說上面程式碼的執行流程。
首先 AService 初始化,初始化完成之後,存入到三級快取中。
執行 populateBean 方法進行 AService 的屬性填充,填充時發現需要用到 BService,於是就去初始化 BService。
初始化 BService 發現需要用到 AService,於是就去快取池中找,找到之後拿來用,但是!!!這裏找到的 AService 不是代理物件,而是原始物件。因為在三級快取中儲存的 AService 的那個 ObjectFactory 工廠,在對 AService 進行提前 AOP 的時候,執行的是 SmartInstantiationAwareBeanPostProcessor 型別的後置處理器 中的 getEarlyBeanReference 方法,如果是普通的 AOP,呼叫 getEarlyBeanReference 方法最終會觸發提前 AOP,但是,這裏執行的是 AsyncAnnotationBeanPostProcessor 中的 getEarlyBeanReference 方法,該方法只是返回了原始的 Bean,並未做任何額外處理。
當 BService 建立完成後,AService 繼續初始化,繼續執行 initializeBean 方法。
在 initializeBean 方法中,執行其他的各種後置處理器,包括 AsyncAnnotationBeanPostProcessor,此時呼叫的是 AsyncAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法,在該方法中為 AService 生成了代理物件。
在 initializeBean 方法執行完成之後,AService 會繼續去檢查最終的 Bean 是不是還是一開始的 Bean,如果不是,就去檢查當前 Bean 有沒有被其他 Bean 引用過,如果被引用過,就會拋出來異常,也就是上圖大家看到的異常資訊。
這就是小編和大家分享的三種 Spring 預設無法解決的迴圈依賴,其實也不是無法解決,需要一些額外配置也能解決。
那麼對於以上問題該如何解決?
Spring 裏邊提供了辦法來解決,但是似乎又沒有解決,繼續看你就明白了。
四 @Lazy
前面提到的三種無法自動解決的迴圈依賴,都可以透過新增 @Lazy 註解來解決。
如果是構造器注入,如下:
@Service public class AService { BService bService; @Lazy public AService(BService bService) { this.bService = bService; } public BService getbService() { return bService; } } @Service public class BService { AService aService; @Lazy public BService(AService aService) { this.aService = aService; } public AService getaService() { return aService; } }
@Lazy 註解可以新增在 AService 或者 BService 的構造方法上,也可以都新增上。
新增上之後,我們再去啟動專案,就不會報錯了。這樣看起來問題解決了,但是其實還是差點意思,小夥伴們看一下我的啟動程式碼:
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml"); AService aService = ctx.getBean(AService.class); BService bService = ctx.getBean(BService.class); System.out.println("aService.getClass() = " + aService.getClass()); System.out.println("bService.getClass() = " + bService.getClass()); System.out.println("aService.getbService().getClass() = " + aService.getbService().getClass()); System.out.println("bService.getaService().getClass() = " + bService.getaService().getClass());
最終列印結果如下:
小夥伴們看到,我們從容器中獲取到的 AService 和 BService 的 Bean 都是正常的未被代理的物件,事實上我們的原始程式碼確實也沒有需要代理的地方。但是,AService 中的 BService 以及 BService 中的 AService 卻都是代理物件,按理說 AService 中的 BService 應該和我們從 Spring 容器中獲取到的 BService 一致,BService 中的 AService 也應該和 Spring 容器中獲取到的 AService 一致,但實際上,兩者卻並不相同。
不過這樣也好懂了,為什麼 Spring 能把一個死結給解開,就是因為 AService 和 BService 各自注入的 Bean 都不是原始的 Bean,都是一個代理的 Bean,AService 中注入的 BService 是一個代理物件,同理,BService 中注入的 AService 也是一個代理物件。
這也是為什麼我一開始說這個問題 Spring 解決了又沒解決。
其實,這就是 @Lazy 這個註解的工作原理,看名字,加了該註解的物件會被延遲載入,實際上被該註解標記的物件,會自動生成一個代理物件。
前面提到的另外兩個問題,也可以透過 @Lazy 註解來解決,程式碼如下:
@Service @Scope("prototype") public class AService { @Lazy @Autowired BService bService; } @Service @Scope("prototype") public class BService { @Lazy @Autowired AService aService; }
這裏 @Lazy 只要一個其實就能解決問題,也可以兩個都新增。
對於含有 @Async 註解的情況,也可以透過 @Lazy 註解來解決:
@Service public class AService { @Autowired @Lazy BService bService; @Async public void hello() { bService.hello(); } public BService getbService() { return bService; } } @Service public class BService { @Autowired AService aService; public void hello() { System.out.println("xxx"); } public AService getaService() { return aService; } }
如此,迴圈依賴可破!
總而言之一句話,@Lazy 註解是透過建立一箇中間代理層,來破解迴圈依賴的。
2. 原理分析
接下來我們再來簡單分析一下 @Lazy 註解處理的原始碼。
先來回顧一下屬性注入的過程:
在建立 Bean 的時候,原始 Bean 建立出來之後,會呼叫 populateBean 方法進行 Bean 的屬性填充。
接下來呼叫 postProcessAfterInstantiation 方法去判斷是否需要執行後置處理器,如果不需要,就直接返回了。
呼叫 postProcessProperties 方法,去觸發各種後置處理器的執行。
在第 3 步的方法中,呼叫 findAutowiringMetadata,這個方法又會進一步觸發 buildAutorwiringMetadata 方法,去找到包含了 @Autowired、@Value 以及 @Inject 註解的屬性或者方法,並將之封裝為 InjectedElement 返回。
呼叫 InjectedElement#inject 方法進行屬性注入。
接下來執行 resolvedCachedArgument 方法嘗試從快取中找到需要的 Bean 物件。
如果快取中不存在,則呼叫 resolveFieldValue 方法去容器中找到 Bean。
最後呼叫 makeAccessible 和 set 方法完成屬性的賦值。
在第 7 步中,呼叫 resolveFieldValue 方法去解析 Bean,@Lazy 註解的相關邏輯就是在這個方法中進行處理的。
resolveFieldValue 方法最終會執行到 resolveDependency 方法:
@Nullable public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { descriptor.initParameterNameDiscovery(getParameterNameDiscoverer()); if (Optional.class == descriptor.getDependencyType()) { return createOptionalDependency(descriptor, requestingBeanName); } else if (ObjectFactory.class == descriptor.getDependencyType() || ObjectProvider.class == descriptor.getDependencyType()) { return new DependencyObjectProvider(descriptor, requestingBeanName); } else if (javaxInjectProviderClass == descriptor.getDependencyType()) { return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName); } else { Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary( descriptor, requestingBeanName); if (result == null) { result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); } return result; } }
在這個方法中,首先會判斷注入的屬性型別是 Optional、ObjectFactory 還是 JSR-330 中的註解,我們這裏都不是,所以走最後一個分支。
在最後一個 else 中,首先呼叫 getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary 方法看一下是否需要延遲載入 Bean 物件,@Lazy 註解就是在這裏進行處理的。如果能夠延遲載入,那麼該方法的返回值就不為 null,就可以直接返回了,就不需要執行 doResolveDependency 方法了。
ContextAnnotationAutowireCandidateResolver#getLazyResolutionProxyIfNecessary:
@Override @Nullable public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null); }
大家看一下,這個方法首先會呼叫 isLazy 去判斷一下是否需要延遲載入,如果需要,則呼叫 buildLazyResolutionProxy 方法構建一個延遲載入的物件;如果不需要,則直接返回一個 null 即可。
protected boolean isLazy(DependencyDescriptor descriptor) { for (Annotation ann : descriptor.getAnnotations()) { Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class); if (lazy != null && lazy.value()) { return true; } } MethodParameter methodParam = descriptor.getMethodParameter(); if (methodParam != null) { Method method = methodParam.getMethod(); if (method == null || void.class == method.getReturnType()) { Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class); if (lazy != null && lazy.value()) { return true; } } } return false; }
這個判斷方法主要是檢查當前類中各種引數上是否含有 @Lazy 註解、方法、屬性以及類名上是否含有 @Lazy 註解,如果有,則返回 true,否則返回 false。
再來看 buildLazyResolutionProxy 方法:
private Object buildLazyResolutionProxy( final DependencyDescriptor descriptor, final @Nullable String beanName, boolean classOnly) { BeanFactory beanFactory = getBeanFactory(); final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory; TargetSource ts = new TargetSource() { @Override public Class<?> getTargetClass() { return descriptor.getDependencyType(); } @Override public boolean isStatic() { return false; } @Override public Object getTarget() { Set<String> autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null); Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null); if (target == null) { Class<?> type = getTargetClass(); if (Map.class == type) { return Collections.emptyMap(); } else if (List.class == type) { return Collections.emptyList(); } else if (Set.class == type || Collection.class == type) { return Collections.emptySet(); } throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(), "Optional dependency not present for lazy injection point"); } if (autowiredBeanNames != null) { for (String autowiredBeanName : autowiredBeanNames) { if (dlbf.containsBean(autowiredBeanName)) { dlbf.registerDependentBean(autowiredBeanName, beanName); } } } return target; } @Override public void releaseTarget(Object target) { } }; ProxyFactory pf = new ProxyFactory(); pf.setTargetSource(ts); Class<?> dependencyType = descriptor.getDependencyType(); if (dependencyType.isInterface()) { pf.addInterface(dependencyType); } ClassLoader classLoader = dlbf.getBeanClassLoader(); return (classOnly ? pf.getProxyClass(classLoader) : pf.getProxy(classLoader)); }
這個方法就是用來生成代理的物件的,這裏構建了代理物件 TargetSource,在其 getTarget 方法中,會去執行 doResolveDependency 獲取到被代理的物件,而 getTarget 方法只有在需要的時候纔會被呼叫。所以,@Lazy 註解所做的事情,就是在給 Bean 中的各個屬性注入值的時候,原本需要去 Spring 容器中找注入的物件,現在不找了,先給一個代理物件頂著,需要的時候再去 Spring 容器中查詢。
好啦,現在小夥伴們明白了 @Lazy 註解是如何解決 Spring 迴圈依賴了吧~
雖然解決了,但是我們在日常開發中,要是能避免迴圈依賴還是要去避免~