Spring AOP學習筆記01:AOP概述

木瓜芒果發表於2020-06-03

1. AOP概述

  軟體開發一直在尋求更加高效、更易維護甚至更易擴充套件的方式。為了提高開發效率,我們對開發使用的語言進行抽象,走過了從彙編時代到現在各種高階語言繁盛之時期;為了便於維護和擴充套件,我們對某些相同的功能進行歸類並使之模組化,衝出了最初的"原始部落",走過了從過程化程式設計到物件導向程式設計(OOP)的"短暫而漫長"的歷程。但不管走過的路有多長,多麼坎坷,我們一直沒有停止尋找更加完美、更加高效的軟體開發方法,過去如此,現在亦然。

  當OOP被提出來,以取代過去基於過程化程式設計的開發方法時,或許那個時代的人都會以為,物件導向程式設計和麵向物件的軟體開發就是我們一直追求的那顆能夠搞定一切的"銀彈"。但不得不承認的是,即使物件導向的軟體開發模式,依然不能很好地解決軟體開發中的所有問題。

  軟體開發的目的,最終是為了解決各種需求,包括業務需求和系統需求。使用物件導向方法,我們可以對業務需求等普通關注點進行很好的抽象和封裝,並且使之模組化。但對於系統需求(比如日誌記錄、許可權驗證、事務管理等)一類的關注點來說,情況卻有所不同。

  對於業務需求而言,需求與其具體實現之間的關係基本上是一對一的。我們可以在系統中某一個確定的點找到針對這種需求的實現,無論從開發還是維護的角度,都比較方便。比如電商系統中的賬戶管理模組、訂單模組、支付模組等,可以很容易地按照功能劃分模組並完成開發。

  但是,事情並沒有結束!開發中為了除錯或在進入生產環境後為了對系統進行監控,我們需要為這些業務需求的實現物件新增日誌記錄功能;或者,業務方法的執行需要一定的許可權限制,那麼方法執行前肯定需要有相應的安全檢查功能。而這些則屬於系統需求的範疇。雖然需求都很明確(加入日誌記錄、加入安全檢查),但是要將這些需求以物件導向的方式實現並整合到整個的系統中去,可就不是一個需求對應一個實現那麼簡單了,系統中的每個業務物件都需要加入日誌記錄,加入相應的安全檢查,那麼,這些需求的實現程式碼就會遍及所有業務物件。

  對於系統中普通的業務關注點,OOP可以很好地對其進行分解並使之模組化,但卻無法更好地避免類似於系統需求的實現在系統中各處散落這樣的問題。所以,我們要尋求一種更好的方法,它可以在OOP的基礎上更上一層樓,提出一套全新的方法論來避免以上問題,也可以提供某種方法對基於OOP的開發模式做一個補足,幫助OOP以更好的方式解決以上問題。迄今為止,我們還找不到比OOP更加有效的軟體開發模式。不過,我們找到了後者,那就是AOP,對OOP的補足。

  AOP全稱為Aspect-Oriented Programming,中文通常翻譯為面向方面程式設計。使用AOP,我們可以對類似於Logging和Security等系統需求進行模組化的組織,簡化系統需求與實現之間的對比關係,進而使得整個系統的實現更具模組化。

  對於一個軟體系統而言,日誌記錄、安全檢查、事務管理等系統需求就像一把把刀“惡狠狠”地橫切到我們組織良好的各個業務功能模組之上。以AOP的行話來說,這些系統需求是系統中的橫切關注點(cross-cutting concern)。使用傳統方法,我們無法更好地以模組化的方式,對這些橫切關注點進行組織和實現。所以AOP引入了Aspect的概念,用來以模組化的形式對系統中的橫切關注點進行封裝。Aspect 之對於AOP,就相當於Class之對於OOP。我們說過AOP僅是對OOP方法的一種補足,當我們把以Class形式模組化的業務需求和以Aspect形式模組化的系統需求拼裝到一起的時候,整個系統就算完成了。

 

2. AOP相關概念

  在進一步學習Spring AOP之前,我們還需要了解一下AOP涉及的相關概念:

