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

在實際研發中,Spring是咱們常常會使用的框架,畢竟它們太火了,也所以Spring相關的知識點也是面試必問點,今天咱們就大話Aop。 特意在週末推文,由於該篇文章閱讀起來仍是比較輕鬆詼諧的,固然了,更主要的是週末的我也在充電學習,但願有追求的朋友們也儘可能不要放過週末時間,適當充電,爲了走上人生巔峯,迎娶白富美。【話說有沒有白富美介紹(o≖◡≖)】面試

接下來,直接進入正文。spring

爲何要有aop

咱們都知道Java是一種面向對象編程【也就是OOP】的語言,不得不說面向對象編程是一種及其優秀的設計,可是任何語言都沒法十全十美,對於OOP語言來講,當須要爲部分對象引入公共部分的時候,OOP就會引入大量的重複代碼【這些代碼咱們能夠稱之爲橫切代碼】。而這也是Aop出現的緣由,沒錯,Aop就是被設計出來彌補OOP短板的。Aop即是將這些橫切代碼封裝到一個可重用模塊中,繼而下降模塊間的耦合度,這樣也有利於後面維護。編程

Aop是什麼東西

學過Spring的都知道,Spring內比較核心的功能即是Ioc和Aop,Ioc的主要做用是應用對象之間的解耦,而Aop則能夠實現橫切代碼【如權限、日誌等】與他們綁定的對象之間的解耦,舉個淺顯易懂的小栗子,在用戶調用不少接口的地方,咱們都須要作權限認證,判斷用戶是否有調用該接口的權限,若是每一個接口都要本身去作相似的處理,未免有點sb了,也不夠裝x,所以Aop就能夠派上用場了,將這些處理的代碼放到切片中,定義一下切片、鏈接點和通知,刷刷刷跑起來就ojbk了。app

想要了解Aop,就要先理解如下幾個術語,如PointCut、Advice、JoinPoint。接下來儘可能用白話文描述下。框架

PointCut【切點】 其實切點的概念很好理解,你想要去切某個東西以前總得先知道要在哪裏切入是吧,切點格式以下:execution(* com.nuofankj.springdemo.aop.Service.(..)) 能夠看出來,格式使用了正常表達式來定義那個範圍內的類、那些接口會被當成切點,簡單明瞭。ide

Advice Advice行內不少人都定義成了通知,可是我總以爲有點勉強。所謂的Advice其實就是定義了Aop什麼時候被調用,確實有種通知的感受,什麼時候調用其實也不過如下幾種:函數

  • Before 在方法被調用以前調用
  • After 在方法完成以後調用
  • After-returning 在方法成功執行以後調用
  • After-throwing 在方法拋出異常以後調用
  • Around 在被通知的方法調用以前和調用以後調用

JoinPoint【鏈接點】 JoinPoint鏈接點,其實很好理解,上面又有通知、又有切點,那和具體業務的鏈接點又是什麼呢?沒錯,其實就是對應業務的方法對象,由於咱們在橫切代碼中是有可能須要用到具體方法中的具體數據的,而鏈接點即可以作到這一點。源碼分析

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

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

圖片描述
圖片描述
接下來該給出說了那麼久的切片了
圖片描述
能夠從中看到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的失敗。

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

首先給出Main類

2

能夠看到我這裏用的是AnnotationConfigApplicationContext,解釋下

AnnotationConfigApplicationContext是一個用來管理註解bean的容器,因此我能夠用該容器取得我定義了@Service註解的類的實例。

打斷點後,啓動程序,咱們能夠看到TestDemo的實例在idea的表現是這樣的

3

而BuyService的實例卻不一樣

4

咱們能夠從看到BuyService是SpringCGLIB強化過的一個實例,那麼問題來了

  • 爲何BuyService被強化過而TestDemo沒有?
  • SpringCGLIB又是什麼?
  • Spring是在何時生成一個強化後的實例的?

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

爲何BuyService被強化過而TestDemo沒有?

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

5

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

SpringCGLIB又是什麼?

