Spring系列之AOP的原理及手動實現

寧願。發表於2018-12-21

目錄

引入

到目前為止,我們已經完成了簡易的IOC和DI的功能,雖然相比如Spring來說肯定是非常簡陋的,但是畢竟我們是為了理解原理的,也沒必要一定要做一個和Spring一樣的東西。到了現在並不能讓我們鬆一口氣,前面的IOC和DI都還算比較簡單,這裡要介紹的AOP難度就稍微要大一點了。

tips

本篇內容難度較大,每一步都需要理清思路,可能需要多看幾遍,多畫類圖和手動實現更容易掌握。

AOP

什麼是AOP

Aspect Oriented Programming:面向切面程式設計,作用簡單來說就是在不改變原類程式碼的前提下,對類中的功能進行增強或者新增新的功能。

AOP在我們開發過程中使用頻率非常的高,比如我們要在多個地方重用一段程式碼的功能,這時我們可以選擇的方式很多,比如直接程式碼拷貝,也可以將程式碼封裝成類或方法,使用時呼叫。但是問題是這種方式對程式碼來說有著很強的侵入性,對於程式設計師來說,將重複的東西拷來拷去也是一件麻煩事。而AOP可以很好的解決這類問題,在AOP中我們可以指定對一類方法進行指定需要增強的功能。比如我們在系統中記錄資料修改的日誌,每個對資料修改的方法都要記錄,但是其實完全是一樣的方法,使用AOP能大大增加開發效率。

AOP的一些概念

通知(advice):通知定義了一個切面在什麼時候需要完成什麼樣的功能,通知和切點組成切面。

切點(pointCut):切點定義了切面需要作用在什麼地方。

切面(Aspect):是通知和切點的組合,表示在指定的時間點什對指點的地方進行一些額外的操作。

連結點(join points):連線點表示可以被選擇用來增強的位置,連線點是一組集合,在程式執行中的整個週期中都存在。

織入(Weaving):在不改變原類程式碼的前提下,對功能進行增強。

Spring系列之AOP的原理及手動實現

關於AOP的簡單分析

通知(advice)

通知定義了一個切面在什麼時候需要完成什麼樣的功能,很明顯advice的實現不是由框架來完成,而是由使用者建立好advice然後註冊到框架中,讓框架在適當的時候使用它。這裡我們需要考慮幾個問題。

使用者建立好的advice框架怎麼感知?框架如何對使用者註冊的不同的advice進行隔離?

這個問題很簡單,大多數人都明白,這就類似於Java中的JDBC,Java提供一套公共的介面,各個資料庫廠商實現Java提供的介面來完成對資料庫的操作。我們這裡也提供一套用於AOP的介面,使用者在使用時對介面進行實現即可。

advice的時機有哪些?需要提供哪些介面?

這裡直接拿Spring中定義好的增強的時機。

  • Before——在方法呼叫之前呼叫通知
  • After——在方法完成之後呼叫通知,無論方法執行成功與否
  • After-returning——在方法執行成功之後呼叫通知
  • After-throwing——在方法丟擲異常後進行通知
  • Around——通知包裹了被通知的方法,在被通知的方法呼叫之前和呼叫之後執行自定義的行為

好了,我們可以使用一個介面來定義上面的處理方法,在使用者使用的時候實現方法即可,如下:

Spring系列之AOP的原理及手動實現

貌似差不多了,但是我們需要注意到,使用者在使用advice的使用,不可能說每次都是需要對上訴幾種方式同時進行增強,更多可能是隻需要一種方式。但是如果只有一個介面的話就要求使用者每次都需要實現所有的方法,這樣顯的十分的不友好。

我們應該讓這些不同的方法對於使用者來說是可選,需要什麼就實現哪一個。那麼我們需要將每一個方法都對應一個介面嗎?不需要。上面的after(...)afterSuccess(...)都是在方法執行之後實現,不同在於一個需要成功後的返回值而另一個不需要,這兩個可以作為一個實現由返回值區分。進行異常後的增強處理這要求對被執行的方法進行包裹住,捕獲異常。這就和環繞差不多了,兩者可以放一起。