2.1 切點(JoinPoint)

  在系統執行之前,AOP的功能模組都需要織入到OOP的功能模組中。所以,要進行這種織入過程,我們需要知道在系統的哪些執行點上進行織入操作,這些將要在其之上進行織入操作的系統執行點就稱之為切點(Joinpoint)。對應到spring中可以理解為具體攔截的某個業務點。

  以下是一些較為常見的Joinpoint型別

  • 方法呼叫(Method Call)。當某個方法被呼叫的時候所處的程式執行點。
  • 方法呼叫執行(Method Call execution)。也可以稱之為方法執行,該Joinpoint型別代表的是某個方法內部執行開始時點,這需要與上面的方法呼叫型別的Jointpoint進行區分。方法呼叫(method call)是在呼叫物件上的執行點,而方法執行(method execution)則是在被呼叫到的方法邏輯執行的時點,對於同一物件,方法呼叫要先於方法執行。
  • 構造方法呼叫(Constructor Call)。程式執行過程中對某個物件呼叫其構造方法進行初始化的時點。
  • 構造方法執行(Constructor Call Execution)。構造方法執行和構造方法呼叫之間的關係類似於方法執行和方法呼叫之間的關係,指的是某個物件構造方法內部執行的開始時點。
  • 欄位設定(Field Set)。物件的某個屬性通過setter方法被設定或者直接被設定的時點。
  • 欄位獲取(Field Get)。物件的某個屬性通過getter方法獲取或者直接訪問的時點。
  • 異常處理(Exception Handler Execution)。在某些型別異常丟擲後,對應的異常處理邏輯執行的時點。
  • 類初始化(Class initialization)。類中某些靜態型別或者靜態塊的初始化時點。

  基本上程式執行過程中你認為必要的執行時點都可以作為Joinpoint,但是對於一些位置,具體的AOP實現產品在捕捉的時候可能存在一定的困難,或者能夠實現但付出太多卻可能收效甚微。在Spring AOP中最常見的就是前面的方法執行型別的Joinpoint。

2.2 切面(Pointcut)

  Pointcut概念代表的是JointPoint的表述方式。將橫切邏輯織入當前系統的過程中,需要參照Pointcut規定的Jointpoint資訊,才可以知道應該往系統的哪些Joinpoint上織入橫切邏輯。

一個Pointcut可以指定系統中符合條件的一組Joinpoint,但是其是如何來指定的呢?通常有如下幾種方式:

  • 直接指定Joinpoint所在方法名稱。這種形式的Pointcut表述方式比較簡單,而且功能單一,通常只限於支援方法級別Joinpoint的AOP框架。並且這種方式只能一個一個指定,所以通常只限於Joinpoint較少且較為簡單的情況。

  • 正規表示式。這是比較普遍的Pointcut表達方式,可以充分利用正規表示式的強大功能來歸納表述符合某種條件的多組Joinpoint。幾乎現在大部分的Java平臺的AOP產品都支援這種形式的Pointcut表達形式,包括Jboss AOP、Spring AOP以及AspectWerkz等。

  • 使用特定的Pointcut表述語言。這是一種最為強大的表達Pointcut的方式,很靈活,但具體實現起來可能會很複雜,需要設計該表述語言的語法,實現相應的直譯器等許多工作。AspectJ使用這種方式來指定Pointcut,它提供了一種類似於正規表示式的針對Pointcut的表述語言,在表達Pointcut方面支援比較完善,而且Spring 2.0之後也是支援這種方式。

2.3 通知(Advice)

  Advice是單一橫切關注點邏輯的載體,它代表將會織入到Joinpoint的橫切邏輯。如果將Aspect比作OOP中的Class,那麼Advice就相當於Class中的Method。

  按照Advice在Jointpoint位置執行時機的差異或者完成功能的不同,Advice可以分成多種具體形式。

  • Before Advice

  Before Advice是在Joinpoint指定位置之前執行的Advice型別。通常,它不會中斷程式執行流程,但如果必要,可以通過在Before Advice中丟擲異常的方式來中斷當前程式流程。如果當前Before Advice將被織入到方法執行型別的Joinpoint,那麼這個Before Advice就會先於方法執行而執行。   通常,可以使用Before Advice做一些系統的初始化工作,比如設定系統初始值,獲取必要系統資源。

  • After Advice

  顧名思義,After Advice就是在相應連線點之後執行的Advice型別,但該型別的Advice還可以細分為三種:

  After returning Advice。只有當前Joinpoint處執行流程正常完成後,After returning Advice才會執行。

  After throwing Advice。又稱Throws Advice,只有在當前Joinpoint執行過程中丟擲異常的情況下,才會執行。比如某個方法執行型別的Joinpoint丟擲某異常而沒有正常返回。

  After Advice。或許叫After (Finally) Advice更為確切,該型別Advice不管Joinpoint處執行流程是正常終了還是丟擲異常都會執行,就好像Java中的finally塊一樣。

  • Around Advice

  Around Advice對附加其上的Joinpoint進行"包裹",可以在Joinpoint之前和之後都指定相應的邏輯,甚至於中斷或者忽略Joinpoint處原來程式流程的執行。