CGLIB其實就是一種實現動態代理的技術,利用了ASM開源包,先將代理對象類的class文件加載進來,以後經過修改其字節碼而且生成子類。結合demo來解讀即是SpringCGLIB會先將BuyService加載到內存中,以後經過修改字節碼生成BuyService的子類,該子類即是強化後的BuyService,上文看到的強化後的實例即是該子類的實例。

Spring是在何時生成一個強化後的實例的?

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

【思考Time】 爲何我會選擇從切片入手呢?緣由很簡單,Spring就是由於發現了切片,而且對切片進行解析後才知道了要強化哪些類。

6

切片的處理第一步即是要加上@Aspect註解,學過註解的都知道,註解的做用更多的是標誌識別,也就是告訴Spring這個類要作相關特殊處理,所以咱們能夠基於該認識,反調該註解使用的地方

7

能夠從截圖看出,我反調了@Aspect後定位到了AbstractAspectJAdvisorFactory類中的hasAspectAnnotation函數,而且攜帶參數clazz,所以我猜想該接口就是用來識別clazz是否使用了註解@Aspect的地方,因而我打上了斷點,而且加了條件 clazz == AuthAspect.class ,從新啓動後

8

咱們看到確實被斷點到了,能夠得出個人猜想是對的。 咱們先看下斷點後作了什麼事情,以後再看下具體是哪裏進行了掃描。在斷點處按F8繼續往下走,最後發現

13

沒錯,能夠看到最終是構建成了一個Advisor對象 ,而且放入了BeanFactoryAspectJAdvisorsBuilder中的advisorsCache中,這樣意味着Spring最終會將使用了@Aspect註解的類構建成Advisor對象後保存進BeanFactoryAspectJAdvisorsBuilder.advisorsCache中。

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

【思考Time】 爲何我會選擇在advisorsCache調用了put的地方打斷點呢?緣由很簡單,由於咱們上面已經分析出@Aspect註解的類構建成Advisor對象後保存進BeanFactoryAspectJAdvisorsBuilder.advisorsCache中,而我經過反調知道put的地方只有一個,所以我能夠判定在此處打斷點能夠知道到底哪裏進行了掃描的操做。

14

經過打斷點後我從idea的Frames面板中看到

19

沒錯,作了掃描@Aspect註解的掃描器是AbstractAutoProxyCreator類

11
12

咱們能夠從中看到AbstractAutoProxyCreator最終實現了InstantiationAwareBeanPostProcessor接口。

【思考Time】 這個接口有什麼做用呢?具體能夠看我前陣子寫的一篇文章:mp.weixin.qq.com/s/r2OEqsap6…

如今已經找到了掃描註解的地方,而且咱們也看到了最終是生成了Advisor對象 ,而且放入了BeanFactoryAspectJAdvisorsBuilder中的advisorsCache中,那麼Spring是在何時生成強化後的實例的呢? 接下來個人切入點是AbstractAutoProxyCreator中的postProcessAfterInitialization接口。

【思考Time】 之因此會選擇AbstractAutoProxyCreator爲切入點,是由於經過命名能夠看出這是SpringAop用來構建代理[強化]對象的地方,而且因爲SpringCGLIB是先將目標類加載到內存中,以後經過修改字節碼生成目標類的子類,所以我猜想強化是在目標類實例化後觸發postProcessAfterInitialization的時候進行的。

所以我在postProcessAfterInitialization接口中作了斷點,而且加了調試條件。

14

能夠看到我這裏斷點到了ChatService這個類。

【思考Time】 爲何專門斷點ChatService這個類?之因此會專門定位這個類,由於個人切面的目標類就包含了ChatService,經過定位到該類,咱們能夠一步步捕捉Spring的強化操做。

咱們能夠看到,生成強化後的對象就藏在wrapIfNecessary中。

【思考Time】 爲何我會知道是生成強化後的對象就藏在wrapIfNecessary中呢?由於我經過調試發現,在調用了wrapIfNecessary接口後,返回的對象是強化後的對象。