類圖:

Spring系列之AOP的原理及手動實現

pointcut

advice基本就這樣了,下面就是pointcut了。說起切點,用過Spring中的AOP的肯定對切入點表示式比較瞭解了,在Spring中使用者通過切入點表示式來定義我們的增強功能作用在那一類方法上。這個切入點表示式十分的重要。對於我們的手寫AOP來說,也需要提供這樣的功能。當然表示式由使用者來寫,由我們的框架來解析使用者的表示式,然後對應到具體的方法上。

如何解析使用者定義的表示式?上面說到了,由一串字元來匹配一個或多個不同的目標,我們第一個反應肯定是正規表示式,很明顯這個功能使用正則是可以進行實現的。但實際上這樣的表示式還有很多。比如AspectJAnt path等。具體使用什麼就自己決定了,這裡我實現正則匹配這一種。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
複製程式碼
  1. 如何找到我們要增強的方法呢?

當我們確定好有哪些類的哪些方法需要增強,後面就需要考慮我們如何獲取到這些方法(對方法增強肯定需要獲取到具體的方法)。

  1. 有了表示式我們可以確定具體的類和方法,表示式只是定義了相對的路徑,如何根據相對路徑獲取Class檔案地址?

對bean例項的增強是在初始化的時候完成的,初始化的時候判斷如果需要增強,則通過代理生成代理物件,在返回時由該代理物件代替原例項被註冊到容器中。

  1. Class檔案有了,怎麼取到類中的方法?

在前面章節中我們獲取過方法,使用Class物件即可獲取所有的非私有方法。在實際呼叫被增強方法時,將該方法與所有的advice進行匹配,如果有匹配到advice,則執行相應的增強。當然我們並不需要每一次都需要遍歷獲取,為了效率可以對方法和增強的advice進行快取。

Aspect/Advisor

我們有了增強功能的實現和確定了需要增強那些方法。到了現在我們就需要將拿到的方法進行增強了。

在執行過程中對已有的類或方法的功能進行增強同時又不改變原有類的程式碼,這妥妥的代理模式嘛。如果不理解代理模式的可以看這個教程:代理模式,代理模式可以在執行期間對方法進行增強,很好的實現我們的需求。

到現在,使用者要實現AOP需要提供什麼呢?

使用者如果要實現AOP,首先必須提供一個Advice(通知)來增強功能,一個expression表示式來定義增強哪些方法,實際上還需要指定使用哪一個解析器來解析傳入的表示式(正則,AspectJ...)。如果單獨提供這些東西對使用者來說還是比較麻煩的,而框架的作用是幫使用者簡化開發過程中的流程,儘量的簡單化。所以在這裡我們可以對使用者提供一個新的外觀(門面),讓使用者更加簡單的使用。這裡其實是使用了外觀模式的思想。

Spring系列之AOP的原理及手動實現

當我們在註冊bean和呼叫方法時,對方法的增強會用到Advisor,所以我們還需要提供一個註冊和獲取Advisor的介面。

AdvisorRegistry

Weaving

現在我們有了切面,使用者也已經能夠比較簡單的來定義如何使用切面,最重要的一步到了,那就是我們應該如何對需要增強的類進行增強呢?什麼時候進行增強?

上面已經說過了對類和方法進行增強就使用代理模式來增強。那麼我們作為框架該在什麼什麼時候來增強呢?

這裡有兩種時機。一是在啟動容器初始化bean的時候就進行增強,然後容器中存放的不是bean的例項,而是bean的代理例項。二是在每一次使用bean的時候判斷一次是否需要增強,需要就對其增強,然後返回bean的代理例項。這兩種方法很明顯第一種比較友好,只是讓容器的啟動時間稍微長了一點,而第二種在執行時判斷,會使得使用者的體驗變差。

在初始化bean的那個過程來增強?會不會存在問題?