2.4 Aspect

  Aspect是對系統中的橫切關注點邏輯進行模組化封裝的AOP概念實體,可以理解為攔截器類,其中會定義切點以及攔截處理邏輯。通常情況下,Aspect可以包含多個Pointcut以及相關Advice定義。在Spring中,是通過使用@AspectJ註解並結合普通POJO來宣告Aspect的。

@AspectJ
public class AspectClass{
    // pointcut 定義

    // advice 定義
}

2.5 目標物件

  符合Pointcut所指定的條件,將在織入過程中被織入橫切邏輯的物件,稱為目標物件(Target Object)。

 

3. Spring AOP

  AOP只是一種理念,要實現這種理念,通常需要一種現實的方式。Spring AOP就是一款AOP的實現產品,Spring AOP是Spring核心框架的重要組成部分,通常認為它與Spring的IoC容器以及Spring框架對其他JavaEE服務的整合共同組成了Spring框架的"質量三角",足見其地位之重要。

  在Java語言的基礎之上,Spring AOP對AOP的概念進行了適當的抽象和實現,使得每個AOP的概念都可以落到實處,在詳細學習Spring AOP概念實體之前,我們有必要先看一下其是如何運作的。

  Spring AOP從最初發布以來,一直延續了最初的設計,也就是採用動態代理機制和位元組碼生成技術來實現基於Java語言的簡單而強大的AOP框架。與最初的AspectJ採用編譯器將橫切邏輯織入目標物件不同,動態代理機制和位元組碼生成都是在執行期間為目標物件生成一個代理物件,再將橫切邏輯織入到這個代理物件中,系統最終使用的是織入了橫切邏輯的代理物件,而不是真正的目標物件。

  要理解這種差別以及最終可以達到的效果,有必要先從動態代理機制的根源--代理模式(Proxy Pattern)開始說起。。。

 

3.1 設計模式之代理模式

  說到代理,舉幾個簡單的例子,比如房地產中介就是一種代理,我們偶爾使用的網路代理也是一種代理,類似例子很多,就不一一列舉了。代理處於訪問者與被訪問者之間,可以隔離這兩者之間的直接互動,訪問者與代理打交道就好像在跟被訪問者在打交道一樣,因為代理通常幾乎會全權擁有被代理者的職能,代理能夠處理的訪問請求就不必要勞煩被訪問者來處理了。從這個角度來講,有兩個好處:

  • 代理可以減少被訪問者的負擔;
  • 即使代理最終要將訪問請求轉發給真正的被訪問者,它也可以在轉發訪問請求之前或者之後加入特定的邏輯,比如安全訪問限制;

  在軟體系統中,代理機制的實現有現成的設計模式支援,即代理模式。在代理模式中通常涉及4種角色:

  • ISubject。該介面是對被訪問者或者被訪問資源的抽象。在嚴格的設計模式中,這樣的抽象介面是必須的。
  • SubjectImpl。這是被訪問者或者被訪問資源的具體實現類。如果你要訪問某位明星,那麼SubjectImpl就是你想要訪問的明星;如果你想要買房子,那麼SubjectImpl就是房主。
  • SubjectProxy。這是被訪問者或者被訪問資源的代理實現類,該類持有一個ISubject介面的具體例項。在這個場景中,我們要對SubjectImpl進行代理,那麼SubjectProxy現在持有的就是SubjectImpl的例項。
  • Client。這代表訪問者的抽象角色,Client將會訪問ISubject型別的物件或者資源。在這個場景中,Client將會請求具體的SubjectImpl例項,但Client無法直接請求其真正要訪問的資源SubjectImpl,而是必須通過ISubject資源的訪問代理類SubjectProxy進行。

  SubjectImpl和SubjectProxy都實現了相同的介面ISubject,而SubjectProxy內部持有SubjectImpl的引用。當Client通過request()請求服務的時候,SubjectProxy將轉發該請求給SubjectImpl。從這個角度來說,SubjectProxy反而有多此一舉之嫌了,不過SubjectProxy的作用不只侷限於請求的轉發,更多時候是對請求新增更多訪問限制。SubjectImpl和SubjectProxy之間的呼叫關係如下程式碼所示:

