天天看點

學Aop?看這篇文章就夠了!!!

在實際研發中,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連接配接點,其實很好了解,上面又有通知、又有切點,那和具體業務的連接配接點又是什麼呢?沒錯,其實就是對應業務的方法對象,因為我們在橫切代碼中是有可能需要用到具體方法中的具體資料的,而連接配接點便可以做到這一點。

給出一個Aop在實際中的應用場景

先給出兩個業務内的接口,一個是聊天,一個是購買東西

學Aop?看這篇文章就夠了!!!
學Aop?看這篇文章就夠了!!!

接下來該給出說了那麼久的切片了

學Aop?看這篇文章就夠了!!!

可以從中看到PointCut【切點】是

execution(* com.nuofankj.springdemo.aop.Service.(…))

Advice是

Before

JoinPoint【連接配接點】是

MethodSignature signature = (MethodSignature) joinPoint.getSignature();

Method method = signature.getMethod();

代碼淺顯易懂,其實就是将ChatService和BuyService裡邊給userId做權限校驗的邏輯抽出來做成切片。

那麼如何拿到具體業務方法内的具體參數呢?

這裡是定義了一個新的注解

學Aop?看這篇文章就夠了!!!

作用可以直接看注釋,使用地方如下

學Aop?看這篇文章就夠了!!!

可以看到對應接口使用了AuthPermission的注解,而取出的地方在于

學Aop?看這篇文章就夠了!!!

是的,這樣便可以取出來對應的接口傳遞的userId具體是什麼了,而校驗邏輯可以自己處理。

送佛送到西,不對,撸碼撸整套,接下來給出運作的主類

學Aop?看這篇文章就夠了!!!

可以看到,上面有一個接口傳遞的userId是1,另一個是123,而上面權限認證隻有1才說通過,否則會抛出異常。

運作結果如下

學Aop?看這篇文章就夠了!!!

運作結果可想而知,1的通過驗證,123的失敗。

Spring Aop做了什麼【開始源碼跟蹤閱讀】

首先給出Main類

學Aop?看這篇文章就夠了!!!

可以看到我這裡用的是AnnotationConfigApplicationContext,解釋下

AnnotationConfigApplicationContext是一個用來管理注解bean的容器,是以我可以用該容器取得我定義了@Service注解的類的執行個體。

打斷點後,啟動程式,我們可以看到TestDemo的執行個體在idea的表現是這樣的

學Aop?看這篇文章就夠了!!!

而BuyService的執行個體卻不同

學Aop?看這篇文章就夠了!!!

我們可以從看到BuyService是SpringCGLIB強化過的一個執行個體,那麼問題來了

  • 為什麼BuyService被強化過而TestDemo沒有?
  • SpringCGLIB又是什麼?
  • Spring是在什麼時候生成一個強化後的執行個體的?

帶着這些疑問,讓我們一步步從Spring源碼中找到答案。

為什麼BuyService被強化過而TestDemo沒有?

這個問題比較簡單,我們可以看回上面我對切片的定義

學Aop?看這篇文章就夠了!!!

可以從代碼中看出,我定義的切點是*Service命名的類,而TestDemo很明顯不符合這個設定,是以TestDemo逃過被強化的命運。

SpringCGLIB又是什麼?

CGLIB其實就是一種實作動态代理的技術,利用了ASM開源包,先将代理對象類的class檔案加載進來,之後通過修改其位元組碼并且生成子類。結合demo來解讀便是SpringCGLIB會先将BuyService加載到記憶體中,之後通過修改位元組碼生成BuyService的子類,該子類便是強化後的BuyService,上文看到的強化後的執行個體便是該子類的執行個體。

Spring是在什麼時候生成一個強化後的執行個體的?

這個便厲害了,首先,我們要先從Spring如何加載切片入手。

【思考Time】 為什麼我會選擇從切片入手呢?原因很簡單,Spring就是因為發現了切片,并且對切片進行解析後才知道了要強化哪些類。
學Aop?看這篇文章就夠了!!!

切片的處理第一步便是要加上@Aspect注解,學過注解的都知道,注解的作用更多的是标志識别,也就是告訴Spring這個類要做相關特殊處理,是以我們可以基于該認識,反調該注解使用的地方

學Aop?看這篇文章就夠了!!!