根據之前的介紹,我們的框架初始化bean是在BeanFactory中進行,還包括bean的例項化,引數注入以及將bean放入容器中等。很明顯對bean的增強應該是在bean例項化完成並在還沒有放進容器中的時候。那麼也就是在BeanFactory的doGetBean方法中了。這裡有一個小問題在於,doGetBean方法做的事情已經夠多了,繼續往裡加入程式碼無疑會使得程式碼大爆炸,很難維護也不易擴充套件。為了解決這個問題這裡我們可以使用觀察者模式來解決這一問題,將doGetBean方法中每一個過程都作為一個觀察者存在,當我們需要新增功能是既可以新增一個觀察者然後注入,這樣不會對已有程式碼做出改變。

定義一個觀察者的介面:

BeanPostProcessor

這裡我們暫時只定義了aop應用的觀察者,其他的比如例項化,引數注入後面慢慢加。

BeanPostProcessor是在BeanFactory中對bean進行操作時觸發,我們也應該在BeanFactory中加入BeanPostProcessor的列表和註冊BeanPostProcessor的方法。

BeanFactory

在這裡的觀察者模式的應用中,BeanFactory充當subject角色,BeanPostProcessor則充當observer的角色,BeanFactory監聽BeanPostProcessor,我們可以將功能抽出為一個BeanPostProcessor,將其註冊到BeanFactory中,這樣既不會使得BeanFactory中程式碼過多,同時也比較容易做到了功能的解耦,假設我們不需要某一個功能,那麼直接接觸繫結即可而不需要任何其他操作。在這裡我們只實現了Aop功能的註冊。

image

假設我們要對其他功能也抽為一個觀察者,那麼直接繼承BeanPostProcessor介面實現自己的功能然後註冊到BeanFactory中。

功能實現分析

現在介面有了,我們現在需要考慮如何來實現功能了。那麼我們現在梳理一下我們需要做什麼。

  1. 在進行bean建立的時候,需要判斷該bean是否需要被增強,這個工作是由AopPostProcessor介面來做,判斷是否需要被增強和通過哪種方式來增強(JDK代理還是cglib代理)。如果需要增強則建立代理物件,註冊到容器是則使用該代理物件。
  2. 在1中說到需要建立代理物件,那麼我們也就需要提供代理的實現,目前代理主要是通過JDK代理和cglib代理模式,兩者的主要區別在去JDK代理模式必須要求類實現了介面,而cglib則不需要。
  3. 在實際對例項增強方法呼叫時,框架需要對該方法的增強方法進行呼叫,如何進行呼叫以及存在多個增強方法是如何來呼叫。

現在我們對以上的問題分別分析解決。

代理實現

代理的實現就是常規的實現,我們提供對外建立代理例項的方法和執行方法的處理。

AopProxy

JDKDynamicProxy和CglibDynamicProxy共同實現了AopProxy介面,除此之外要實現代理JDKDynamicProxy還需實現InvocationHandler介面,CglibDynamicProxy還需實現MethodInterceptor介面。

可能有朋友注意到了,在建立代理的類中都有一個BeanFactory的變數,之所以會用到這一個型別的變數是因為當方法執行時匹配到advice增強時能從BeanFactory中獲取Advice例項。而Advisor中並沒有存Advice的例項,儲存的是例項名(beanName)。但是問題在於這個變數的值我們如何獲取,對於一般的bean我們可以從容器中獲取,而BeanFactory本身就是容器,當然不可能再從容器中獲取。我們首先梳理下獲取變數值的方法:

  1. 通過依賴注入從容器中獲取,這裡不合適。
  2. 直接建立一個新的值,這裡需要的是容器中的例項,重新建立新的值肯定沒了,如果再按照原流程走一次建立一模一樣的值無疑是一種愚蠢的做法,這裡也不合適。
  3. 傳參,如果方法的呼叫流程可以追溯到該變數整個流程,可以通過傳參的方式傳遞
  4. Spring中的做法,和3差不多,也是我們平時用的比較多的方法。提供一系列介面,介面唯一的作用就是用於傳遞變數的值,並且介面中也只有一個唯一的Set方法。

