摘要:又是什么其實就是一種實現動態代理的技術,利用了開源包,先將代理對象類的文件加載進來,之后通過修改其字節碼并且生成子類。
在實際研發中,Spring是我們經常會使用的框架,畢竟它們太火了,也因此Spring相關的知識點也是面試必問點,今天我們就大話Aop。
特地在周末推文,因為該篇文章閱讀起來還是比較輕松詼諧的,當然了,更主要的是周末的我也在充電學習,希望有追求的朋友們也盡量不要放過周末時間,適當充電,為了走上人生巔峰,迎娶白富美。【話說有沒有白富美介紹(o???)】
接下來,直接進入正文。
為什么要有aop我們都知道Java是一種面向對象編程【也就是OOP】的語言,不得不說面向對象編程是一種及其優秀的設計,但是任何語言都無法十全十美,對于OOP語言來說,當需要為部分對象引入公共部分的時候,OOP就會引入大量的重復代碼【這些代碼我們可以稱之為橫切代碼】。而這也是Aop出現的原因,沒錯,Aop就是被設計出來彌補OOP短板的。Aop便是將這些橫切代碼封裝到一個可重用模塊中,繼而降低模塊間的耦合度,這樣也有利于后面維護。
Aop是什么東西學過Spring的都知道,Spring內比較核心的功能便是Ioc和Aop,Ioc的主要作用是應用對象之間的解耦,而Aop則可以實現橫切代碼【如權限、日志等】與他們綁定的對象之間的解耦,舉個淺顯易懂的小栗子,在用戶調用很多接口的地方,我們都需要做權限認證,判斷用戶是否有調用該接口的權限,如果每個接口都要自己去做類似的處理,未免有點sb了,也不夠裝x,因此Aop就可以派上用場了,將這些處理的代碼放到切片中,定義一下切片、連接點和通知,刷刷刷跑起來就ojbk了。
想要了解Aop,就要先理解以下幾個術語,如PointCut、Advice、JoinPoint。接下來盡量用白話文描述下。
PointCut【切點】
其實切點的概念很好理解,你想要去切某個東西之前總得先知道要在哪里切入是吧,切點格式如下:execution( com.nuofankj.springdemo.aop.Service.*(..))
可以看出來,格式使用了正常表達式來定義那個范圍內的類、那些接口會被當成切點,簡單明了。
Advice
Advice行內很多人都定義成了通知,但是我總覺得有點勉強。所謂的Advice其實就是定義了Aop何時被調用,確實有種通知的感覺,何時調用其實也不過以下幾種:
Before 在方法被調用之前調用
After 在方法完成之后調用
After-returning 在方法成功執行之后調用
After-throwing 在方法拋出異常之后調用
Around 在被通知的方法調用之前和調用之后調用
JoinPoint【連接點】
JoinPoint連接點,其實很好理解,上面又有通知、又有切點,那和具體業務的連接點又是什么呢?沒錯,其實就是對應業務的方法對象,因為我們在橫切代碼中是有可能需要用到具體方法中的具體數據的,而連接點便可以做到這一點。
先給出兩個業務內的接口,一個是聊天,一個是購買東西
接下來該給出說了那么久的切片了
可以從中看到PointCut【切點】是
execution( com.nuofankj.springdemo.aop.Service.*(..))
Advice是
Before
JoinPoint【連接點】是
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
代碼淺顯易懂,其實就是將ChatService和BuyService里邊給userId做權限校驗的邏輯抽出來做成切片。
那么如何拿到具體業務方法內的具體參數呢?
這里是定義了一個新的注解
作用可以直接看注釋,使用地方如下
可以看到對應接口使用了AuthPermission的注解,而取出的地方在于
是的,這樣便可以取出來對應的接口傳遞的userId具體是什么了,而校驗邏輯可以自己處理。
送佛送到西,不對,擼碼擼整套,接下來給出運行的主類
可以看到,上面有一個接口傳遞的userId是1,另一個是123,而上面權限認證只有1才說通過,否則會拋出異常。
運行結果如下
運行結果可想而知,1的通過驗證,123的失敗。
首先給出Main類
可以看到我這里用的是AnnotationConfigApplicationContext,解釋下
AnnotationConfigApplicationContext是一個用來管理注解bean的容器,所以我可以用該容器取得我定義了@Service注解的類的實例。
打斷點后,啟動程序,我們可以看到TestDemo的實例在idea的表現是這樣的
而BuyService的實例卻不同
我們可以從看到BuyService是SpringCGLIB強化過的一個實例,那么問題來了
為什么BuyService被強化過而TestDemo沒有?
SpringCGLIB又是什么?
Spring是在什么時候生成一個強化后的實例的?
帶著這些疑問,讓我們一步步從Spring源碼中找到答案。
為什么BuyService被強化過而TestDemo沒有?
這個問題比較簡單,我們可以看回上面我對切片的定義
可以從代碼中看出,我定義的切點是*Service命名的類,而TestDemo很明顯不符合這個設定,因此TestDemo逃過被強化的命運。
SpringCGLIB又是什么?
CGLIB其實就是一種實現動態代理的技術,利用了ASM開源包,先將代理對象類的class文件加載進來,之后通過修改其字節碼并且生成子類。結合demo來解讀便是SpringCGLIB會先將BuyService加載到內存中,之后通過修改字節碼生成BuyService的子類,該子類便是強化后的BuyService,上文看到的強化后的實例便是該子類的實例。
Spring是在什么時候生成一個強化后的實例的?
這個便厲害了,首先,我們要先從Spring如何加載切片入手。
【思考Time】 為什么我會選擇從切片入手呢?原因很簡單,Spring就是因為發現了切片,并且對切片進行解析后才知道了要強化哪些類。
切片的處理第一步便是要加上@Aspect注解,學過注解的都知道,注解的作用更多的是標志識別,也就是告訴Spring這個類要做相關特殊處理,因此我們可以基于該認識,反調該注解使用的地方
可以從截圖看出,我反調了@Aspect后定位到了AbstractAspectJAdvisorFactory類中的hasAspectAnnotation函數,并且攜帶參數clazz,因此我猜測該接口就是用來識別clazz是否使用了注解@Aspect的地方,于是我打上了斷點,并且加了條件 clazz == AuthAspect.class ,重新啟動后
我們看到確實被斷點到了,可以得出我的猜測是對的。
我們先看下斷點后做了什么事情,之后再看下具體是哪里進行了掃描。在斷點處按F8繼續往下走,最后發現
沒錯,可以看到最終是構建成了一個Advisor對象 ,并且放入了BeanFactoryAspectJAdvisorsBuilder中的advisorsCache中,這樣意味著Spring最終會將使用了@Aspect注解的類構建成Advisor對象后保存進BeanFactoryAspectJAdvisorsBuilder.advisorsCache中。
接下來我們看看具體是哪里進行了使用@Aspect注解的相關類的掃描,這次我斷點的地方在BeanFactoryAspectJAdvisorsBuilder中的advisorsCache調用了put的地方。
【思考Time】 為什么我會選擇在advisorsCache調用了put的地方打斷點呢?原因很簡單,因為我們上面已經分析出@Aspect注解的類構建成Advisor對象后保存進BeanFactoryAspectJAdvisorsBuilder.advisorsCache中,而我通過反調知道put的地方只有一個,因此我可以斷定在此處打斷點可以知道到底哪里進行了掃描的操作。
通過打斷點后我從idea的Frames面板中看到
沒錯,做了掃描@Aspect注解的掃描器是AbstractAutoProxyCreator類
我們可以從中看到AbstractAutoProxyCreator最終實現了InstantiationAwareBeanPostProcessor接口。
【思考Time】 這個接口有什么作用呢?具體可以看我前陣子寫的一篇文章:https://mp.weixin.qq.com/s/r2...
現在已經找到了掃描注解的地方,并且我們也看到了最終是生成了Advisor對象 ,并且放入了BeanFactoryAspectJAdvisorsBuilder中的advisorsCache中,那么Spring是在什么時候生成強化后的實例的呢?
接下來我的切入點是AbstractAutoProxyCreator中的postProcessAfterInitialization接口。
【思考Time】 之所以會選擇AbstractAutoProxyCreator為切入點,是因為通過命名可以看出這是SpringAop用來構建代理[強化]對象的地方,并且由于SpringCGLIB是先將目標類加載到內存中,之后通過修改字節碼生成目標類的子類,因此我猜測強化是在目標類實例化后觸發postProcessAfterInitialization的時候進行的。
因此我在postProcessAfterInitialization接口中做了斷點,并且加了調試條件。
可以看到我這里斷點到了ChatService這個類。
【思考Time】 為什么專門斷點ChatService這個類?之所以會專門定位這個類,因為我的切面的目標類就包含了ChatService,通過定位到該類,我們可以一步步捕捉Spring的強化操作。
我們可以看到,生成強化后的對象就藏在wrapIfNecessary中。
【思考Time】 為什么我會知道是生成強化后的對象就藏在wrapIfNecessary中呢?因為我通過調試發現,在調用了wrapIfNecessary接口后,返回的對象是強化后的對象。
那么問題來了,為什么Spring會知道ChatService類需要進行進行強化呢?我們可以從wrapIfNecessary中走入更深一層,通過調試,可以看到
在此處會從advisorsCache中根據aspectName取出對應的Advisor。拿到Advisor后,便是進行過濾的地方了,通過F8往后走,可以看到過濾的地方在AopUtils.canApply接口中。
可以看到此處傳進來的targetClass符合切面的要求,因此可以進行構建強化對象。
接下來讓我們看下真正產生強化對象的地方了
我們可以看到在AbstractAutoProxyCreator的createProxy函數中看到,最后會構造出一個強化后的chatService。
那么createProxy又做了什么呢?通過斷點一層層深入后,發現最后會到達
通過源碼分析,我們發現在AbstractAutoProxyCreator構建強化對象的時候是調用了createAopProxy函數,重點來了,我們可以看到針對targetClass,也就是ChatService做了判斷,如果targetClass有實現接口或者targetClass是Proxy的子類,那么使用的是JDK的動態代理實現AOP,如果不是才會使用CGLIB實現動態代理。
那么JDK實現的動態代理和CGLIB實現的動態代理有什么區別嗎?
首先動態代理可以分為兩種:JDK動態代理和CGLIB動態代理。從文中我們也可以看出,當目標類有接口的時候才會使用JDK動態代理,其實是因為JDK動態代理無法代理一個沒有接口的類。JDK動態代理是利用反射機制生成一個實現代理接口的匿名類,而CGLIB是針對類實現代理,主要是對指定的類生成一個子類,并且覆蓋其中的方法。
本來想一篇文章說完源碼跟蹤分析Aop和Aop的實現機制代理模式,發現源碼跟蹤分析已經很占篇幅了,因此沒辦法只能再開一篇文章專門闡述Aop的實現機制代理模式,期待下篇文章。
大家都知道,我有個習慣,在動手寫一篇文章之前會先將該文章相關的資料仔細琢磨一遍,然后再結合源碼再調試一遍,結果,說好的
看源碼也確實是
源碼確實有進行了是否是接口的判斷,但是問題來了,我調試的時候發現無論代理類是否有接口,最終都會被強制使用CGLIB代理,沒辦法,只能翻看SpringBoot的相關文檔,最終發現原來SpringBoot從2.0開始就默認使用Cglib代理了,好家伙,怪不得我調試半天找不到原因。
那么如何解決呢?肯定是通過配置啦,按照如下配置即可
在application.properties文件中配置 spring.aop.proxy-target-class=false
即可。
【劃重點】 曾經遇見過面試官問,SpringBoot默認代理類型是什么?看完該篇文章,我們就可以果斷的回答是Cglib代理了。通過調試代碼發現的規則,我想我這輩子都不會忘記這個默認規則。動態代理原理剖析 什么是代理
簡單來說,就是在運行的時候為目標類動態生成代理類,而在操作的時候都是操作代理類,代理模式有個顯而易見的好處,那便是可以在不改變對象方法的情況下對方法進行增強。試想下,我們在你必須要懂的Spring-Aop之應用篇有提到使用Aop來做權限認證,如果不用Aop,那么我們就必須要為所有需要權限認證的方法都加上權限認證代碼,聽起來就覺得蛋疼,你覺得對不對?
為什么不用靜態代理靜態代理類不是說不可以用,如果只有一個類需要被代理,那么自然可以用,如
這是在你必須要懂的Spring-Aop之應用篇使用的一個例子類,該類的作用只是打印出我要買東西。
代理類如下
可以看到這個BuyProxy代理類只是塞了一個IBuyServcie接口進行,而且自身也實現了接口IBuyService,而在buyItem方法被調用的時候會先做自己的操作,再調用塞進去的接口的buyItem方法。
測試類很簡單,如下
運行后很自然而然的打印出
靜態代理就是簡單,但是弊端也很明顯,如果有多個類都需要同樣的代理,都實現了同樣的接口,那么如果使用靜態代理的話,我們就要構造多個Proxy類,就會造成類爆炸。
而使用了Aop后,也就是動態代理后,便可以一次性解決該問題了,具體可以看你必須要懂的Spring-Aop之應用篇中的操作方法。
這里給出一個JDK動態代理的demo
首先給出一個簡單的業務類,Hello類和接口
真正實現了類的代理功能的其實就是這個實現了接口InvocationHandler的JdkProxy類
我們可以看到其中必須實現的方法是invoke,可以看到invoke方法的參數帶有Method對象,這個就是我們的目標Method,現在我們的目的就是要在這個Method在被調用前后實現我們的業務,可以看到在method.invoke反調前后實現了before和after業務。
這里再給出一個Main測試類,作用是取得Hello的代理類,然后調用其中的say方法。
運行結果如下
原理很簡單 在JdkProxyMain中hello調用say的時候,由于Hello已經被“代理”了,所以在調用say函數的時候其實是調用JdkProxy類中的invoke函數,而在invoke函數中先是實現了before函數才實現Object result = method.invoke(target, args),這一句其實是調用say函數,而后才實現after函數,于是這樣就可以不必在改動目標類的前提下實現代理了,并且不會像靜態代理那樣導致類爆炸。
CGLIB動態代理原理先給出一個Cglib動態代理的demo
核心類是實現了MethodInterceptor的CGlibProxy類
可以看到其中實現了方法intercept,先是在目標函數被調用前實現自己的業務,比如before()和after(),之后再通過 proxy.invokeSuper(obj, args) 觸發目標函數。
最后給出入口類
最后給出運行類,運行類如下
可以看到運行結果
原理很簡單 在CglibProxyMain中hello調用say的時候,由于Hello已經被“代理”了,所以在調用say函數的時候其實是調用CGlibProxy類中的intercept函數。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76193.html
摘要:動態地代理,可以猜測一下它的含義,在運行時動態地對某些東西代理,代理它做了其他事情。所以動態代理的內容重點就是這個。所以下一篇我們來細致了解下的到底是怎么使用動態代理的。 之前講了《零基礎帶你看Spring源碼——IOC控制反轉》,本來打算下一篇講講Srping的AOP的,但是其中會涉及到Java的動態代理,所以先單獨一篇來了解下Java的動態代理到底是什么,Java是怎么實現它的。 ...
摘要:作為面試官,我是如何甄別應聘者的包裝程度語言和等其他語言的對比分析和主從復制的原理詳解和持久化的原理是什么面試中經常被問到的持久化與恢復實現故障恢復自動化詳解哨兵技術查漏補缺最易錯過的技術要點大掃盲意外宕機不難解決,但你真的懂數據恢復嗎每秒 作為面試官,我是如何甄別應聘者的包裝程度Go語言和Java、python等其他語言的對比分析 Redis和MySQL Redis:主從復制的原理詳...
摘要:作為面試官,我是如何甄別應聘者的包裝程度語言和等其他語言的對比分析和主從復制的原理詳解和持久化的原理是什么面試中經常被問到的持久化與恢復實現故障恢復自動化詳解哨兵技術查漏補缺最易錯過的技術要點大掃盲意外宕機不難解決,但你真的懂數據恢復嗎每秒 作為面試官,我是如何甄別應聘者的包裝程度Go語言和Java、python等其他語言的對比分析 Redis和MySQL Redis:主從復制的原理詳...
摘要:,,面向切面編程。,切點,切面匹配連接點的點,一般與切點表達式相關,就是切面如何切點。例子中,注解就是切點表達式,匹配對應的連接點,通知,指在切面的某個特定的連接點上執行的動作。,織入,將作用在的過程。因為源碼都是英文寫的。 之前《零基礎帶你看Spring源碼——IOC控制反轉》詳細講了Spring容器的初始化和加載的原理,后面《你真的完全了解Java動態代理嗎?看這篇就夠了》介紹了下...
閱讀 2538·2023-04-26 00:57
閱讀 911·2021-11-25 09:43
閱讀 2221·2021-11-11 16:55
閱讀 2206·2019-08-30 15:53
閱讀 3592·2019-08-30 15:52
閱讀 1459·2019-08-30 14:10
閱讀 3379·2019-08-30 13:22
閱讀 1209·2019-08-29 11:18