public class SubjectProxy implements ISubject{
    private ISubject subject;   // Inject SubjectImpl to SubjectProxy
    public String request(){
        // add pre-process logic if necessary

        String originalResult = subject.request();

        // add post process logic if necessary

        return "Proxy:" + originalResult;
    }
    public ISubject getSubject(){
        return subject;
    }
    public void setSubject(ISubject subject){
        this.subject = subject;
    }
}

public class SubjectImpl implements ISubject{
    public String request(){
        // process logic
        return "OK";
    }
}

  在將請求轉發給被代理物件SubjectImpl之前或者之後,都可以根據情況插入其他處理邏輯,比如在轉發之前記錄方法執行開始時間,在轉發之後記錄結束時間,這樣就能夠對SubjectImpl的request()執行的時間進行檢測。或者,可以只在轉發之後對SubjectImpl的request()方法返回結果進行覆蓋,返回不同的值。甚至,可以不做請求轉發,這樣,就不會有SubjectImpl的訪問發生。

  代理物件SubjectProxy就像是SubjectImpl的影子,只不過這個影子通常擁有更多的功能。如果SubjectImpl是系統中Jointpoint所在的物件(即目標物件),那麼就可以為這個目標物件建立一個代理物件,然後將橫切邏輯新增到這個代理物件中。當系統使用這個代理物件的時候,原有邏輯的實現和橫切邏輯就完全融合到一個系統中。

  Spring AOP本質上就是採用這種代理機制實現的,但是,具體實現細節上有所不同。我們來看一下上面的代理實現,我們是將代理類直接寫好,然後在程式碼中手動初始化代理類並通過呼叫代理類來實現代理功能,發現沒有,如果系統裡面有很多類需要代理相同的能,那麼我們就要寫很多的代理類,儘管它們代理的內容是一樣的,這樣是有問題的。上面這種為對應的目標物件建立靜態代理的方法,原理上是可行的,但具體應用上存在問題,所以要尋找其他方法,那有沒有呢,答案是肯定有的,就是接下來我們要講的動態代理。

 

3.2 動態代理

  JDK1.3之後,引入了動態代理(Dynamic Proxy)機制,可以在執行期間,為相應的介面(Interface)動態生成對應的代理物件,從而幫助我們走出最初使用靜態代理實現AOP的窘境。

  動態代理機制的實現主要由一個類和一個介面組成,即java.lang.reflect.Proxy類和java.lang.reflect.InvocationHandler介面。InvacationHandler就是我們實現橫切邏輯的地方,它是橫切邏輯的載體,作用跟Advice是一樣的。所以在使用動態代理機制實現AOP的過程中,我們可以在InvocationHandler的基礎上細化程式結構,根據Advice的型別,分化出對應不同的Advice型別的程式結構。

  所以,我們可以將橫切關注點邏輯封裝到動態代理的InvocationHandler中,然後在系統執行期間,根據橫切關注點需要織入的模組位置,將橫切邏輯織入到相應的代理類中。以動態代理類為載體的橫切邏輯,現在當然就可以與系統其他實現模組一起工作了。

  動態代理雖好,但不能滿足所有的需求,這種方式實現的唯一缺點或者說優點就是,所有需要織入橫切關注點邏輯的模組類都得實現相應的介面,因為動態代理機制只針對介面有效。如果某個類沒有實現任何的介面,就無法使用動態代理機制為其生成相應的動態代理物件。對於沒有實現任何介面的目標物件我們需要尋找其他方式為其動態的生成代理物件。

  預設情況下,Spring AOP發現目標物件實現了相應介面,則採用動態代理機制為其生成代理物件例項。而如果目標物件沒有實現任何介面,Spring AOP則會嘗試使用一個稱為CGLIB(Code Generation Library)的開源的動態位元組碼生成類庫,為目標物件生成動態的代理物件例項。

 