Aware

提供一個Aware父介面和一系列的子介面,比如BeanFactoryAware ,ApplicationContextAware用於將這些值放到需要的地方。如果那個類需要用到Spring容器的變數值,則直接實現xxxAware介面即可。Spring的做法是在某一個過程中檢測有哪些類實現了Aware介面,然後將值塞進去。

這裡我們的準備工作都已經差不多了,後面就是開始將定義好的介面中的功能實現了。

image

如果存在多個不同型別的增強方法時如何呼叫

由於在增強過程中,對於同一個方法可能有多個增強方法,比如多個環繞增強,多個後置增強等。通常情況下我們是通過一個for迴圈將所有方法執行,這樣的:

執行順序

但是這裡的問題在於,這中間的任何一個環繞方法都會執行一次原方法(被增強的方法),比如在環繞增強中的實現是這樣的:

//before working
//invoke 被加強的方法執行
//after working 
複製程式碼

這樣如果還是一個for迴圈執行的話就會導致一個方法被多次執行,所以for迴圈的方法肯定是不行的。我們需要的是一種類似於遞迴呼叫的方式巢狀執行,這樣的:

遞迴順序

前面的方法執行一部分進入另一個方法,依次進入然後按照反順序結束方法,這樣只需把我們需要加強的方法放在最深層次來執行就可以保證只執行依次了。而責任鏈模式可以很好的做到這一點。

呼叫流程的具體實現:

public class AopAdviceChain {

    private Method nextMethod;
    private Method method;
    private Object target;
    private Object[] args;
    private Object proxy;
    private List<Advice> advices;

    //通知的索引 記錄執行到第多少個advice
    private int index = 0;

    public AopAdviceChain(Method method, Object target, Object[] args, Object proxy, List<Advice> advices) {
        try {
            //對nextMethod初始化 確保呼叫正常進行
            nextMethod = AopAdviceChain.class.getMethod("invoke", null);
        } catch (NoSuchMethodException | SecurityException e) {
            e.printStackTrace();
        }

        this.method = method;
        this.target = target;
        this.args = args;
        this.proxy = proxy;
        this.advices = advices;
    }

    public Object invoke() throws InvocationTargetException, IllegalAccessException {
        if(index < this.advices.size()){
            Advice advice = this.advices.get(index++);
            if(advice instanceof BeforeAdvice){
                //前置增強
                ((BeforeAdvice) advice).before(method, args, target);
            }else if(advice instanceof AroundAdvice){
                //環繞增強
                return ((AroundAdvice) advice).around(nextMethod, null, this);
            } else if(advice instanceof AfterAdvice){
                //後置增強
                //如果是後置增強需要先取到返回值
                Object res = this.invoke();
                ((AfterAdvice) advice).after(method, args, target, res);
                //後置增強後返回  否則會多執行一次
                return res;
            }
            return this.invoke();
        }else {
            return method.invoke(target, args);
        }
    }
}
複製程式碼

在程式碼中可以看到,如果是前置增強則直接呼叫,而如果是環繞或者後置增強,則都不會立刻執行當前的增強方法,而是類似遞迴呼叫一樣,進行下一個執行。這樣就能保證被增強的方法不會被多次執行,同時對方法增強的順序也不會亂。

程式碼託管

在上面基本都只是分析了主要的原理和實現思路,在實際實現過程中涉及的類和藉口會更多,一些涉及到公共方法或者工具類上面都沒有列出,由於程式碼較多限於篇幅原因不在文章列出。若需看實現,程式碼已經全部託管到GitHub

小結

AOP的簡單實現這裡也算是完成了,AOP算是比較難的內容了,主要是涉及到知識點很多。使用的設計模式也很多,包括工廠模式,外觀模式,責任鏈模式等等。並且也和前面的IOC和DI的內容緊密相關。所以大家最好還是理一遍思路後能手動進行實現一次,這樣掌握起來也比較容易。

相關文章