Spring--Spring AOP 實現原理與 CGLIB 應用
AOP(Aspect Orient Programming),也就是面向方面程式設計,作為面向物件程式設計的一種補充,專門用於處理系統中分佈於各個模組(不同方法)中的交叉關注點的問題,在 Java EE 應用中,常常通過 AOP 來處理一些具有橫切性質的系統級服務,如事務管理、安全檢查、快取、物件池管理等。AOP 實現的關鍵就在於 AOP 框架自動建立的 AOP 代理,AOP 代理主要分為靜態代理和動態代理兩大類,靜態代理以 AspectJ 為代表;而動態代理則以 Spring AOP 為代表。本文會從 AspectJ 分析起,逐漸深入,並介紹 CGLIB 來介紹 Spring AOP 框架的實現原理。
AOP(Aspect Orient Programming),作為面向物件程式設計的一種補充,廣泛應用於處理一些具有橫切性質的系統級服務,如事務管理、安全檢查、快取、物件池管理等。AOP 實現的關鍵就在於 AOP 框架自動建立的 AOP 代理,AOP 代理則可分為靜態代理和動態代理兩大類,其中靜態代理是指使用 AOP 框架提供的命令進行編譯,從而在編譯階段就可生成 AOP 代理類,因此也稱為編譯時增強;而動態代理則在執行時藉助於 JDK 動態代理、CGLIB 等在記憶體中“臨時”生成 AOP 動態代理類,因此也被稱為執行時增強。
AOP 的存在價值
在傳統 OOP 程式設計裡以物件為核心,整個軟體系統由系列相互依賴的物件所組成,而這些物件將被抽象成一個一個的類,並允許使用類繼承來管理類與類之間一般到特殊的關係。隨著軟體規模的增大,應用的逐漸升級,慢慢出現了一些 OOP 很難解決的問題。
我們可以通過分析、抽象出一系列具有一定屬性與行為的物件,並通過這些物件之間的協作來形成一個完整的軟體功能。由於物件可以繼承,因此我們可以把具有相同功能或相同特性的屬性抽象到一個層次分明的類結構體系中。隨著軟體規範的不斷擴大,專業化分工越來越系列,以及 OOP 應用實踐的不斷增多,隨之也暴露出了一些 OOP 無法很好解決的問題。
現在假設系統中有 3 段完全相似的程式碼,這些程式碼通常會採用“複製”、“貼上”方式來完成,通過這種“複製”、“貼上”方式開發出來的軟體如圖 1 所示。
圖 1.多個地方包含相同程式碼的軟體
看到如圖 1 所示的示意圖,可能有的讀者已經發現了這種做法的不足之處:如果有一天,圖 1 中的深色程式碼段需要修改,那是不是要開啟 3 個地方的程式碼進行修改?如果不是 3 個地方包含這段程式碼,而是 100 個地方,甚至是 1000 個地方包含這段程式碼段,那會是什麼後果?
為了解決這個問題,我們通常會採用將如圖 1 所示的深色程式碼部分定義成一個方法,然後在 3 個程式碼段中分別呼叫該方法即可。在這種方式下,軟體系統的結構如圖 2 所示。
圖 2 通過方法呼叫實現系統功能
對於如圖 2 所示的軟體系統,如果需要修改深色部分的程式碼,只要修改一個地方即可,不管整個系統中有多少地方呼叫了該方法,程式無須修改這些地方,只需修改被呼叫的方法即可——通過這種方式,大大降低了軟體後期維護的複雜度。
對於如圖 2 所示的方法 1、方法 2、方法 3 依然需要顯式呼叫深色方法,這樣做能夠解決大部分應用場景。但對於一些更特殊的情況:應用需要方法 1、方法 2、方法 3 徹底與深色方法分離——方法 1、方法 2、方法 3 無須直接呼叫深色方法,那如何解決?
因為軟體系統需求變更是很頻繁的事情,系統前期設計方法 1、方法 2、方法 3 時只實現了核心業務功能,過了一段時間,我們需要為方法 1、方法 2、方法 3 都增加事務控制;又過了一段時間,客戶提出方法 1、方法 2、方法 3 需要進行使用者合法性驗證,只有合法的使用者才能執行這些方法;又過了一段時間,客戶又提出方法 1、方法 2、方法 3 應該增加日誌記錄;又過了一段時間,客戶又提出……面對這樣的情況,我們怎麼辦?通常有兩種做法:
- 根據需求說明書,直接拒絕客戶要求。
- 擁抱需求,滿足客戶的需求。
第一種做法顯然不好,客戶是上帝,我們應該儘量滿足客戶的需求。通常會採用第二種做法,那如何解決呢?是不是每次先定義一個新方法,然後修改方法 1、方法 2、方法 3,增加呼叫新方法?這樣做的工作量也不小啊!我們希望有一種特殊的方法:我們只要定義該方法,無須在方法 1、方法 2、方法 3 中顯式呼叫它,系統會“自動”執行該特殊方法。
上面想法聽起來很神奇,甚至有一些不切實際,但其實是完全可以實現的,實現這個需求的技術就是 AOP。AOP 專門用於處理系統中分佈於各個模組(不同方法)中的交叉關注點的問題,在 Java EE 應用中,常常通過 AOP 來處理一些具有橫切性質的系統級服務,如事務管理、安全檢查、快取、物件池管理等,AOP 已經成為一種非常常用的解決方案。
使用 AspectJ 的編譯時增強進行 AOP
AspectJ 是一個基於 Java 語言的 AOP 框架,提供了強大的 AOP 功能,其他很多 AOP 框架都借鑑或採納其中的一些思想。
AspectJ 是 Java 語言的一個 AOP 實現,其主要包括兩個部分:第一個部分定義瞭如何表達、定義 AOP 程式設計中的語法規範,通過這套語言規範,我們可以方便地用 AOP 來解決 Java 語言中存在的交叉關注點問題;另一個部分是工具部分,包括編譯器、除錯工具等。
AspectJ 是最早、功能比較強大的 AOP 實現之一,對整套 AOP 機制都有較好的實現,很多其他語言的 AOP 實現,也借鑑或採納了 AspectJ 中很多設計。在 Java 領域,AspectJ 中的很多語法結構基本上已成為 AOP 領域的標準。
下載、安裝 AspectJ 比較簡單,讀者登入 AspectJ 官網(http://www.eclipse.org/aspectj),即可下載到一個可執行的 JAR 包,使用 java -jar aspectj-1.x.x.jar 命令、多次單擊“Next”按鈕即可成功安裝 AspectJ。
成功安裝了 AspectJ 之後,將會在 E:\Java\AOP\aspectj1.6 路徑下(AspectJ 的安裝路徑)看到如下檔案結構:
- bin:該路徑下存放了 aj、aj5、ajc、ajdoc、ajbrowser 等命令,其中 ajc 命令最常用,它的作用類似於 javac,用於對普通 Java 類進行編譯時增強。
- docs:該路徑下存放了 AspectJ 的使用說明、參考手冊、API 文件等文件。
- lib:該路徑下的 4 個 JAR 檔案是 AspectJ 的核心類庫。
- 相關授權檔案。
一些文件、AspectJ 入門書籍,一談到使用 AspectJ,就認為必須使用 Eclipse 工具,似乎離開了該工具就無法使用 AspectJ 了。
雖然 AspectJ 是 Eclipse 基金組織的開源專案,而且提供了 Eclipse 的 AJDT 外掛(AspectJ Development Tools)來開發 AspectJ 應用,但 AspectJ 絕對無須依賴於 Eclipse 工具。
實際上,AspectJ 的用法非常簡單,就像我們使用 JDK 編譯、執行 Java 程式一樣。下面通過一個簡單的程式來示範 AspectJ 的用法,並分析 AspectJ 如何在編譯時進行增強。
首先編寫一個簡單的 Java 類,這個 Java 類用於模擬一個業務元件。
清單 1.Hello.java
public class Hello
{
// 定義一個簡單方法,模擬應用中的業務邏輯方法
public void sayHello(){System.out.println("Hello AspectJ!");}
// 主方法,程式的入口
public static void main(String[] args)
{
Hello h = new Hello();
h.sayHello();
}
}
上面 Hello 類模擬了一個業務邏輯元件,編譯、執行該 Java 程式,這個結果是沒有任何懸念的,程式將在控制檯列印“Hello AspectJ”字串。
假設現在客戶需要在執行 sayHello() 方法之前啟動事務,當該方法執行結束時關閉事務,在傳統程式設計模式下,我們必須手動修改 sayHello() 方法——如果改為使用 AspectJ,則可以無須修改上面的 sayHello() 方法。
下面我們定義一個特殊的 Java 類。
清單 2.TxAspect.java
public aspect TxAspect
{
// 指定執行 Hello.sayHello() 方法時執行下面程式碼塊
void around():call(void Hello.sayHello()){
System.out.println("開始事務 ...");
proceed();
System.out.println("事務結束 ...");}
}
可能讀者已經發現了,上面類檔案中不是使用 class、interface、enum 在定義 Java 類,而是使用了 aspect ——難道 Java 語言又新增了關鍵字?沒有!上面的 TxAspect 根本不是一個 Java 類,所以 aspect 也不是 Java 支援的關鍵字,它只是 AspectJ 才能識別的關鍵字。
上面粗體字程式碼也不是方法,它只是指定當程式執行 Hello 物件的 sayHello() 方法時,系統將改為執行粗體字程式碼的花括號程式碼塊,其中 proceed() 代表回撥原來的 sayHello() 方法。
正如前面提到的,Java 無法識別 TxAspect.java 檔案的內容,所以我們要使用 ajc.exe 命令來編譯上面的 Java 程式。為了能在命令列使用 ajc.exe 命令,需要把 AspectJ 安裝目錄下的 bin 路徑(比如 E:\Java\AOP\aspectj1.6\bin 目錄)新增到系統的 PATH 環境變數中。接下來執行如下命令進行編譯:
ajc -d . Hello.java TxAspect.java
我們可以把 ajc.exe 理解成 javac.exe 命令,都用於編譯 Java 程式,區別是 ajc.exe 命令可識別 AspectJ 的語法;從這個意義上看,我們可以將 ajc.exe 當成一個增強版的 javac.exe 命令。
執行該 Hello 類依然無須任何改變,因為 Hello 類位於 lee 包下。程式使用如下命令執行 Hello 類:
java lee.Hello
執行該程式,將看到一個令人驚喜的結果:
開始事務 ...
Hello AspectJ!
事務結束 ...
從上面執行結果來看,我們完全可以不對 Hello.java 類進行任何修改,同時又可以滿足客戶的需求:上面程式只是在控制檯列印“開始事務 ...”、“結束事務 ...”來模擬了事務操作,實際上我們可用實際的事務操作程式碼來代替這兩行簡單的語句,這就可以滿足客戶需求了。
如果客戶再次提出新需求,需要在 sayHello() 方法後增加記錄日誌的功能,那也很簡單,我們再定義一個 LogAspect,程式如下:
清單 3.LogAspect.java
public aspect LogAspect
{
// 定義一個 PointCut,其名為 logPointcut
// 該 PointCut 對應於指定 Hello 物件的 sayHello 方法
pointcut logPointcut()
:execution(void Hello.sayHello());
// 在 logPointcut 之後執行下面程式碼塊
after():logPointcut()
{
System.out.println("記錄日誌 ...");
}
}
上面程式的粗體字程式碼定義了一個 Pointcut:logPointcut - 等同於執行 Hello 物件的 sayHello() 方法,並指定在 logPointcut 之後執行簡單的程式碼塊,也就是說,在 sayHello() 方法之後執行指定程式碼塊。使用如下命令來編譯上面的 Java 程式:
ajc -d . *.java
再次執行 Hello 類,將看到如下執行結果:
開始事務 ...
Hello AspectJ!
記錄日誌 ...
事務結束 ...
從上面執行結果來看,通過使用 AspectJ 提供的 AOP 支援,我們可以為 sayHello() 方法不斷增加新功能。
為什麼在對 Hello 類沒有任何修改的前提下,而 Hello 類能不斷地、動態增加新功能呢?這看上去並不符合 Java 基本語法規則啊。實際上我們可以使用 Java 的反編譯工具來反編譯前面程式生成的 Hello.class 檔案,發現 Hello.class 檔案的程式碼如下:
清單 4.Hello.class
package lee; import java.io.PrintStream; import org.aspectj.runtime.internal.AroundClosure; public class Hello { public void sayHello() { try { System.out.println("Hello AspectJ!"); } catch (Throwable localThrowable) { LogAspect.aspectOf().ajc$after$lee_LogAspect$1$9fd5dd97(); throw localThrowable; } LogAspect.aspectOf().ajc$after$lee_LogAspect$1$9fd5dd97(); } ... private static final void sayHello_aroundBody1$advice(Hello target, TxAspect ajc$aspectInstance, AroundClosure ajc$aroundClosure) { System.out.println("開始事務 ..."); AroundClosure localAroundClosure = ajc$aroundClosure; sayHello_aroundBody0(target); System.out.println("事務結束 ..."); } }
不難發現這個 Hello.class 檔案不是由原來的 Hello.java 檔案編譯得到的,該 Hello.class 裡新增了很多內容——這表明 AspectJ 在編譯時“自動”編譯得到了一個新類,這個新類增強了原有的 Hello.java 類的功能,因此 AspectJ 通常被稱為編譯時增強的 AOP 框架。
提示:與 AspectJ 相對的還有另外一種 AOP 框架,它們不需要在編譯時對目標類進行增強,而是執行時生成目標類的代理類,該代理類要麼與目標類實現相同的介面,要麼是目標類的子類——總之,代理類的例項可作為目標類的例項來使用。一般來說,編譯時增強的 AOP 框架在效能上更有優勢——因為執行時動態增強的 AOP 框架需要每次執行時都進行動態增強。
實際上,AspectJ 允許同時為多個方法新增新功能,只要我們定義 Pointcut 時指定匹配更多的方法即可。如下片段:
pointcut xxxPointcut() :execution(void H*.say*());
上面程式中的 xxxPointcut 將可以匹配所有以 H 開頭的類中、所有以 say 開頭的方法,但該方法返回的必須是 void;如果不想匹配任意的返回值型別,則可將程式碼改為如下形式:
pointcut xxxPointcut()
:execution(* H*.say*());
關於如何定義 AspectJ 中的 Aspect、Pointcut 等,讀者可以參考 AspectJ 安裝路徑下的 doc 目錄裡的 quick5.pdf 檔案。
使用 Spring AOP
與 AspectJ 相同的是,Spring AOP 同樣需要對目標類進行增強,也就是生成新的 AOP 代理類;與 AspectJ 不同的是,Spring AOP 無需使用任何特殊命令對 Java 原始碼進行編譯,它採用執行時動態地、在記憶體中臨時生成“代理類”的方式來生成 AOP 代理。
Spring 允許使用 AspectJ Annotation 用於定義方面(Aspect)、切入點(Pointcut)和增強處理(Advice),Spring 框架則可識別並根據這些 Annotation 來生成 AOP 代理。Spring 只是使用了和 AspectJ 5 一樣的註解,但並沒有使用 AspectJ 的編譯器或者織入器(Weaver),底層依然使用的是 Spring AOP,依然是在執行時動態生成 AOP 代理,並不依賴於 AspectJ 的編譯器或者織入器。
簡單地說,Spring 依然採用執行時生成動態代理的方式來增強目標物件,所以它不需要增加額外的編譯,也不需要 AspectJ 的織入器支援;而 AspectJ 在採用編譯時增強,所以 AspectJ 需要使用自己的編譯器來編譯 Java 檔案,還需要織入器。
為了啟用 Spring 對 @AspectJ 方面配置的支援,並保證 Spring 容器中的目標 Bean 被一個或多個方面自動增強,必須在 Spring 配置檔案中配置如下片段:
<?xml version="1.0" encoding="GBK"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- 啟動 @AspectJ 支援 --> <aop:aspectj-autoproxy/> </beans>
當然,如果我們希望完全啟動 Spring 的“零配置”功能,則還需要啟用 Spring 的“零配置”支援,讓 Spring 自動搜尋指定路徑下 Bean 類。
所謂自動增強,指的是 Spring 會判斷一個或多個方面是否需要對指定 Bean 進行增強,並據此自動生成相應的代理,從而使得增強處理在合適的時候被呼叫。
如果不打算使用 Spring 的 XML Schema 配置方式,則應該在 Spring 配置檔案中增加如下片段來啟用 @AspectJ 支援。
<!-- 啟動 @AspectJ 支援 --> <bean class="org.springframework.aop.aspectj.annotation. AnnotationAwareAspectJAutoProxyCreator"/>
上面配置檔案中的 AnnotationAwareAspectJAutoProxyCreator 是一個 Bean 後處理器(BeanPostProcessor),該 Bean 後處理器將會為容器中 Bean 生成 AOP 代理,
當啟動了 @AspectJ 支援後,只要我們在 Spring 容器中配置一個帶 @Aspect 註釋的 Bean,Spring 將會自動識別該 Bean,並將該 Bean 作為方面 Bean 處理。
在 Spring 容器中配置方面 Bean(即帶 @Aspect 註釋的 Bean),與配置普通 Bean 沒有任何區別,一樣使用 <bean.../> 元素進行配置,一樣支援使用依賴注入來配置屬性值;如果我們啟動了 Spring 的“零配置”特性,一樣可以讓 Spring 自動搜尋,並裝載指定路徑下的方面 Bean。
使用 @Aspect 標註一個 Java 類,該 Java 類將會作為方面 Bean,如下面程式碼片段所示:
// 使用 @Aspect 定義一個方面類 @Aspect public class LogAspect { // 定義該類的其他內容 ... }
方面類(用 @Aspect 修飾的類)和其他類一樣可以有方法、屬性定義,還可能包括切入點、增強處理定義。
當我們使用 @Aspect 來修飾一個 Java 類之後,Spring 將不會把該 Bean 當成元件 Bean 處理,因此負責自動增強的後處理 Bean 將會略過該 Bean,不會對該 Bean 進行任何增強處理。
開發時無須擔心使用 @Aspect 定義的方面類被增強處理,當 Spring 容器檢測到某個 Bean 類使用了 @Aspect 標註之後,Spring 容器不會對該 Bean 類進行增強。
下面將會考慮採用 Spring AOP 來改寫前面介紹的例子:
下面例子使用一個簡單的 Chinese 類來模擬業務邏輯元件:
清單 5.Chinese.java
@Component public class Chinese { // 實現 Person 介面的 sayHello() 方法 public String sayHello(String name) { System.out.println("-- 正在執行 sayHello 方法 --"); // 返回簡單的字串 return name + " Hello , Spring AOP"; } // 定義一個 eat() 方法 public void eat(String food) { System.out.println("我正在吃 :"+ food); } }
提供了上面 Chinese 類之後,接下來假設同樣需要為上面 Chinese 類的每個方法增加事務控制、日誌記錄,此時可以考慮使用 Around、AfterReturning 兩種增強處理。
先看 AfterReturning 增強處理程式碼。
清單 6.AfterReturningAdviceTest.java
// 定義一個方面
@Aspect
public class AfterReturningAdviceTest
{
// 匹配 org.crazyit.app.service.impl 包下所有類的、
// 所有方法的執行作為切入點
@AfterReturning(returning="rvt",
pointcut="execution(* org.crazyit.app.service.impl.*.*(..))")
public void log(Object rvt)
{
System.out.println("獲取目標方法返回值 :" + rvt);
System.out.println("模擬記錄日誌功能 ...");
}
}
上面 Aspect 類使用了 @Aspect 修飾,這樣 Spring 會將它當成一個方面 Bean 進行處理。其中程式中粗體字程式碼指定將會在呼叫 org.crazyit.app.service.impl 包下的所有類的所有方法之後織入 log(Object rvt) 方法。
再看 Around 增強處理程式碼:
清單 7.AfterReturningAdviceTest.java
// 定義一個方面
@Aspect
public class AroundAdviceTest
{
// 匹配 org.crazyit.app.service.impl 包下所有類的、
// 所有方法的執行作為切入點
@Around("execution(* org.crazyit.app.service.impl.*.*(..))")
public Object processTx(ProceedingJoinPoint jp)
throws java.lang.Throwable
{
System.out.println("執行目標方法之前,模擬開始事務 ...");
// 執行目標方法,並儲存目標方法執行後的返回值
Object rvt = jp.proceed(new String[]{"被改變的引數"});
System.out.println("執行目標方法之後,模擬結束事務 ...");
return rvt + " 新增的內容";
}
}
與前面的 AfterReturning 增強處理類似的,此處同樣使用了 @Aspect 來修飾前面 Bean,其中粗體字程式碼指定在呼叫 org.crazyit.app.service.impl 包下的所有類的所有方法的“前後(Around)” 織入 processTx(ProceedingJoinPoint jp) 方法
需要指出的是,雖然此處只介紹了 Spring AOP 的 AfterReturning、Around 兩種增強處理,但實際上 Spring 還支援 Before、After、AfterThrowing 等增強處理,關於 Spring AOP 程式設計更多、更細緻的程式設計細節,可以參考《輕量級 Java EE 企業應用實戰》一書。
本示例採用了 Spring 的零配置來開啟 Spring AOP,因此上面 Chinese 類使用了 @Component 修飾,而方面 Bean 則使用了 @Aspect 修飾,方面 Bean 中的 Advice 則分別使用了 @AfterReturning、@Around 修飾。接下來只要為 Spring 提供如下配置檔案即可:
清單 8.bean.xml
<?xml version="1.0" encoding="GBK"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- 指定自動搜尋 Bean 元件、自動搜尋方面類 --> <context:component-scan base-package="org.crazyit.app.service ,org.crazyit.app.advice"> <context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/> </context:component-scan> <!-- 啟動 @AspectJ 支援 --> <aop:aspectj-autoproxy/> </beans>
接下來按傳統方式來獲取 Spring 容器中 chinese Bean、並呼叫該 Bean 的兩個方法,程式程式碼如下:
清單 9.BeanTest.java
public class BeanTest { public static void main(String[] args) { // 建立 Spring 容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml"); Chinese p = ctx.getBean("chinese" ,Chinese.class); System.out.println(p.sayHello("張三")); p.eat("西瓜"); } }
從上面開發過程可以看出,對於 Spring AOP 而言,開發者提供的業務元件、方面 Bean 並沒有任何特別的地方。只是方面 Bean 需要使用 @Aspect 修飾即可。程式不需要使用特別的編譯器、織入器進行處理。
執行上面程式,將可以看到如下執行結果:
執行目標方法之前,模擬開始事務 ...
-- 正在執行 sayHello 方法 --
執行目標方法之後,模擬結束事務 ...
獲取目標方法返回值 : 被改變的引數 Hello , Spring AOP 新增的內容
模擬記錄日誌功能 ...
被改變的引數 Hello , Spring AOP 新增的內容
執行目標方法之前,模擬開始事務 ...
我正在吃 : 被改變的引數
執行目標方法之後,模擬結束事務 ...
獲取目標方法返回值 :null 新增的內容
模擬記錄日誌功能 ...
雖然程式是在呼叫 Chinese 物件的 sayHello、eat 兩個方法,但從上面執行結果不難看出:實際執行的絕對不是 Chinese 物件的方法,而是 AOP 代理的方法。也就是說,Spring AOP 同樣為 Chinese 類生成了 AOP 代理類。這一點可通過在程式中增加如下程式碼看出:
System.out.println(p.getClass());
上面程式碼可以輸出 p 變數所引用物件的實現類,再次執行程式將可以看到上面程式碼產生 class org.crazyit.app.service.impl.Chinese$$EnhancerByCGLIB$$290441d2 的輸出,這才是 p 變數所引用的物件的實現類,這個類也就是 Spring AOP 動態生成的 AOP 代理類。從 AOP 代理類的類名可以看出,AOP 代理類是由 CGLIB 來生成的。
如果將上面程式程式稍作修改:只要讓上面業務邏輯類 Chinese 類實現一個任意介面——這種做法更符合 Spring 所倡導的“面向介面程式設計”的原則。假設程式為 Chinese 類提供如下 Person 介面,並讓 Chinese 類實現該介面:
清單 10.Person.java
public interface Person { String sayHello(String name); void eat(String food); }
接下來讓 BeanTest 類面向 Person 介面、而不是 Chinese 類程式設計。即將 BeanTest 類改為如下形式:
清單 11.BeanTest.java
public class BeanTest
{
public static void main(String[] args)
{
// 建立 Spring 容器
ApplicationContext ctx = new
ClassPathXmlApplicationContext("bean.xml");
Person p = ctx.getBean("chinese" ,Person.class);
System.out.println(p.sayHello("張三"));
p.eat("西瓜");
System.out.println(p.getClass());
}
}
原來的程式是將面向 Chinese 類程式設計,現在將該程式改為面向 Person 介面程式設計,再次執行該程式,程式執行結果沒有發生改變。只是 System.out.println(p.getClass()); 將會輸出 class $Proxy7,這說明此時的 AOP 代理並不是由 CGLIB 生成的,而是由 JDK 動態代理生成的。
Spring AOP 框架對 AOP 代理類的處理原則是:如果目標物件的實現類實現了介面,Spring AOP 將會採用 JDK 動態代理來生成 AOP 代理類;如果目標物件的實現類沒有實現介面,Spring AOP 將會採用 CGLIB 來生成 AOP 代理類——不過這個選擇過程對開發者完全透明、開發者也無需關心。
Spring AOP 會動態選擇使用 JDK 動態代理、CGLIB 來生成 AOP 代理,如果目標類實現了介面,Spring AOP 則無需 CGLIB 的支援,直接使用 JDK 提供的 Proxy 和 InvocationHandler 來生成 AOP 代理即可。關於如何 Proxy 和 InvocationHandler 來生成動態代理不在本文介紹範圍之內,如果讀者對 Proxy 和 InvocationHandler 的用法感興趣則可自行參考 Java API 文件或《瘋狂 Java 講義》。
Spring AOP 原理剖析
通過前面介紹可以知道:AOP 代理其實是由 AOP 框架動態生成的一個物件,該物件可作為目標物件使用。AOP 代理包含了目標物件的全部方法,但 AOP 代理中的方法與目標物件的方法存在差異:AOP 方法在特定切入點添加了增強處理,並回調了目標物件的方法。
AOP 代理所包含的方法與目標物件的方法示意圖如圖 3 所示。
圖 3.AOP 代理的方法與目標物件的方法
Spring 的 AOP 代理由 Spring 的 IoC 容器負責生成、管理,其依賴關係也由 IoC 容器負責管理。因此,AOP 代理可以直接使用容器中的其他 Bean 例項作為目標,這種關係可由 IoC 容器的依賴注入提供。
縱觀 AOP 程式設計,其中需要程式設計師參與的只有 3 個部分:
- 定義普通業務元件。
- 定義切入點,一個切入點可能橫切多個業務元件。
- 定義增強處理,增強處理就是在 AOP 框架為普通業務元件織入的處理動作。
上面 3 個部分的第一個部分是最平常不過的事情,無須額外說明。那麼進行 AOP 程式設計的關鍵就是定義切入點和定義增強處理。一旦定義了合適的切入點和增強處理,AOP 框架將會自動生成 AOP 代理,而 AOP 代理的方法大致有如下公式:
代理物件的方法 = 增強處理 + 被代理物件的方法
在上面這個業務定義中,不難發現 Spring AOP 的實現原理其實很簡單:AOP 框架負責動態地生成 AOP 代理類,這個代理類的方法則由 Advice 和回撥目標物件的方法所組成。
對於前面提到的圖 2 所示的軟體呼叫結構:當方法 1、方法 2、方法 3 ……都需要去呼叫某個具有“橫切”性質的方法時,傳統的做法是程式設計師去手動修改方法 1、方法 2、方法 3 ……、通過程式碼來呼叫這個具有“橫切”性質的方法,但這種做法的可擴充套件性不好,因為每次都要改程式碼。
於是 AOP 框架出現了,AOP 框架則可以“動態的”生成一個新的代理類,而這個代理類所包含的方法 1、方法 2、方法 3 ……也增加了呼叫這個具有“橫切”性質的方法——但這種呼叫由 AOP 框架自動生成的代理類來負責,因此具有了極好的擴充套件性。程式設計師無需手動修改方法 1、方法 2、方法 3 的程式碼,程式設計師只要定義切入點即可—— AOP 框架所生成的 AOP 代理類中包含了新的方法 1、訪法 2、方法 3,而 AOP 框架會根據切入點來決定是否要在方法 1、方法 2、方法 3 中回撥具有“橫切”性質的方法。
簡而言之:AOP 原理的奧妙就在於動態地生成了代理類,這個代理類實現了圖 2 的呼叫——這種呼叫無需程式設計師修改程式碼。接下來介紹的 CGLIB 就是一個代理生成庫,下面介紹如何使用 CGLIB 來生成代理類。
使用 CGLIB 生成代理類
CGLIB(Code Generation Library),簡單來說,就是一個程式碼生成類庫。它可以在執行時候動態是生成某個類的子類。
此處使用前面定義的 Chinese 類,現在改為直接使用 CGLIB 來生成代理,這個代理類同樣可以實現 Spring AOP 代理所達到的效果。
下面先為 CGLIB 提供一個攔截器實現類:
清單 12.AroundAdvice.java
public class AroundAdvice implements MethodInterceptor { public Object intercept(Object target, Method method , Object[] args, MethodProxy proxy) throws java.lang.Throwable { System.out.println("執行目標方法之前,模擬開始事務 ..."); // 執行目標方法,並儲存目標方法執行後的返回值 Object rvt = proxy.invokeSuper(target, new String[]{"被改變的引數"}); System.out.println("執行目標方法之後,模擬結束事務 ..."); return rvt + " 新增的內容"; } }
上面這個 AroundAdvice.java 的作用就像前面介紹的 Around Advice,它可以在呼叫目標方法之前、呼叫目標方法之後織入增強處理。
接下來程式提供一個 ChineseProxyFactory 類,這個 ChineseProxyFactory 類會通過 CGLIB 來為 Chinese 生成代理類:
清單 13.ChineseProxyFactory.java
public class ChineseProxyFactory { public static Chinese getAuthInstance() { Enhancer en = new Enhancer(); // 設定要代理的目標類 en.setSuperclass(Chinese.class); // 設定要代理的攔截器 en.setCallback(new AroundAdvice()); // 生成代理類的例項 return (Chinese)en.create(); } }
上面粗體字程式碼就是使用 CGLIB 的 Enhancer 生成代理物件的關鍵程式碼,此時的 Enhancer 將以 Chinese 類作為目標類,以 AroundAdvice 物件作為“Advice”,程式將會生成一個 Chinese 的子類,這個子類就是 CGLIB 生成代理類,它可作為 Chinese 物件使用,但它增強了 Chinese 類的方法。
測試 Chinese 代理類的主程式如下:
清單 14.Main.java
public class Main { public static void main(String[] args) { Chinese chin = ChineseProxyFactory.getAuthInstance(); System.out.println(chin.sayHello("孫悟空")); chin.eat("西瓜"); System.out.println(chin.getClass()); } }
執行上面主程式,看到如下輸出結果:
執行目標方法之前,模擬開始事務 ...
-- 正在執行 sayHello 方法 --
執行目標方法之後,模擬結束事務 ...
被改變的引數 Hello , CGLIB 新增的內容
執行目標方法之前,模擬開始事務 ...
我正在吃 : 被改變的引數
執行目標方法之後,模擬結束事務 ...
class lee.Chinese $ $ EnhancerByCGLIB $ $ 4bd097d9
從上面輸出結果來看,CGLIB 生成的代理完全可以作為 Chinese 物件來使用,而且 CGLIB 代理物件的 sayHello()、eat() 兩個方法已經增加了事務控制(只是模擬),這個 CGLIB 代理其實就是 Spring AOP 所生成的 AOP 代理。
通過程式最後的輸出,不難發現這個代理物件的實現類是 lee.Chinese $ $ EnhancerByCGLIB $ $ 4bd097d9,這就是 CGLIB 所生成的代理類,這個代理類的格式與前面 Spring AOP 所生成的代理類的格式完全相同。
這就是 Spring AOP 的根本所在:Spring AOP 就是通過 CGLIB 來動態地生成代理物件,這個代理物件就是所謂的 AOP 代理,而 AOP 代理的方法則通過在目標物件的切入點動態地織入增強處理,從而完成了對目標方法的增強。
小結
AOP 廣泛應用於處理一些具有橫切性質的系統級服務,AOP 的出現是對 OOP 的良好補充,它使得開發者能用更優雅的方式處理具有橫切性質的服務。不管是那種 AOP 實現,不論是 AspectJ、還是 Spring AOP,它們都需要動態地生成一個 AOP 代理類,區別只是生成 AOP 代理類的時機不同:AspectJ 採用編譯時生成 AOP 代理類,因此具有更好的效能,但需要使用特定的編譯器進行處理;而 Spring AOP 則採用執行時生成 AOP 代理類,因此無需使用特定編譯器進行處理。由於 Spring AOP 需要在每次執行時生成 AOP 代理,因此效能略差一些。