Spring AOP 實現原理與 CGLIB 應用

AOP(Aspect Orient Programming),做爲面向對象編程的一種補充,普遍應用於處理一些具備橫切性質的系統級服務,如事務管理、安全檢查、緩存、對象池管理等。AOP 實現的關鍵就在於 AOP 框架自動建立的 AOP 代理,AOP 代理則可分爲靜態代理和動態代理兩大類,其中靜態代理是指使用 AOP 框架提供的命令進行編譯,從而在編譯階段就可生成 AOP 代理類,所以也稱爲編譯時加強;而動態代理則在運行時藉助於 JDK 動態代理、CGLIB 等在內存中「臨時」生成 AOP 動態代理類,所以也被稱爲運行時加強。 java

AOP 的存在價值

在傳統 OOP 編程裏以對象爲核心,整個軟件系統由系列相互依賴的對象所組成,而這些對象將被抽象成一個一個的類,並容許使用類繼承來管理類與類之間通常到特殊的關係。隨着軟件規模的增大,應用的逐漸升級,慢慢出現了一些 OOP 很難解決的問題。 程序員

咱們能夠經過分析、抽象出一系列具備必定屬性與行爲的對象,並經過這些對象之間的協做來造成一個完整的軟件功能。因爲對象能夠繼承,所以咱們能夠把具備相同功能或相同特性的屬性抽象到一個井井有條的類結構體系中。隨着軟件規範的不斷擴大,專業化分工愈來愈系列,以及 OOP 應用實踐的不斷增多,隨之也暴露出了一些 OOP 沒法很好解決的問題。 spring

如今假設系統中有 3 段徹底類似的代碼,這些代碼一般會採用「複製」、「粘貼」方式來完成,經過這種「複製」、「粘貼」方式開發出來的軟件如圖 1 所示。 express

圖 1.多個地方包含相同代碼的軟件
圖 1 多個地方包含相同代碼的軟件

看到如圖 1 所示的示意圖,可能有的讀者已經發現了這種作法的不足之處:若是有一天,圖 1 中的深色代碼段須要修改,那是否是要打開 3 個地方的代碼進行修改?若是不是 3 個地方包含這段代碼,而是 100 個地方,甚至是 1000 個地方包含這段代碼段,那會是什麼後果? 編程

爲了解決這個問題,咱們一般會採用將如圖 1 所示的深色代碼部分定義成一個方法,而後在 3 個代碼段中分別調用該方法便可。在這種方式下,軟件系統的結構如圖 2 所示。 緩存

圖 2 經過方法調用實現系統功能
圖 2 經過方法調用實現系統功能

對於如圖 2 所示的軟件系統,若是須要修改深色部分的代碼,只要修改一個地方便可,無論整個系統中有多少地方調用了該方法,程序無須修改這些地方,只需修改被調用的方法便可——經過這種方式,大大下降了軟件後期維護的複雜度。 安全

對於如圖 2 所示的方法 一、方法 二、方法 3 依然須要顯式調用深色方法,這樣作可以解決大部分應用場景。但對於一些更特殊的狀況:應用須要方法 一、方法 二、方法 3 完全與深色方法分離——方法 一、方法 二、方法 3 無須直接調用深色方法,那如何解決? app

由於軟件系統需求變動是很頻繁的事情,系統前期設計方法 一、方法 二、方法 3 時只實現了核心業務功能,過了一段時間,咱們須要爲方法 一、方法 二、方法 3 都增長事務控制;又過了一段時間,客戶提出方法 一、方法 二、方法 3 須要進行用戶合法性驗證,只有合法的用戶才能執行這些方法;又過了一段時間,客戶又提出方法 一、方法 二、方法 3 應該增長日誌記錄;又過了一段時間,客戶又提出……面對這樣的狀況,咱們怎麼辦?一般有兩種作法: 框架

  • 根據需求說明書,直接拒絕客戶要求。
  • 擁抱需求,知足客戶的需求。

第一種作法顯然很差,客戶是上帝,咱們應該儘可能知足客戶的需求。一般會採用第二種作法,那如何解決呢?是否是每次先定義一個新方法,而後修改方法 一、方法 二、方法 3,增長調用新方法?這樣作的工做量也不小啊!咱們但願有一種特殊的方法:咱們只要定義該方法,無須在方法 一、方法 二、方法 3 中顯式調用它,系統會「自動」執行該特殊方法。 eclipse

上面想法聽起來很神奇,甚至有一些不切實際,但實際上是徹底能夠實現的,實現這個需求的技術就是 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、aj五、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

點擊查看代碼清單

可能讀者已經發現了,上面類文件中不是使用 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 代理的方法與目標對象的方法
圖 3.AOP 代理的方法與目標對象的方法

Spring 的 AOP 代理由 Spring 的 IoC 容器負責生成、管理,其依賴關係也由 IoC 容器負責管理。所以,AOP 代理能夠直接使用容器中的其餘 Bean 實例做爲目標,這種關係可由 IoC 容器的依賴注入提供。

縱觀 AOP 編程,其中須要程序員參與的只有 3 個部分:

  • 定義普通業務組件。
  • 定義切入點,一個切入點可能橫切多個業務組件。
  • 定義加強處理,加強處理就是在 AOP 框架爲普通業務組件織入的處理動做。

上面 3 個部分的第一個部分是最日常不過的事情,無須額外說明。那麼進行 AOP 編程的關鍵就是定義切入點和定義加強處理。一旦定義了合適的切入點和加強處理,AOP 框架將會自動生成 AOP 代理,而 AOP 代理的方法大體有以下公式:

代理對象的方法 = 加強處理 + 被代理對象的方法

在上面這個業務定義中,不難發現 Spring AOP 的實現原理其實很簡單:AOP 框架負責動態地生成 AOP 代理類,這個代理類的方法則由 Advice 和回調目標對象的方法所組成。

對於前面提到的圖 2 所示的軟件調用結構:當方法 一、方法 二、方法 3 ……都須要去調用某個具備「橫切」性質的方法時,傳統的作法是程序員去手動修改方法 一、方法 二、方法 3 ……、經過代碼來調用這個具備「橫切」性質的方法,但這種作法的可擴展性很差,由於每次都要改代碼。

因而 AOP 框架出現了,AOP 框架則能夠「動態的」生成一個新的代理類,而這個代理類所包含的方法 一、方法 二、方法 3 ……也增長了調用這個具備「橫切」性質的方法——但這種調用由 AOP 框架自動生成的代理類來負責,所以具備了極好的擴展性。程序員無需手動修改方法 一、方法 二、方法 3 的代碼,程序員只要定義切入點便可—— AOP 框架所生成的 AOP 代理類中包含了新的方法 一、訪法 二、方法 3,而 AOP 框架會根據切入點來決定是否要在方法 一、方法 二、方法 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 代理,所以性能略差一些。

相關文章
相關標籤/搜索