那麼問題來了,爲何Spring會知道ChatService類須要進行進行強化呢?咱們能夠從wrapIfNecessary中走入更深一層,經過調試,能夠看到

16

在此處會從advisorsCache中根據aspectName取出對應的Advisor。拿到Advisor後,即是進行過濾的地方了,經過F8日後走,能夠看到過濾的地方在AopUtils.canApply接口中。

17

能夠看到此處傳進來的targetClass符合切面的要求,所以能夠進行構建強化對象。 接下來讓咱們看下真正產生強化對象的地方了

18

咱們能夠看到在AbstractAutoProxyCreator的createProxy函數中看到,最後會構造出一個強化後的chatService。 那麼createProxy又作了什麼呢?經過斷點一層層深刻後,發現最後會到達

18

經過源碼分析,咱們發如今AbstractAutoProxyCreator構建強化對象的時候是調用了createAopProxy函數,重點來了,咱們能夠看到針對targetClass,也就是ChatService作了判斷,若是targetClass有實現接口或者targetClass是Proxy的子類,那麼使用的是JDK的動態代理實現AOP,若是不是纔會使用CGLIB實現動態代理。

那麼JDK實現的動態代理和CGLIB實現的動態代理有什麼區別嗎? 首先動態代理能夠分爲兩種:JDK動態代理和CGLIB動態代理。從文中咱們也能夠看出,當目標類有接口的時候纔會使用JDK動態代理,實際上是由於JDK動態代理沒法代理一個沒有接口的類。JDK動態代理是利用反射機制生成一個實現代理接口的匿名類,而CGLIB是針對類實現代理,主要是對指定的類生成一個子類,而且覆蓋其中的方法。

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之應用篇使用的一個例子類,該類的做用只是打印出我要買東西。

3

代理類以下

4

能夠看到這個BuyProxy代理類只是塞了一個IBuyServcie接口進行,並且自身也實現了接口IBuyService,而在buyItem方法被調用的時候會先作本身的操做,再調用塞進去的接口的buyItem方法。 測試類很簡單,以下

5

運行後很天然而然的打印出

6

靜態代理就是簡單,可是弊端也很明顯,若是有多個類都須要一樣的代理,都實現了一樣的接口,那麼若是使用靜態代理的話,咱們就要構造多個Proxy類,就會形成類爆炸。 而使用了Aop後,也就是動態代理後,即可以一次性解決該問題了,具體能夠看你必需要懂的Spring-Aop之應用篇中的操做方法。

JDK動態代理原理

這裏給出一個JDK動態代理的demo 首先給出一個簡單的業務類,Hello類和接口

7

8

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

9

咱們能夠看到其中必須實現的方法是invoke,能夠看到invoke方法的參數帶有Method對象,這個就是咱們的目標Method,如今咱們的目的就是要在這個Method在被調用先後實現咱們的業務,能夠看到在method.invoke反調先後實現了before和after業務。

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

10

運行結果以下

11

原理很簡單 在JdkProxyMain中hello調用say的時候,因爲Hello已經被「代理」了,因此在調用say函數的時候實際上是調用JdkProxy類中的invoke函數,而在invoke函數中先是實現了before函數才實現Object result = method.invoke(target, args),這一句實際上是調用say函數,然後才實現after函數,因而這樣就能夠沒必要在改動目標類的前提下實現代理了,而且不會像靜態代理那樣致使類爆炸。

CGLIB動態代理原理

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

13

核心類是實現了MethodInterceptor的CGlibProxy類

14

能夠看到其中實現了方法intercept,先是在目標函數被調用前實現本身的業務,好比before()和after(),以後再經過 proxy.invokeSuper(obj, args) 觸發目標函數。

最後給出入口類

15

最後給出運行類,運行類以下

15

能夠看到運行結果

16

原理很簡單 在CglibProxyMain中hello調用say的時候,因爲Hello已經被「代理」了,因此在調用say函數的時候實際上是調用CGlibProxy類中的intercept函數。

logo
相關文章
相關標籤/搜索