Spring是java的重要框架俱饿。這塊的知識點也很多糟港,這里只是簡單的挑了比較高頻的兩個問題來講:一個是spring的aop的順序算是開胃菜琉兜,重點就是spring 的循環(huán)依賴凯正。下面開始正文:
Spring 的aop順序。
aop簡單來講就是面向切面編程豌蟋。spring利用aop可以對業(yè)務(wù)邏輯的各個部分進(jìn)行隔離廊散,從而使業(yè)務(wù)邏輯的各個部分之間耦合度降低。提高程序的可用性和開發(fā)效率梧疲。
這么說是很官方的語言允睹,舉一個實際工作中的例子:一般運營后臺的一些增刪改操作运准,我們常常會把操作人,操作參數(shù)缭受,ip等記錄成日志胁澳。這樣的話某一天某個管理員失心瘋故意損壞數(shù)據(jù)我們也可以通過這個日志定位到某個人,某個ip米者,從而來判斷是盜號了還是怎么樣的韭畸。
而這樣的記錄日志操作是每一個方法都要調(diào)用的。一般這個時候雖然我們也可以每個方法中調(diào)用同樣的代碼蔓搞。但是更好一點的方式是直接將記錄操作用aop的前置或者后置通知來做記錄日志的操作胰丁。下面簡單介紹下aop中常用注解:
- @Before:前置通知:目標(biāo)方法執(zhí)行之前執(zhí)行。
- @After:后置通知:目標(biāo)方法之后執(zhí)行(始終執(zhí)行)
- @AfterReturning:返回后通知:執(zhí)行方法結(jié)束前執(zhí)行(異常不執(zhí)行)
- @AfterThrowing:異常通知:出現(xiàn)異常時執(zhí)行
- @Around:環(huán)繞通知:環(huán)繞目標(biāo)方法執(zhí)行
以上的注解其實很容易理解喂分,甚至沒注解的話只看單詞都能猜的差不多锦庸。但是其實這些注解的執(zhí)行順序是很有意思的。比如@After和@AfterRentruning/@AfterThrowing哪個先執(zhí)行蒲祈?這些執(zhí)行順序其實不是一成不變的甘萧。
我們現(xiàn)在常用的是Spring boot框架。但是18梆掸,19年比較流行的是Spring boot1扬卷。近兩年用的都是Spring boot2.而sb1的底層是spring4.sb2的底層是spring5.
spring4和spring5中,這些注解的執(zhí)行順序是不同的沥潭。下面讓我們簡單的用代碼看一下順序邀泉。先用spring5嬉挡,也就是spring boot2版本測試下钝鸽,如下代碼:
隨便寫個方法:
然后寫環(huán)繞方法:
@Aspect
@Component
public class MyAspect {
@Before("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
public void before() {
System.out.println("<<<<<<<<<<<<<<<<<before-前置通知");
}
@After("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
public void after() {
System.out.println("<<<<<<<<<<<<<<<<after-后置通知");
}
@AfterReturning("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
public void aft() {
System.out.println("<<<<<<<<<<<<<<AfterReturning-返回通知");
}
@AfterThrowing("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
public void at() {
System.out.println("<<<<<<<<<<<<<<AfterThrowing-異常通知");
}
@Around("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
public void around(ProceedingJoinPoint point) throws Throwable{
System.out.println("<<<<<<<<<<<<<<<Around-環(huán)繞通知前");
point.proceed();
System.out.println("<<<<<<<<<<<<<<<Around-環(huán)繞通知后");
}
}
這個也沒什么業(yè)務(wù)邏輯庞钢,就是單純的打印語句以便于看清執(zhí)行順序拔恰。重點是我們的方法不要返回數(shù)值類型,要返回包裝類基括,不然會報個錯颜懊。
注意這里先用正常的數(shù)字訪問,然后y=0訪問(y=0會除以0報錯)
同樣的代碼风皿,我只改一下spring boot的版本:
![修改spring boot為1.x版本](https://upload-images.jianshu.io/upload_images/16553345-70252c1784f4bc74.png?im 一張對比下:
明顯能看出來河爹,這里主要的@After和@Around還有另外兩個異常或者正常返回的順序桐款。明顯在4的時候其實邏輯是不合適的咸这、因為@After是無論如何都是必須執(zhí)行的。既然這樣應(yīng)該是finally里的東西魔眨,也就是最后執(zhí)行媳维。所以在spring5改成了正衬鹧或者異常返回之后執(zhí)行。而環(huán)繞的話有點類似于方法的第一行和最后一行的一個輸出語句侄刽。代碼走到了會執(zhí)行指黎。但是代碼走不到就執(zhí)行不了。下面一張簡單的五種注解的位置:
這個我們可以理解為輸出語句就是切面的方法州丹。至于這個方法能不能執(zhí)行還得看代碼能不能走到這個方法中醋安。不要死記硬背執(zhí)行順序,而是要知道是在什么情況下才能執(zhí)行到墓毒。
這個比較簡單茬故,就不多說了,下面說說spring的循環(huán)依賴蚁鳖。
Spring中的循環(huán)依賴
跟這個問題相關(guān)的問題有一下:
- spring中的三級緩存是什么磺芭?三個Map有什么異同?
- 一般我們說的spring容器是什么醉箕?
- 如何檢測是否存在循環(huán)依賴钾腺?實際開發(fā)中見過循環(huán)依賴的異常么?
- 多例的情況下循環(huán)依賴問題為什么無法解決讥裤?
下面我們一點一點的介紹和學(xué)習(xí)放棒。
什么是循環(huán)依賴呢?
簡單來說就似乎多個bean之間相互依賴己英。形成了一個閉環(huán)间螟。比如A依賴于B,B依賴于C损肛,C依賴于A厢破。下面用最簡單的代碼表示下這種情況:
而通常來講,面試問Spring容器內(nèi)部如何解決循環(huán)依賴治拿,一定是指默認(rèn)的單例Bean中屬性互相引用的場景摩泪。如下代碼示例:
注意只要形成閉環(huán)就算是循環(huán)依賴,不管是2個劫谅,還是多個bean见坑。反正到這里我們起碼簡單的明白了什么是循環(huán)依賴。
兩種注入方式對循環(huán)依賴的影響
循環(huán)依賴的提出和解決其實官網(wǎng)上也都有說捏检,下面的官網(wǎng)截圖:
其實這里也涉及到了兩種spring的注入方式:構(gòu)造器注入和setter注入荞驴。
而官網(wǎng)很明確的說了,構(gòu)造器注入不推薦贯城,容易產(chǎn)生循環(huán)依賴的問題熊楼。并且如果檢測到代碼中存在這種現(xiàn)象會報異常。
而我們AB循環(huán)依賴的問題只要A的注入方式是setter并且是singleton冤狡,就不會有循環(huán)依賴的問題(spring容器中默認(rèn)都是單例的孙蒙。而且B這樣也可以解決)项棠。
下面代碼演示下兩種注入方式的循環(huán)依賴:
如上A,B兩個類。別說Spring了挎峦,我們自己都創(chuàng)建不出來A或者B對象了香追。因為創(chuàng)建A要先有B,創(chuàng)建B先有A坦胶。有點類似于先有雞先有蛋的哲學(xué)問題了透典。所以說這個構(gòu)造器的循環(huán)依賴是解決不了的。而setter方式注入的話顿苇,是可以解決的峭咒。
如果代碼是這樣無論A還是B都是很容易創(chuàng)建的了。而且其實我們在實際代碼中也經(jīng)常這樣做纪岁。比如說訂單商品表和訂單表凑队。有時為了特定的需求會互相注入的。然后因為是用setter方式注入幔翰,所以兩個對象都有默認(rèn)的無參構(gòu)造漩氨,所以很容易就可以創(chuàng)建對象。
其實上面說的都是純java代碼的循環(huán)依賴遗增。下面開始說spring容器的循環(huán)依賴(其實原理類似叫惊。畢竟spring也是對java代碼的封裝)。
Spring中的循環(huán)依賴
Spring容器中默認(rèn)的單例(singleton)的場景是支持循環(huán)依賴的做修。而原型(Prototype)的場景是不持支循環(huán)依賴的霍狰。
之所以這樣的原因就似乎因為:Spring內(nèi)部通過三級緩存來解決循環(huán)依賴的。
Spring中的三級緩存
說到spring的三級緩存饰及,一個很重要的類要被提到了:DefaultSingletonBeanRegistry
我們可以直接看下這個類的源碼:
這三個map中:
- singletonObjects:一級緩存(也叫單例池)蔗坯。存放已經(jīng)經(jīng)歷了完整生命周期的bean對象。
- earlySingtonObjects:二級緩存旋炒。存放早期暴露出來的bean對象步悠,bean的生命周期未結(jié)束(屬性還沒填充完)
- singletonFactories:三級緩存签杈。存放可以生成Bean的工廠瘫镇。
只有單例的bean會通過三級緩存提前暴露來解決循環(huán)依賴的問題。而非單例的bean每次從容器中獲取的都是一個新的對象答姥。都會重新創(chuàng)建铣除,所以非單例的bean是沒有緩存的,不會將其放到三級緩存中鹦付。
debug調(diào)試查看spring三級緩存工作運作
想要明白運作過程有一些前備知識簡單的說一下:
實例化/初始化:
- 實體化:內(nèi)存中申請了一塊內(nèi)存空間尚粘。(可以理解為想要蓋房子。跟國家申請了一塊地)
- 初始化屬性填充:完成屬性的各種賦值(開始在這塊地上動工敲长。打地基蓋房子等郎嫁。)
三級緩存:
- singletonObjects:一級緩存(也叫單例池)秉继。存放已經(jīng)經(jīng)歷了完整生命周期的bean對象。
- earlySingtonObjects:二級緩存泽铛。存放早期暴露出來的bean對象尚辑,bean的生命周期未結(jié)束(屬性還沒填充完)已經(jīng)實例化但沒有初始化。
- singletonFactories:三級緩存盔腔。存放可以生成Bean的工廠杠茬。
三級緩存的使用流程:
- A創(chuàng)建過程中需要B,于是A將自己放到三級緩存里面弛随,去實例化B瓢喉。
- B實例化的時候發(fā)現(xiàn)需要A,于是B先查一級緩存舀透,發(fā)現(xiàn)一級緩存中沒有A栓票。于是查二級緩存,發(fā)現(xiàn)還是沒有愕够,最后查三級緩存逗载,發(fā)現(xiàn)了A,然后把三級緩存里面的A放到了二級緩存中链烈,并且刪除了三級緩存中的A厉斟。
- B順利初始化完成,將自己放到一級緩存中(此時B里面的A依然是實例化狀態(tài))强衡。然后回來接著創(chuàng)建A擦秽,此時B已經(jīng)創(chuàng)建結(jié)束,直接從一級緩存中拿到B漩勤,然后完成創(chuàng)建感挥。并且A將自己存到一級緩存中。
四大方法:
- getSingleton:從容器中獲取這個單例bean(這個有多個重載方法越败。其中有不同的業(yè)務(wù)邏輯触幼,主要是用來把bean在不同級別的緩存中移動。)
- doCreateBean:這個其實包含了populateBean究飞,可以理解為初始化和實例化bean的方法置谦。但是從這里出來的bean不在一級緩存中,外層套了個getSingleton方法將bean放入到一級緩存亿傅。
- populateBean: 填充實例化的bean的屬性媒峡。如果有屬性沒有那么會遞歸創(chuàng)建屬性的bean的方法套娃。
- addSingleton:將這個初始化的bean放入到一級緩存中葵擎。
說了這么多前置知識谅阿,下面讓我們跟著debug走一遍代碼。親眼看看執(zhí)行過程:
代碼還是之前的注入A,B兩個組件的代碼。簡單貼出來:
如上代碼签餐。然后我們跟著代碼一步一步走:
注意最開始是幾個run方法一層一層走我就不說了寓涨,直接到正經(jīng)的方法中:
注意我紅色框起來的方法氯檐,這個方法執(zhí)行完會在公平打印出A,B創(chuàng)建完成缅茉。所以真正的創(chuàng)建過程在這個方法里。另外說一個細(xì)節(jié):在prepareContext方法執(zhí)行完后會打印spring boot的logo男摧。
其實我們完全可以第一個斷點就下在這個SpringApplication類的315行(就是refreshContext方法這行蔬墩。前面的都是跳來跳去的沒啥意義)
然后走進(jìn)這個方法:
還是這個類,走到了758行耗拓。同樣走完這行會A,B創(chuàng)建完拇颅。所以創(chuàng)建過程在這個方法里,進(jìn)去看:
然后這個方法一步一步走乔询,我是先無腦過樟插。然后注意看控制臺。確定這個創(chuàng)建過程發(fā)現(xiàn)的方法竿刁,一點點細(xì)化黄锤。這樣做就是要不斷debug。耐心點就好了食拜。繼續(xù)往下走:
然后發(fā)現(xiàn)finishBeanFactoryInitialization(beanFactory);這行代碼執(zhí)行的過程中創(chuàng)建了A,B兩個對象鸵熟。所以咱們可以把斷點設(shè)置在這行代碼中并且重新debug:
我框起來的方法是創(chuàng)建A,B的方法负甸。繼續(xù)往里走:
注意這里定位到了getBean方法流强。而且這個方法是創(chuàng)建所有的bean,容器中有好多bean呻待,所以這里要多跑幾遍打月。注意看當(dāng)前遍歷到是是不是我們測試用的這兩個對象。繼續(xù)往下走:
doGetBean方法是四大方法之一蚕捉。進(jìn)入到這個方法后發(fā)現(xiàn)有個從容器中獲取bean的方法:
同樣這個方法也是四大方法之一奏篙。雖然我們知道這個時候肯定是容器中沒有這個bean了,但是我們依然可以進(jìn)入看看方法:
然后方法一直往下走迫淹,
注意這里會走進(jìn)這個getSingleton方法中秘通,返回中調(diào)用了createBean這個方法。并且在這個方法中輸出了A創(chuàng)建千绪。我們進(jìn)入這個方法打個斷點伶氢。
這個代碼的調(diào)試就是順著一步一步往下走橙数,其實一來可以向我那樣無腦過,看控制臺輸出打印語句記住這行代碼下次調(diào)試進(jìn)入到方法里拱她,也可以每一個方法都點進(jìn)去瞅一眼,反正這個方法中很明顯應(yīng)該進(jìn)入的方法就是我框起來的這個doCreateBean瑞妇。
進(jìn)入到這個方法中前幾行的邏輯也挺簡單的稿静。如果這個bean構(gòu)造器是null。那么執(zhí)行createBeanInstance方法辕狰。注意這個時候我們確定還沒有創(chuàng)建a這個bean改备,所以可以考慮格外注意create的方法。
繼續(xù)走進(jìn)createBeanInstance方法:
首先這個方法是通過反射做了很多的事蔓倍。
emmmm...再一次跟丟了悬钳,不過這個時候a中的b是null:
因為上面的A只是單純的創(chuàng)建了,B也沒填充偶翅,所以我去翻了一下createBeanInstance的方法,從注釋可以看出來最后調(diào)用無參構(gòu)造器創(chuàng)建了對象:
所以這一步就不重新走了默勾,我們繼續(xù)往下:
繼續(xù)走這個方法,注意看這個判斷比較有意思聚谁,還記得當(dāng)時三級緩存中三個map中的二級緩存的名字就是earlySingletonObjects吧母剥。然后這個判斷中有個變量,spring中默認(rèn)值就是true:
所以這里的判斷其實本質(zhì)上就是判斷當(dāng)前bean是不是單例的形导。是的話就支持三級緩存环疼,也就是進(jìn)入到這個if方法中,如下方法:
這里是一個lambda朵耕,我是先進(jìn)入到方法中打個斷點然后往下debug的:
這個方法的邏輯很明了:如果一級緩存中沒有這個bean炫隶,那么把這個bean放入到三級緩存中,并且從二級緩存中移除這個bean(其實事實上這個時候二級緩存中是沒有a的阎曹,這個刪除就是一個空刪)等限。最后一個不是三級緩存中的map,所以先不管芬膝。繼續(xù)往下走望门。
注意現(xiàn)在這個時候:a這個bean已經(jīng)在三級緩存中了。
繼續(xù)往下走代碼又走到了四大方法之一:populateBean锰霜。也就是屬性填充筹误,注意這個時候我們要把b填充進(jìn)來了:
這塊的方法名字比較明顯,正常的情況下是沒有的癣缅,所以這個走了下面這個分支:
往這個方法里面走:
這個方法是需要b這個屬性但是b又沒有厨剪,所以解決這個value。我們繼續(xù)往里走:
然后繼續(xù)往resolveReference方法里走:
這個方法代碼走到了我紅框框起來的方法友存,getBean祷膳。這個名字很眼熟吧,我們走進(jìn)去:
到了這我們必須眼熟啊直晨,之前創(chuàng)建a的時候從這里開始的。所以說現(xiàn)在需要b,所以b也要和a一樣走一遍勇皇。這我就不一步步調(diào)試走了罩句,因為A,B是我們自己寫的敛摘,配置啥的都一樣门烂,我們盲猜就能猜到走和a一樣的流程。
直接用文字來講:
- getBean方法進(jìn)入流程
2.doGetBean方法開始去獲取bean - getSingleton方法試圖直接從容器中拿bean(這個步驟沒啥好說的兄淫,但是第一次進(jìn)來肯定獲取不到屯远,所以走下面的步驟)
- 如果沒有則再走getSingleton方法(注意這里和第三步的getSingleton是兩個方法。上一步單純的三級緩存中拿捕虽。而這個方法中會有業(yè)務(wù)邏輯)
- getSingleton方法中有個getObject方法是用匿名內(nèi)部類的方式書寫的(第四步中寫的)慨丐。是createBean方法。
- createBean方法中經(jīng)過一系列判斷薯鳍,進(jìn)入doCreateBean方法
- doCreateBean方法中調(diào)用createBeanInstance利用反射真正的創(chuàng)建了B這個bean(但是這個時候是無屬性填充的)
- 創(chuàng)建完后調(diào)用addSingletonFactory試圖把這個bean添加到工廠(如果一級緩存中沒有這個bean咖气,則把這個bean從二級緩存中刪除,放到三級緩存挖滤,但是其實正常來講二級緩存中也沒這個bean崩溪,所以多一步刪除為了保險吧)
- 當(dāng)前b這個bean已經(jīng)完成初始化,接下來進(jìn)入populateBean方法斩松,進(jìn)行屬性填充
- 我們發(fā)現(xiàn)b中有個屬性a伶唯,因為a我們之前已經(jīng)實例化了,所以容器中有a惧盹,所以調(diào)用applyPropertyValues方法填充這個屬性乳幸。
- applyPropertyValues方法中對所有屬性進(jìn)行resolveValueIfNecessary方法判斷,a進(jìn)入此方法
- resolveValueIfNecessary方法返回值是resolveReference方法的結(jié)果
- resolveReference方法會走進(jìn)getBean方法試圖從容器中拿a
- 循環(huán)走到getBean方法的doGetBean方法钧椰,進(jìn)入到getSingleton方法獲取bean
- 這個方法是第一個getSingleton方法粹断,從三級緩存中依次獲取bean的。而且注意這個時候a已經(jīng)在三級緩存中了嫡霞,所以進(jìn)入最后一個邏輯中瓶埋,從三級緩存中獲取bean刪除,并且放入到二級緩存中诊沪。而且這個doGetBean方法中會直接將a返回养筒。
- 將a填充到b的這個屬性中。
- 注意這里是重點端姚。上面的邏輯中從第5步開始這些代碼都發(fā)生在一個匿名內(nèi)部類中晕粪。而這個代碼的外層是 getSingleton方法。所以現(xiàn)在回到這個方法中渐裸。
- 這個getSingleton方法的最后一步是addSingleton方法巫湘。(getSingleton方法有多個重載装悲,這里不要記混了)
- 這個方法將此bean也就是b放入一級緩存中,并且刪除二三級緩存的此bean剩膘。
- 至此b實例化完成并且初始化完成衅斩。
-
我們創(chuàng)建b是在a需要b的時候走進(jìn)來的盆顾。當(dāng)b創(chuàng)建完成繼續(xù)走a的填充屬性的代碼怠褐。把b填充給a。a也初始化完成您宪。
而三級緩存能解決循環(huán)依賴的主要原因:bean的創(chuàng)建是分為創(chuàng)建原始bean對象和填充對象屬性和初始化兩個步驟的奈懒。也就是說Spring解決循環(huán)依賴依靠的是bean的“中間態(tài)”這個概念。中間態(tài)是指已經(jīng)實例化但是還沒初始化的狀態(tài)宪巨。
三級緩存的學(xué)名:
- 一級緩存:單例池磷杏。
- 二級緩存:提前曝光對象
- 三級緩存: 提前曝光對象工廠
所以循環(huán)依賴中我們可以先解決有沒有的問題。然后再去一點點填充屬性捏卓。而之所以必須單例才能實現(xiàn)循環(huán)依賴也是這個原因极祸。我感覺說到這里循環(huán)依賴就說的挺明白了。
一點不夸張的說這段debug我用了三四個小時才算是理明白這個代碼的走向怠晴,流程什么的遥金。而且就像一開始說的一不小心就debug丟了,一切從頭再來蒜田。但是這個是真的很有意思的一個東西稿械,有時候調(diào)著調(diào)著就會覺得寫出這種代碼的人簡直神仙。首先閱讀spring源碼可能除了吹NB不會有什么立刻馬上的明顯的提高冲粤。畢竟工作中寫個循環(huán)依賴啥的太扯了美莫。但是閱讀本身是一個開拓思路的好途徑。會有一種恍然大悟的感覺梯捕,啊厢呵,原來代碼還能這么寫。另外也是一個學(xué)習(xí)方式傀顾,畢竟一般跳槽大多數(shù)都會接手現(xiàn)有項目襟铭,所以學(xué)會閱讀源碼,快速理解源碼都挺有用的锣笨。甚至換個角度:看別人寫的好的小說是放松蝌矛,看別人寫的好的代碼不也是享受和放松么?這是一種很有成就感也很有意義的事错英。學(xué)會享受閱讀源碼入撒,尤其是比較復(fù)雜的源碼真的不錯。
另外我一直強調(diào)的一個觀點:別難為自己椭岩。如果在代碼中找不到樂趣茅逮,看代碼如上墳璃赡,就換個工作吧。這個社會搬磚也能養(yǎng)活自己献雅,還不用動腦碉考。干嘛非要去做讓自己抑郁的事呢?如果只是為了混日子也沒必要學(xué)這些挺身,還是那句話侯谁,放過自己。
本篇筆記就記到這里章钾,如果稍微幫到你了記得點個喜歡點個關(guān)注墙贱。也祝大家工作順順利利,生活健健康康~贱傀!另外因為我也不知道我圖文表述的夠不夠清楚惨撇,如果有同樣在閱讀spring源碼的小伙伴可以留言或者私聊我加個好友一起討論呀。