3.3 動態位元組碼增強

  使用動態位元組碼生成技術擴充套件物件行為的原理是,我們可以對目標物件進行繼承擴充套件,為其生成相應的子類,而子類可以通過覆寫來擴充套件父類的行為,只要將橫切邏輯的實現放到子類中,然後讓系統使用擴充套件後的目標物件的子類,就可以達到與代理模式相同的效果了。

  但是使用繼承的方式來擴充套件物件定義,也不能像靜態代理模式那樣,為每個不同型別的目標物件都單獨建立相應的擴充套件子類。所以,我們要藉助於CGLIB這樣的動態位元組碼生成庫,在系統執行期間動態地為目標物件生成相應的擴充套件子類。

  我們知道,Java虛擬機器載入的檔案都是符合一定規範的,所以,只要交給Java虛擬機器執行的檔案符合Java class規範,程式的執行就沒有問題。通常的class檔案都是從Java原始碼檔案使用Javac編譯器編譯而成的,但只要符合Java class規範,我們也可以使用ASM或者CGLiB等Java工具庫,在程式執行期間,動態構建位元組碼的class檔案。

  在這樣的前提下,我們可以為需要織入橫切邏輯的模組類在執行期間,通過動態位元組碼增強技術,為這些系統模組類生成相應的子類,而將橫切邏輯加到這些子類中,讓應用程式在執行期間使用從這些動態生成的子類,從而達到將橫切邏輯織入系統的目的。   使用動態位元組碼增強技術,即使模組類沒有實現相應的介面,我們依然可以對其進行擴充套件,而不用像動態代理那樣受限於介面。不過,這種實現機制依然存在不足,如果需要擴充套件的類以及類中的例項方法等宣告為final的話,則無法對其進行子類化的擴充套件。

 

3.4 一個spring aop示例

  上面說了這麼多,下面就來看一個簡單的例子,體會一下aop的魔法吧。如果只是引用了spring-context,那麼還需要引入spring-aspects:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>3.2.18.RELEASE</version>
</dependency>

  這裡我們採用xml配置的方式來開啟aop功能,在resources目錄下新增一個xml配置檔案,其中<aop:aspectj-autoproxy/>是用來開啟aop的:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop = "http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
     http://www.springframework.org/schema/aop
     http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
     
     <aop:aspectj-autoproxy/>
     
     <bean id = "test" class = "spring.aop.TestAopBean"/>
     <bean class = "spring.aop.AspectJTest"/>
</beans>

  新增Aspect:

@Aspect
public class AspectJTest {

    @Pointcut("execution(* *.test(..))")
    public void test(){
        
    }

    @Before("test()")
    public void beforeTest(){
        System.out.println("beforeTest");
    }
    
    @After("test()")
    public void afterTest(){
        System.out.println("afterTest");
    }

    @Around("test()")
    public Object aroundTest(ProceedingJoinPoint p){
        System.out.println("before1");
        Object o = null;
        try{
            o = p.proceed();
        }catch (Throwable e){
            e.printStackTrace();
        }
        System.out.println("after1");
        return o;
    }
}

  新增測試類:

public class TestAopBean {

    private String testStr = "testStr";

    public String getTestStr(){
        return testStr;
    }

    public void setTestStr(String testStr){
        this.testStr = testStr;
    }

    public void test(){
        System.out.println("hello test");
    }
    
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("aspectJTest.xml");
        TestAopBean test = (TestAopBean)ctx.getBean("test");
        test.test();
    }
}

  可以看到輸出結果:

before1
beforeTest
hello test
after1
afterTest

  這是一個aop簡單示例,我們寫了一個切面(Pointcut),用來指定Joinpoint的位置在執行test()方法時;同時分別定義了三個Advice(Before、After、Around)用來指定要織入的動作,最後將Pointcut和Advice封裝到一個Aspect中,這樣就完成了橫切邏輯的織入。

 

4. 總結

  在深入學習Spring AOP之前,我們先對AOP的概況進行了介紹,接著一起探索了Spring AOP的實現機制,包括最原始的代理模式,直至最終的動態代理與動態位元組碼生成技術。

  • AOP是能夠讓我們在不影響系統原有功能前提下,為軟體系統橫向擴充套件功能;
  • Spring AOP通過兩種方式實現:JDK動態代理、動態位元組碼增強;

  在瞭解了這些內容之後,我們將繼續深入學習Spring AOP。

相關文章