可以從截圖看出,我反調了@Aspect後定位到了AbstractAspectJAdvisorFactory類中的hasAspectAnnotation函數,并且攜帶參數clazz,是以我猜測該接口就是用來識别clazz是否使用了注解@Aspect的地方,于是我打上了斷點,并且加了條件 clazz == AuthAspect.class ,重新啟動後

學Aop?看這篇文章就夠了!!!

我們看到确實被斷點到了,可以得出我的猜測是對的。

我們先看下斷點後做了什麼事情,之後再看下具體是哪裡進行了掃描。在斷點處按F8繼續往下走,最後發現

學Aop?看這篇文章就夠了!!!

沒錯,可以看到最終是建構成了一個Advisor對象 ,并且放入了BeanFactoryAspectJAdvisorsBuilder中的advisorsCache中,這樣意味着Spring最終會将使用了@Aspect注解的類建構成Advisor對象後儲存進BeanFactoryAspectJAdvisorsBuilder.advisorsCache中。

接下來我們看看具體是哪裡進行了使用@Aspect注解的相關類的掃描,這次我斷點的地方在BeanFactoryAspectJAdvisorsBuilder中的advisorsCache調用了put的地方。

【思考Time】 為什麼我會選擇在advisorsCache調用了put的地方打斷點呢?原因很簡單,因為我們上面已經分析出@Aspect注解的類建構成Advisor對象後儲存進BeanFactoryAspectJAdvisorsBuilder.advisorsCache中,而我通過反調知道put的地方隻有一個,是以我可以斷定在此處打斷點可以知道到底哪裡進行了掃描的操作。
學Aop?看這篇文章就夠了!!!

通過打斷點後我從idea的Frames面闆中看到

學Aop?看這篇文章就夠了!!!

沒錯,做了掃描@Aspect注解的掃描器是AbstractAutoProxyCreator類

學Aop?看這篇文章就夠了!!!
學Aop?看這篇文章就夠了!!!

我們可以從中看到AbstractAutoProxyCreator最終實作了InstantiationAwareBeanPostProcessor接口。

【思考Time】 這個接口有什麼作用呢?具體可以看我前陣子寫的一篇文章:https://mp.weixin.qq.com/s/r2OEqsap6NgaEnNveO1mVg

現在已經找到了掃描注解的地方,并且我們也看到了最終是生成了Advisor對象 ,并且放入了BeanFactoryAspectJAdvisorsBuilder中的advisorsCache中,那麼Spring是在什麼時候生成強化後的執行個體的呢?

接下來我的切入點是AbstractAutoProxyCreator中的postProcessAfterInitialization接口。

【思考Time】 之是以會選擇AbstractAutoProxyCreator為切入點,是因為通過命名可以看出這是SpringAop用來建構代理[強化]對象的地方,并且由于SpringCGLIB是先将目标類加載到記憶體中,之後通過修改位元組碼生成目标類的子類,是以我猜測強化是在目标類執行個體化後觸發postProcessAfterInitialization的時候進行的。

是以我在postProcessAfterInitialization接口中做了斷點,并且加了調試條件。

學Aop?看這篇文章就夠了!!!

可以看到我這裡斷點到了ChatService這個類。

【思考Time】 為什麼專門斷點ChatService這個類?之是以會專門定位這個類,因為我的切面的目标類就包含了ChatService,通過定位到該類,我們可以一步步捕捉Spring的強化操作。

我們可以看到,生成強化後的對象就藏在wrapIfNecessary中。

【思考Time】 為什麼我會知道是生成強化後的對象就藏在wrapIfNecessary中呢?因為我通過調試發現,在調用了wrapIfNecessary接口後,傳回的對象是強化後的對象。

那麼問題來了,為什麼Spring會知道ChatService類需要進行進行強化呢?我們可以從wrapIfNecessary中走入更深一層,通過調試,可以看到

學Aop?看這篇文章就夠了!!!

在此處會從advisorsCache中根據aspectName取出對應的Advisor。拿到Advisor後,便是進行過濾的地方了,通過F8往後走,可以看到過濾的地方在AopUtils.canApply接口中。

學Aop?看這篇文章就夠了!!!

可以看到此處傳進來的targetClass符合切面的要求,是以可以進行建構強化對象。

接下來讓我們看下真正産生強化對象的地方了

學Aop?看這篇文章就夠了!!!

我們可以看到在AbstractAutoProxyCreator的createProxy函數中看到,最後會構造出一個強化後的chatService。

