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

目錄

引入

到目前爲止,咱們已經完成了簡易的IOC和DI的功能,雖然相好比Spring來講確定是很是簡陋的,可是畢竟咱們是爲了理解原理的,也不必必定要作一個和Spring同樣的東西。到了如今並不能讓咱們鬆一口氣,前面的IOC和DI都還算比較簡單,這裏要介紹的AOP難度就稍微要大一點了。html

tips

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

AOP

什麼是AOP

Aspect Oriented Programming:面向切面編程,做用簡單來講就是在不改變原類代碼的前提下,對類中的功能進行加強或者添加新的功能。程序員

AOP在咱們開發過程當中使用頻率很是的高,好比咱們要在多個地方重用一段代碼的功能,這時咱們能夠選擇的方式不少,好比直接代碼拷貝,也能夠將代碼封裝成類或方法,使用時調用。可是問題是這種方式對代碼來講有着很強的侵入性,對於程序員來講,將重複的東西拷來拷去也是一件麻煩事。而AOP能夠很好的解決這類問題,在AOP中咱們能夠指定對一類方法進行指定須要加強的功能。好比咱們在系統中記錄數據修改的日誌,每一個對數據修改的方法都要記錄,可是其實徹底是同樣的方法,使用AOP能大大增長開發效率。github

AOP的一些概念

通知(advice):通知定義了一個切面在何時須要完成什麼樣的功能,通知和切點組成切面。正則表達式

切點(pointCut):切點定義了切面須要做用在什麼地方。spring

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

連接點(join points):鏈接點表示能夠被選擇用來加強的位置,鏈接點是一組集合,在程序運行中的整個週期中都存在。express

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

關於AOP的簡單分析

通知(advice)

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

用戶建立好的advice框架怎麼感知?框架如何對用戶註冊的不一樣的advice進行隔離?

這個問題很簡單,大多數人都明白,這就相似於Java中的JDBC,Java提供一套公共的接口,各個數據庫廠商實現Java提供的接口來完成對數據庫的操做。咱們這裏也提供一套用於AOP的接口,用戶在使用時對接口進行實現便可。

advice的時機有哪些?須要提供哪些接口?

這裏直接拿Spring中定義好的加強的時機。

  • Before——在方法調用以前調用通知
  • After——在方法完成以後調用通知,不管方法執行成功與否
  • After-returning——在方法執行成功以後調用通知
  • After-throwing——在方法拋出異常後進行通知
  • Around——通知包裹了被通知的方法,在被通知的方法調用以前和調用以後執行自定義的行爲

好了,咱們可使用一個接口來定義上面的處理方法,在用戶使用的時候實現方法便可,以下:

貌似差很少了,可是咱們須要注意到,用戶在使用advice的使用,不可能說每次都是須要對上訴幾種方式同時進行加強,更多多是隻須要一種方式。可是若是隻有一個接口的話就要求用戶每次都須要實現全部的方法,這樣顯的十分的不友好。

咱們應該讓這些不一樣的方法對於用戶來講是可選,須要什麼就實現哪個。那麼咱們須要將每個方法都對應一個接口嗎?不須要。上面的after(...)afterSuccess(...)都是在方法執行以後實現,不一樣在於一個須要成功後的返回值而另外一個不須要,這兩個能夠做爲一個實現由返回值區分。進行異常後的加強處理這要求對被執行的方法進行包裹住,捕獲異常。這就和環繞差很少了,二者能夠放一塊兒。

類圖:

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...)。若是單獨提供這些東西對用戶來講仍是比較麻煩的,而框架的做用是幫用戶簡化開發過程當中的流程,儘可能的簡單化。因此在這裏咱們能夠對用戶提供一個新的外觀(門面),讓用戶更加簡單的使用。這裏實際上是使用了外觀模式的思想。

當咱們在註冊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的內容緊密相關。因此你們最好仍是理一遍思路後能手動進行實現一次,這樣掌握起來也比較容易。

相關文章
相關標籤/搜索