那麼createProxy又做了什麼呢?通過斷點一層層深入後,發現最後會到達

學Aop?看這篇文章就夠了!!!

通過源碼分析,我們發現在AbstractAutoProxyCreator建構強化對象的時候是調用了createAopProxy函數,重點來了,我們可以看到針對targetClass,也就是ChatService做了判斷,如果targetClass有實作接口或者targetClass是Proxy的子類,那麼使用的是JDK的動态代理實作AOP,如果不是才會使用CGLIB實作動态代理。

那麼JDK實作的動态代理和CGLIB實作的動态代理有什麼差別嗎?

首先動态代理可以分為兩種:JDK動态代理和CGLIB動态代理。從文中我們也可以看出,當目标類有接口的時候才會使用JDK動态代理,其實是因為JDK動态代理無法代理一個沒有接口的類。JDK動态代理是利用反射機制生成一個實作代理接口的匿名類,而CGLIB是針對類實作代理,主要是對指定的類生成一個子類,并且覆寫其中的方法。

Aop實作機制之代理模式

本來想一篇文章說完源碼跟蹤分析Aop和Aop的實作機制代理模式,發現源碼跟蹤分析已經很占篇幅了,是以沒辦法隻能再開一篇文章專門闡述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之應用篇使用的一個例子類,該類的作用隻是列印出我要買東西。

學Aop?看這篇文章就夠了!!!

代理類如下

學Aop?看這篇文章就夠了!!!

可以看到這個BuyProxy代理類隻是塞了一個IBuyServcie接口進行,而且自身也實作了接口IBuyService,而在buyItem方法被調用的時候會先做自己的操作,再調用塞進去的接口的buyItem方法。

測試類很簡單,如下

學Aop?看這篇文章就夠了!!!

運作後很自然而然的列印出

學Aop?看這篇文章就夠了!!!

靜态代理就是簡單,但是弊端也很明顯,如果有多個類都需要同樣的代理,都實作了同樣的接口,那麼如果使用靜态代理的話,我們就要構造多個Proxy類,就會造成類爆炸。

而使用了Aop後,也就是動态代理後,便可以一次性解決該問題了,具體可以看你必須要懂的Spring-Aop之應用篇中的操作方法。

JDK動态代理原理

這裡給出一個JDK動态代理的demo

首先給出一個簡單的業務類,Hello類和接口

學Aop?看這篇文章就夠了!!!
學Aop?看這篇文章就夠了!!!

真正實作了類的代理功能的其實就是這個實作了接口InvocationHandler的JdkProxy類

學Aop?看這篇文章就夠了!!!

我們可以看到其中必須實作的方法是invoke,可以看到invoke方法的參數帶有Method對象,這個就是我們的目标Method,現在我們的目的就是要在這個Method在被調用前後實作我們的業務,可以看到在method.invoke反調前後實作了before和after業務。

這裡再給出一個Main測試類,作用是取得Hello的代理類,然後調用其中的say方法。

學Aop?看這篇文章就夠了!!!

運作結果如下

學Aop?看這篇文章就夠了!!!

原理很簡單 在JdkProxyMain中hello調用say的時候,由于Hello已經被“代理”了,是以在調用say函數的時候其實是調用JdkProxy類中的invoke函數,而在invoke函數中先是實作了before函數才實作Object result = method.invoke(target, args),這一句其實是調用say函數,而後才實作after函數,于是這樣就可以不必在改動目标類的前提下實作代理了,并且不會像靜态代理那樣導緻類爆炸。

CGLIB動态代理原理

先給出一個Cglib動态代理的demo

學Aop?看這篇文章就夠了!!!

核心類是實作了MethodInterceptor的CGlibProxy類

學Aop?看這篇文章就夠了!!!

可以看到其中實作了方法intercept,先是在目标函數被調用前實作自己的業務,比如before()和after(),之後再通過 proxy.invokeSuper(obj, args) 觸發目标函數。

最後給出入口類

學Aop?看這篇文章就夠了!!!

最後給出運作類,運作類如下

學Aop?看這篇文章就夠了!!!

可以看到運作結果

學Aop?看這篇文章就夠了!!!

原理很簡單 在CglibProxyMain中hello調用say的時候,由于Hello已經被“代理”了,是以在調用say函數的時候其實是調用CGlibProxy類中的intercept函數。

學Aop?看這篇文章就夠了!!!