86
Spring AOP Logging Security Cross-cutting concerns Crossing-cutting concerns Aspect Aspect-oriented programming Aspect Weave AOP AOP OOP Object-oriented programming Spring AOP AOP Spring AOP Spring Spring AOP 4

Spring 2.0 技術手冊第四章 - Spring AOP

Embed Size (px)

Citation preview

Page 1: Spring 2.0 技術手冊第四章 - Spring AOP

Spring AOP

在一個服務的流程中插入與商務邏輯無關的系統服務邏輯(例如Logging、Security),這樣的邏輯稱為 Cross-cutting concerns,將Crossing-cutting concerns 獨立出來設計為一個物件,這樣的特殊物件稱之為 Aspect,Aspect-oriented programming 著重在 Aspect 的設計及與應用程式的縫合(Weave),以上各式各樣與 AOP 相關的名詞,在這個章節中都將獲得解釋。

AOP 跟 OOP(Object-oriented programming)並不相互抵觸,它們是可以相輔相成的兩個設計模型,Spring AOP 是實現 AOP 的一種技術,而 Spring AOP 也是 Spring 中一些子框架或子功能所依賴的核心,讓您了解如何使用 Spring AOP 也是這個章節的重點之一。

4

Page 2: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�2

4.1 AOP 入門

AOP全名 Aspect-oriented programming,對於尚未接觸過 AOP的人來說,AOP 中許多抽象的術語名詞令人難以理解,像是 Cross-cutting

concerns、Aspect、Weave 等,在這個小節中,將先從代理機制(Spring實現 AOP 的一種方式)來看一個實際且簡單的示範程式,從中介紹 AOP的觀念與各種術語。

4.1.1 從代理機制初探 AOP 暫且先將 AOP這個英文縮寫名詞放到一邊,先從一個簡單常見的例子來看一個議題,這個例子當中含有日誌(Logging)動作,程式中很常需要為某些動作或事件作下記錄,以便在事後檢視程式運作過程,或是作為除錯時的資訊。 來看一個最簡單的例子,當您需要在執行某些方法時留下日誌訊息,直覺的,您可能會如下撰寫:

package onlyfun.caterpillar;

import java.util.logging.*;

public class HelloSpeaker {

private Logger logger =

Logger.getLogger(this.getClass().getName());

public void hello(String name) {

// 方法執行開始時留下記錄方法執行開始時留下記錄方法執行開始時留下記錄方法執行開始時留下記錄

logger.log(Level.INFO, "hello method starts....");

// 程式主要功能

System.out.println("Hello, " + name);

// 方法執行完畢前留下記錄方法執行完畢前留下記錄方法執行完畢前留下記錄方法執行完畢前留下記錄

logger.log(Level.INFO, "hello method ends....");

}

Page 3: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�3

} 在 HelloSpeaker類別中,當執行 hello() 方法時,您希望方法執行開始與執行完畢時都能留下記錄,最簡單的作法就是如以上的程式設計,在方法執行的前後加上日誌動作,然而日誌的這幾行程式碼橫切入(Cross-

cutting)HelloSpeaker類別中,對於 HelloSpeaker來說,日誌的這幾個動作並不屬於 HelloSpeaker商務邏輯(顯示 "Hello" 等文字),這使得 Hello-

Speaker增加了額外的職責。 想想如果程式中這種日誌的動作到處都有需求,以上的寫法勢必造成您到處撰寫這些日誌動作的程式碼,這使得維護日誌程式碼的困難度加大。如果需要的服務(Service)不只有日誌動作,有一些非物件本身職責的相關動作也混入了物件之中(例如權限檢查、交易管理等等),會使得物件的負擔更形加重,甚至混淆了物件本身該負有的職責,物件本身的職責所佔的程式碼,或許還少於這些與物件職責不相關的動作或服務的程式碼。 另一方面,使用以上的寫法,若不再需要日誌(或權限檢查、交易管理等)的服務,那麼將需要修改所有留下日誌動作的程式碼,您無法簡單的將這些相關服務從既有的程式中移去。 可以使用代理(Proxy)機制來解決這個問題,在這邊討論兩種代理方式:靜態代理(Static proxy)與動態代理(Dynamic proxy)。

� 靜態代理 在靜態代理的實現中,代理物件與被代理物件必須實現同一個介面,在代理物件中可以實現日誌等相關服務,並在需要的時候再呼叫被代理的物件,如此被代理物件當中就可以僅保留商務相關職責。 舉個實際的例子來說,首先定義一個 IHello 介面:

Page 4: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�4

StaticProxyDemo IHello.java

package onlyfun.caterpillar;

public interface IHello {

public void hello(String name);

} 然後讓實現商務邏輯的 HelloSpeaker 類別實現 IHello 介面,例如:

StaticProxyDemo HelloSpeaker.java

package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {

public void hello(String name) {

System.out.println("Hello, " + name);

}

} 可以看到,在 HelloSpeaker 類別中現在沒有任何日誌的程式碼插入其中,日誌服務的實現將被放至代理物件之中,代理物件同樣也要實現 IHello 介面,例如:

StaticProxyDemo HelloProxy.java

package onlyfun.caterpillar;

import java.util.logging.*;

public class HelloProxy implements IHello {

private Logger logger =

Logger.getLogger(this.getClass().getName());

private IHello helloObject;

public HelloProxy(IHello helloObject) {

this.helloObject = helloObject;

}

Page 5: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4��

public void hello(String name) {

// 日誌服務

log("hello method starts....");

// 執行商務邏輯

helloObject.hello(name);

// 日誌服務

log("hello method ends....");

}

private void log(String msg) {

logger.log(Level.INFO, msg);

}

} 在 HelloProxy 類別的 hello() 方法中,真正實現商務邏輯前後可以安排日誌服務,實際撰寫一個測試程式來看看如何使用代理物件。

StaticProxyDemo ProxyDemo.java

package onlyfun.caterpillar;

public class ProxyDemo {

public static void main(String[] args) {

IHello proxy =

new HelloProxy(new HelloSpeaker());

proxy.hello("Justin");

}

} 程式中呼叫執行的是代理物件,建構代理物件時必須給它一個被代理物件,記得在操作取回的代理物件時,必須轉換操作介面為 IHello 介面,來看一下實際執行的結果:

Page 6: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�6

圖 4.1 StaticProxyDemo 專案執行結果 代理物件 HelloProxy 將代理真正的 HelloSpeaker 來執行 hello(),並在其前後加上日誌的動作,這使得 HelloSpeaker 在撰寫時不必介入日誌動作,HelloSpeaker 可以專心於它的職責,可以從圖解的方式來更進一步看出代理機制的運作流程:

圖 4.2 代理機制的運作流程 這是靜態代理的基本範例,然而如您所看到的,代理物件的一個介面只服務於一種類型的物件,而且如果要代理的方法很多,勢必要為每個方法進行代理,靜態代理在程式規模稍大時就無法勝任,在這邊介紹靜態代理的目的,是在讓您了解代理的基本原理。

Page 7: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�7

很多人會問:圖 4.1 的日誌訊息為何都出現在"Hello, Justin"文字之前(您執行的結果也許是在之後)?範例執行結果有錯嗎?要知道,日誌的動作是由另一個執行緒來進行,日誌動作並不影響(或介入)程式流程,範例的重點在於日誌動作確實有執行,而不是在訊息於主控台出現的順序,日誌動作也不是用來預測程式執行順序之用。如果真的想看到主控台出現訊息的順序,符合程式中物件與方法的呼叫順序,那麼將日誌的部份全部改成標準輸出即可。

� 動態代理 在 JDK 1.3 之後加入了可協助開發動態代理功能的 API,您不必為特定物件與方法撰寫特定的代理物件,使用動態代理,可以使得一個處理者(Handler)服務於各個物件。首先,一個處理者的類別設計必須實作java.lang.reflect.InvocationHandler介面,以實例來進行說明,例如設計一個 LogHandler 類別:

DynamicProxyDemo LogHandler.java

package onlyfun.caterpillar;

import java.util.logging.*;

import java.lang.reflect.*;

public class LogHandler implements InvocationHandler {

private Logger logger =

Logger.getLogger(this.getClass().getName());

private Object delegate;

public Object bind(Object delegate) {

this.delegate = delegate;

return Proxy.newProxyInstance(

delegate.getClass().getClassLoader(),

Page 8: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�8

delegate.getClass().getInterfaces(),

this);

}

public Object invoke(Object proxy, Method method,

Object[] args) throws Throwable {

Object result = null;

try {

log("method starts..." + method);

result = method.invoke(delegate, args);

logger.log(Level.INFO, "method ends..." + method);

} catch (Exception e){

log(e.toString());

}

return result;

}

private void log(String message) {

logger.log(Level.INFO, message);

}

} 主要的概念是使用 Proxy.newProxyInstance() 靜態方法建立一個代理物件,建立代理物件時必須告知所要代理的介面,之後您可以操作所建立的代理物件,在每次操作時會執行 InvocationHandler 的 invoke() 方法,invoke() 方法會傳入被代理物件的方法名稱與執行參數,實際上要執行的方法交由 method.invoke(),您在 method.invoke() 前後加上日誌動作,method.invoke() 傳回的物件是實際方法執行過後的回傳結果。 要實現動態代理,同樣必須定義所要代理的介面,例如:

Page 9: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4��

DynamicProxyDemo IHello.java

package onlyfun.caterpillar;

public interface IHello {

public void hello(String name);

} 然後讓實現商務邏輯的 HelloSpeaker 類別實現 IHello 介面,例如:

DynamicProxyDemo HelloSpeaker.java

package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {

public void hello(String name) {

System.out.println("Hello, " + name);

}

} 眼尖的您或許發現到了,這跟之前 StaticProxyDemo 專案中的 IHello 介面、HelloSpeaker 是相同的內容,在這邊撰寫出來是為了範例的完整呈現。接下來撰寫一個測試的程式,您要使用 LogHandler 的 bind() 方法來綁定被代理物件,如下所示:

DynamicProxyDemo ProxyDemo.java

package onlyfun.caterpillar;

public class ProxyDemo {

public static void main(String[] args) {

LogHandler logHandler = new LogHandler();

IHello helloProxy =

(IHello) logHandler.bind(new HelloSpeaker());

helloProxy.hello("Justin");

}

}

Page 10: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�1�

來看一下執行的結果:

圖 4.3 DynamicProxyDemo 專案的執行結果

LogHandler 不再服務於特定物件與介面,而 HelloSpeaker 也不用插入任何有關於日誌的動作,它不用意識到日誌動作的存在。 回到 AOP 的議題上,這個例子與 AOP 有何關係? 如 4.2 頁的例子中,HelloSpeaker 本身的職責是顯示招呼文字,卻必須插入日誌動作,這使得 HelloSpeaker的職責加重,在 AOP的術語來說,日誌的程式碼橫切(Cross-cutting)入 HelloSpeaker的程式執行流程中,日誌這樣的動作在 AOP中稱之為橫切關切點(Cross-cutting concern)。 使用代理物件將日誌等與商務邏輯無關的動作或任務提取出來,設計為一個服務物件,像是之前範例中示範的 HelloProxy 或是 LogHandler,這樣的物件稱之為切面(Aspect)。

AOP 中的 Aspect 所指的可以像是日誌等這類的動作或服務,您將這些動作(Cross-cutting concerns)設計為通用、不介入特定商務物件的一個職責清楚的 Aspect物件,這就是所謂的 Aspect-oriented programming,縮寫名詞即為 AOP。 在好的設計之下,Aspect可以獨立於應用程式之外,在必要的時候,可以介入應用程式之中提供服務,而不需要相關服務的時候,又可以將這些Aspect直接從應用程式中脫離,而應用程式本身不需修改任何一行程式碼。

Page 11: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�11

AOP 的相關英文名詞術語要翻成中文實在很拗口,而且這些英文名詞術語現在也沒有個統一的中文翻譯名詞,因此接下來的章節中,將以 AOP 原本的英文名詞術語來呈現,將中文翻譯的名詞問題,留待日後看有無更令人信服的中文名詞。

4.1.2 AOP 觀念與術語

AOP全名為 Aspect-Oriented Programming,有關於 AOP的許多名詞術語都過於抽象,單從字面上並不容易理解其名詞意義,這邊將以之前介紹代理機制的範例,來逐一對照以介紹 AOP的術語與觀念:

� Cross-cutting concern 在 DynamicProxyDemo 專案的例子中,日誌的動作原先被橫切(Cross-

cutting)入至 HelloSpeaker 本身所負責的商務流程之中,另外類似於日誌這類的動作,如安全(Security)檢查、交易(Transaction)等系統層面的服務(Service),在一些應用程式之中常被見到安插至各個物件的處理流程之中,這些動作在 AOP 的術語中被稱之為 Cross-cutting concerns。 以圖片說明可強調出 Cross-cutting concerns 的意涵,例如原來的商務流程是很單純的:

Page 12: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�12

圖 4.4 AOP 圖解之一 現在為了要加入日誌與安全檢查等服務,物件的程式碼中像被硬生生的寫入相關的日誌、安全檢查程式片段,則可使用以下圖解表示出 Cross-

cutting 與 Cross-cutting concerns 的概念:

圖 4.5 AOP 圖解之二

Cross-cutting concerns 若直接撰寫在負責某商務的物件之流程中,會使得維護程式的成本增高,若您今天要將物件中的日誌功能修改或是移除該服務,則必須修改所有撰寫日誌服務的程式碼,然後重新編譯,另一方面,Cross-cutting concerns 混雜於商務邏輯之中,使得商務物件本身的邏輯或程式的撰寫更為複雜。

� Aspect 將散落於各個商務邏輯之中的 Cross-cutting concerns 收集起來,設計各個獨立可重用的物件,這些物件稱之為 Aspect。例如在 DynamicProxyDemo專案中,將日誌的動作設計為一個 LogHandler 類別,LogHandler 類別在AOP 的術語就是 Aspect 的一個具體實例。在 AOP 中著重於 Aspect 的辨認,將之從商務流程中獨立出來,在需要該服務的時候,縫合(Weave)至應用程式之上,不需要服務的時候,也可以馬上從應用程式中脫離,應用程式中的可重用組件不用作任何的修改,例如在 DynamicProxyDemo 專

Page 13: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�13

案中的 HelloSpeaker 所代表的角色就是應用程式中可重用的組件,在它需要日誌服務時並不用修改本身的程式碼。 另一方面,對於應用程式中可重用的組件來說,以 AOP 的設計方式,它不用知道提供服務的物件是否存在,具體的說,與服務相關的 API 不會出現在可重用的應用程式組件之中,因而可提高這些組件的重用性,您可以將這些組件應用至其它的應用程式之中,而不會因為目前加入了某些服務而與目前的應用程式框架發生耦合。

� Advice

Aspect 當中對 Cross-cutting concerns 的具體實作稱之為 Advice。以日誌的動作而言, Advice 中會包括日誌程式碼是如何實作的,像是DynamicProxyDemo 專案中 LogHandler 的 invoke()方法,就是 Advice 的一個具體實例。Advice 中包括了 Cross-cutting concerns 的行為或所要提供的服務。

� Joinpoint

Advice 在應用程式執行時加入商務流程的點或時機稱之為 Joinpoint,具體來說,就是 Advice 在應用程式中被執行的時機。Spring 只支援方法的Jointpoint,執行時機可能是某個方法被執行之前或之後(或兩者都有),或是方法中某個例外發生的時候。

� Pointcut

Pointcut 定義了感興趣的 Jointpoint,當呼叫的方法符合 Pointcut 表示式時,將 Advice 縫合至應用程式上提供服務。在 Spring 2.0 中具體的說,您可以在定義檔或 Annotation 中撰寫 Pointcut,說明哪些 Advice 要應用至方法的前後。

Page 14: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�14

� Target 一個 Advice 被應用的對象或目標物件,例如 DynamicProxyDemo 專案中的 HelloSpeaker 就是 LogHandler 中 Advice 的 Target。

� Introduction 對於一個現存的類別,Introduction 可以為其增加行為,而不用修改該類別的程式,具體的說,可以為某個已撰寫、編譯完成的類別,在執行時期動態加入一些方法或行為,而不用修改或新增任何一行程式碼。

� Proxy 在《Expert One-on-One J2EE Development WIthout EJB》一書中,Rod

Johnson、Juergen Hoeller 在第八章中有提到,AOP 的實作有五個主要的策略:Dynamic Proxies、Dynamic Byte Code Generation、Java Code

Generation、Use of a Custon Class Loader、Language Extensions。 在之前靜態代理與動態代理中,已經使用實際的程式範例介紹過代理機制,Spring 的 AOP 主要是透過動態代理來完成,可用於代理任何的介面。另一方面,Spring 也可以使用 CGLIB 代理,用以代理類別,像是一些遺留類別(Legacy classes)。

� Weave

Advice 被應用至物件之上的過程稱之為縫合(Weave),在 AOP 中縫合的方式有幾個時間點:編譯時期(Compile time)、類別載入時期(Classload

time)、執行時期(Runtime)。 結合 DynamicProxyDemo 的實例,將以上介紹過的 AOP 相關名詞具體的使用圖片來表示,有助於對每一個名詞的理解與認識:

Page 15: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�1�

圖 4.6 AOP 的各個名詞示意

4.1.3 Spring AOP 不同的 AOP框架會有其對 AOP概念的不同實作方式,主要的差別在於所提供的 Pointcuts、Aspects的豐富程度,以及它們如何被縫合(Weave)至應用程式(像是 Pointcuts的定義方式)、代理的方式等。

Spring 的 Advices 是用 Java 程式語言來撰寫,而不使用特定的 AOP語言,在定義 Pointcuts時可以使用 XML組態檔案或 Annotation,撰寫的方式對於 Java開發人員來說都很熟悉,您不必學習特定的語法,就可以用熟悉的 Java程式語言與 XML格式來運用 Spring AOP。 若選擇以實作 API的方式來實現 Advices,Spring的 AOP會實作 AOP

Alliance(http://www.sourceforge.net/ projects/aopalliance)所規範的介面,AOP Alliance是由許多團體所組成的聯合計畫(Joint project),這些團體對於 AOP的實作要求必須遵循制訂出來的介面規範,目的是對 Java

Page 16: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�16

的 AOP實作介面標準化,以增加 AOP實作類別在不同的 Java應用程式之間的可移植性。 在 Spring 2.0中,您的 Advices可以不實作任何介面,直接在設定檔中定義相關資訊,就可以為 Targets 導入 Advices。您也可以選擇使用Annotation的方式,在類別之中直接標示,由 Spring為您完成所有的代理物件設定。

Spring的 Advices是在執行時期導入至 Targets,您可以讓 Targets實作預先定義好的介面,則 Spring在執行時期會使用 java.lang.reflect.Proxy來進行動態代理,如果不實作介面,則 Spring 會使用 CGLIB 為 Targets產生一個子類別作為代理類別(Proxy classes)。 在 Spring AOP 中,您應該以實作介面的方式為優先,這可以讓應用程式的組件彼此之間的耦合度降低,使用 Proxy classes的方式,由於必須產生子類別,所以對於被宣告為 final的方法無法進行代理,而且這個方式基本上是讓一些無法更動原始碼的第三方(Third-party)類別或是舊類別(Legacy classes)來使用。

Spring只支援方法的 Joinpoints,也就是 Advices將在方法執行的前後被應用,Spring不支援 Field成員的 Jointpoints,這是因為在 Spring的設計哲學中認為,支援 Field成員的 Joinpoints會破壞物件的封裝性。 在 Spring 2.0中提供了三種實現 AOP的方式。 第一種是傳統實作 Spring API的方式,也就是 Spring 1.2或先前版本實現 AOP的方式,您必須實作 Spring當中的 AOP API來定義 Advice,並設定代理物件,本章一開始介紹 Spring AOP 時,首先要介紹的就是這種方式,好處是您可以掌握 Spring AOP的許多細節,在使用 Spring 2.0新的AOP設定方式時,會容易理解其背後運作的原理。

Page 17: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�17

第二種是基於 XML的設定,您必須使用基於 Schema的 XML設定,並使用 Spring 2.0新的<aop>標籤,在設定上會比第一種方式簡潔許多,而且 Advice不用實作特定的介面。 第 三 種是 使 用 @AspectJ 的 Annotation 支 援 ,由 於 使用 到Annotation,所以這種方式必須在 JDK 5.0以上的版本才可以運作,好處是不用在 XML 文件中作繁瑣的設定,只要在 Advice 上標記適當的Annotation即可完成設定。

Page 18: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�18

4.2 Advices

Advices實作了 Aspect的真正邏輯,在 Java中具體來說就是一個類別,或更細粒度的設計為一個方法(由一個類別來集中管理許多 Advices)。由於縫合至 Targets 的時機不同,Spring 提供了幾種不同的 Advices,像是Before Advice、After Advice、Around Advice、Throw Advice。在這個小節中,您可以從撰寫、使用 Advices來開始認識 Spring AOP。

4.2.1 Before Advice

Before Advice 會在目標物件的方法執行之前被呼叫,您可以實現org.springframework.aop.MethodBeforeAdvice介面來實作 Before Advice的邏輯,該介面的定義如下所示:

package org.springframework.aop;

public interface MethodBeforeAdvice extends BeforeAdvice {

void before(Method method, Object[] args,

Object target) throws Throwable;

} 在定義中可以看到,MethodBeforeAdvice繼承自 BeforeAdvice介面,而 BeforeAdvice 介面又繼承自 Advice 介面,後兩者都是標籤介面(Tag

interface),只是用作標示而無定義任何方法,MethodBeforeAdvice繼承了 BeforeAdvice,before() 方法會在目標物件(Target)所指定的方法執行之前被執行,您可以取得被執行的Method實例、引數及目標物件,before() 方法上宣告為 void,所以不傳回任何的結果,在 before() 方法執行完畢之後,除非您丟出例外,否則目標物件上的方法就會被執行。 以實例來示範如何使用 Before Advice,首先定義目標物件必須實作的介面:

Page 19: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�1�

BeforeAdviceDemo IHello.java

package onlyfun.caterpillar;

public interface IHello {

public void hello(String name);

} 接著定義一個 HelloSpeaker類別,讓其實現 IHello介面:

BeforeAdviceDemo HelloSpeaker.java

package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {

public void hello(String name) {

System.out.println("Hello, " + name);

}

} 現在 HelloSpeaker已經撰寫完畢,在不對它進行任何修改的情況下,您想要在 hello() 方法執行之前,可以記錄一些訊息,想像一下這是您拿到的一個組件,您沒有原始碼,但想對它增加一些日誌的服務。您可以實作MethodBeforeAdvice介面,例如:

BeforeAdviceDemo LogBeforeAdvice.java

package onlyfun.caterpillar;

import java.lang.reflect.Method;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.springframework.aop.MethodBeforeAdvice;

public class LogBeforeAdvice

implements MethodBeforeAdvice {

private Logger logger =

Logger.getLogger(this.getClass().getName());

Page 20: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�2�

public void before(Method method, Object[] args,

Object target) throws Throwable {

logger.log(Level.INFO,

"method starts..." + method);

}

} 在 before() 方法的實作中,您加入了一些記錄資訊的程式碼,LogBeforeAdvice 類別被設計為一個獨立的服務,可以提供服務給需要的物件,接著只要在定義檔中如下定義:

BeforeAdviceDemo beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="logBeforeAdvice"

class="onlyfun.caterpillar.LogBeforeAdvice"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<bean id="helloProxy"

class="org.springframework.aop.framework.ProxyFactoryBean">

<property name="proxyInterfaces"

value="onlyfun.caterpillar.IHello"/>

<property name="target" ref="helloSpeaker"/>

<property name="interceptorNames">

<list>

<value>logBeforeAdvice</value>

</list>

</property>

</bean>

</beans>

Page 21: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�21

注意到除了建立 Advice 及 Target 的物件實例之外,您還使用了org.springframework.aop.framework.ProxyFactoryBean,這個類別會被 BeanFactory 或是 ApplicationContext 用來建立代理物件(回憶一下前一個小節,Spring AOP主要是透過代理機制來實現,因而需要建立代理物件),您要在 "proxyInterfaces" 屬性上設定代理時可運用的介面,在

"target" 上設定 Target物件,在 "interceptorNames" 上設定 Advice實例,在不指定目標方法時,Before Advice會被縫合(Weave)至介面上所有定義的方法之前。 可以撰寫以下的程式測試一下 Before Advice的運作:

BeforeAdviceDemo SpringAOPDemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringAOPDemo {

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

IHello helloProxy =

(IHello) context.getBean("helloProxy");

helloProxy.hello("Justin");

}

} 記得在操作取回的代理物件時,必須轉換操作介面為 IHello介面,來看一下這個程式的執行結果:

Page 22: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�22

圖 4.4 BeforeAdviceDemo 專案的執行結果 在這個範例中可以看到,您所設計的 HelloSpeaker與 LogBeforeAdvice是兩個獨立的物件,對於 HelloSpeaker來說,它不用知道 LogBeforeAdvice的存在(也就是沒有任何與 LogBeforeAdvice 相關的 API 撰寫在 Hello-

Speaker中),而 LogBeforeAdvice也可以運用至其它的物件之上,Hello-

Speaker與 LogBeforeAdvice都是可以重複使用的設計。 可以看出 AOP 的精神,著重於 Aspects 的辨識,設計可重複使用的Advices,就如 OOP重視物件的辨識,設計可重複使用的物件。 如果您是逐一將 .jar 檔案加入至專案管理之中的話,記得將

spring-core.jar、spring-beans.jar、spring-context.jar、spring-aop.jar與 commons-logging.jar等檔案加入 Classpath。

4.2.2 After Advice

After Advice會在目標方法執行之後被呼叫,您可以實現 org.spring-

framework.aop.AfterReturningAdvice介面來實作 After Advice的邏輯,AfterReturningAdvice介面的定義如下:

package org.springframework.aop;

public interface AfterReturningAdvice extends Advice {

void afterReturning(Object returnValue, Method m,

Page 23: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�23

Object[] args, Object target) throws Throwable;

}

AfterReturningAdvice直接繼承自 Advice介面,afterReturning() 當中傳入的引數有目標方法的回傳值、方法實例、引數、與目標物件,afterReturning() 方法傳回 void,若要中止接下來的應用程式流程,丟出例外是唯一的方式。 可以在 Before Advice中介紹的例子當中,為HelloSpeaker的 hello() 方法執行之後,加上 AfterReturningAdvice,首先定義一個 LogAfterAdvice類別來實作 AfterReturningAdvice介面:

AfterAdviceDemo LogAfterAdvice.java

package onlyfun.caterpillar;

import java.lang.reflect.Method;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.springframework.aop.AfterReturningAdvice;

public class LogAfterAdvice

implements AfterReturningAdvice {

private Logger logger =

Logger.getLogger(this.getClass().getName());

public void afterReturning(Object object,

Method method,

Object[] args,

Object target) throws Throwable {

logger.log(Level.INFO, "method ends..." + method);

}

} 接著只要在 beans-config.xml 中增加 After Advice 的實例,以及在ProxyFactoryBean 中的 "interceptorNames" 增加對 LogAfterAdvice 的參考,定義檔撰寫如下:

Page 24: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�24

AfterAdviceDemo beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="logBeforeAdvice"

class="onlyfun.caterpillar.LogBeforeAdvice"/>

<bean id="logAfterAdvice"

class="onlyfun.caterpillar.LogAfterAdvice"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<bean id="helloProxy"

class="org.springframework.aop.framework.ProxyFactoryBean">

<property name="proxyInterfaces"

value="onlyfun.caterpillar.IHello"/>

<property name="target" ref="helloSpeaker"/>

<property name="interceptorNames">

<list>

<value>logBeforeAdvice</value>

<value>logAfterAdvice</value>

</list>

</property>

</bean>

</beans> 在定義中,除了之前設定的 LogBeforeAdvice 之外,還加入了LogAfterAdvice 的日誌服務,其它的程式與 BeforeAdviceDemo 專案相同,執行時的一個參考畫面如下所示:

Page 25: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�2�

圖 4.5 AfterAdviceDemo 專案的執行結果 您也可以設計一個 LogAspect 類別,讓它同時實現 Method-

BeforeAdvice 與 AfterReturningAdvice 介面,本節的範例為了強調介面的實作,因而將 Advice 分開設計。

4.2.3 Around Advice 在 Before Advice與 After Advice的介紹中,您已經知道如何在目標物件的方法執行前、後介入 Advices 的服務邏輯,事實上如果您要在方法執行前後加入 Advices的服務邏輯,您可以直接透過實作 org.aopalliance.

intercept.MethodInterceptor介面,於方法執行前、後執行相關的服務,而不用分別提供 Before Advice與 After Advice,MethodInterceptor介面的定義如下:

package org.aopalliance.intercept;

public interface MethodInterceptor {

public Object invoke(

MethodInvocation methodInvocation) throws Throwable;

} 注意到 MethodInterceptor的套件名稱是 org.aopalliance.intercept,可以得知這個介面是由 AOP Alliance 所制訂,這表示實作 MethodInter-

ceptor介面的類別,將可以相容於遵守 AOP Alliance規範的 AOP框架。

Page 26: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�26

與 Before Advice及 After Advice不同的是,在 MethodInterceptor的invoke() 方法中,您要自行決定是否使用 org.aopalliance.intercept.

MethodInvocation的 proceed() 方法來執行目標物件的方法。proceed() 會回傳方法執行後的 Object執行結果,所以在 invoke() 結束之前,您有機會修改這個物件,或是回傳另一個完全不相干的物件,當然的,只有在真正必要的時候才會這麼做。 實際來看看如何實作 MethodInterceptor,可以直接在之前的 AfterAdvice-

Demo專案進行撰寫:

AroundAdviceDemo LogInterceptor.java

package onlyfun.caterpillar;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.aopalliance.intercept.MethodInterceptor;

import org.aopalliance.intercept.MethodInvocation;

public class LogInterceptor implements MethodInterceptor {

private Logger logger =

Logger.getLogger(this.getClass().getName());

public Object invoke(MethodInvocation methodInvocation)

throws Throwable {

logger.log(Level.INFO,

"method starts..." + methodInvocation.getMethod());

Object result = null;

try {

result = methodInvocation.proceed();

}

finally {

logger.log(Level.INFO,

"method ends..." +

methodInvocation.getMethod() + "\n");

}

Page 27: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�27

return result;

}

} 可以用這個 Interceptor來取代AfterAdviceDemo專案中的 LogBefore-

Advice與 LogAfterAdvice,Bean定義檔的撰寫方式如下所示:

AroundAdviceDemo beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="logInterceptor"

class="onlyfun.caterpillar.LogInterceptor"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<bean id="helloProxy"

class="org.springframework.aop.framework.ProxyFactoryBean">

<property name="proxyInterfaces"

value="onlyfun.caterpillar.IHello"/>

<property name="target" ref="helloSpeaker"/>

<property name="interceptorNames">

<list>

<value>logInterceptor</value>

</list>

</property>

</bean>

</beans> 執行結果與 AfterAdviceDemo專案的執行結果是一樣的,可以參考圖4.5的畫面。

Page 28: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�28

Spring 在真正執行某個方法前,會先插入 Interceptor,如果有多個Interceptor,每個 Interceptor會執行自己的處理,然後執行MethodInvocation的 proceed() 方法,這將執行流程轉給下一個 Interceptor,如果沒有下一個Interceptor,就執行真正呼叫的方法,方法執行完畢後,再一層一層返回Interceptor堆疊,最後離開堆疊返回應用程式本身的流程。

4.2.4 Throw Advice 若想要在例外發生時通知某些服務物件作某些事,您可以使用 Throws

Advice,在 Spring中想要實作 Throws Advice,必須實作 org.springframe-

work.aop.ThrowsAdvice介面,然而這個介面並沒有定義任何的方法,它只是一個標籤介面(Tag interface),您可以在當中定義 afterThrowing方法名稱,只要它是以下的形式:

afterThrowing([Method], [args], [target], subclassOfThrowable); 方括號 [] 中的設定,例如 Method、args與 target等表示是可以省略的,方法中一定要的是 subclassOfThrowable,也就是這個參數必須是Throwable 的子類別,在例外發生時,會檢驗所設定的 Throws Advice 中是否有符合例外類型的方法,如果有的話就通知它執行,以下是兩個方法宣告的例子:

void afterThrowing(Throwable throwable);

void afterThrowing(Method method, Object[] args,

Object target, Throwable throwable); 在方法上如果宣告不同的 Throwable 型態,則依例外發生類型的不同,會通知不同的方法,例如 SomeException會通知宣告有 SomeException參數的方法,而 OtherException會通知宣告有 OtherException的方法。

Page 29: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�2�

注意到當例外發生時,Throw Advice的任務只是執行對應的方法,您並不能在 Throws Advice中將例外處理掉,在 Throw Advice執行完畢後,原先的例外仍被傳播至應用程式之中,Throw Advice並不介入應用程式的例外處理,例外處理仍舊是應用程式本身所要負責的,如果想要在 Throw

Advice處理時中止應用程式的處理流程,作法是丟出其它的例外。 來看個 Throws Advice的實際例子,首先定義 IHello介面:

ThrowAdviceDemo IHello.java

package onlyfun.caterpillar;

public interface IHello {

public void hello(String name) throws Throwable;

} 接著定義一個 HelloSpeaker類別來實作 IHello介面,並在 hello() 方法當中模擬程式發生錯誤時的例外丟出:

ThrowAdviceDemo HelloSpeaker.java

package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {

public void hello(String name) throws Throwable {

System.out.println("Hello, " + name);

// 抱歉!程式錯誤!發生例外 XD

throw new Exception("發生例外...");

}

} 如果您需要在應用程式丟出例外時,介入 Throw Advice 提供一些服務,例如記錄一些例外資訊,則可以實作 ThrowAdvice介面:

Page 30: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�3�

ThrowAdviceDemo SomeThrowAdvice.java

package onlyfun.caterpillar;

import java.lang.reflect.Method;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.springframework.aop.ThrowsAdvice;

public class SomeThrowAdvice implements ThrowsAdvice {

private Logger logger =

Logger.getLogger(this.getClass().getName());

public void afterThrowing(Method method, Object[] args,

Object target, Throwable subclass) {

// 記錄例外

logger.log(Level.INFO,

"Logging that a " + subclass +

"Exception was thrown in " + method);

}

} 接著在 Bean定義檔中寫下以下的定義,讓 Throw Advice在例外發生時提供日誌服務:

ThrowAdviceDemo beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="someThrowAdvice"

class="onlyfun.caterpillar.SomeThrowAdvice"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<bean id="helloProxy"

Page 31: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�31

class="org.springframework.aop.framework.ProxyFactoryBean">

<property name="proxyInterfaces"

value="onlyfun.caterpillar.IHello"/>

<property name="target" ref="helloSpeaker"/>

<property name="interceptorNames">

<list>

<value>someThrowAdvice</value>

</list>

</property>

</bean>

</beans> 可以撰寫以下的程式來測試一下 Throw Advice是否如預期般運作:

ThrowAdviceDemo SpringAOPDemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringAOPDemo {

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

IHello helloProxy =

(IHello) context.getBean("helloProxy");

try {

helloProxy.hello("Justin");

}

catch(Throwable throwable) {

// 應用程式的例外處理

System.err.println(throwable);

}

}

}

Page 32: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�32

在程式中,應用程式本身定義了例外處理的邏輯,Throw Advice不介入應用程式處理例外的邏輯,Throw Advice提供額外的日誌服務,來看看一個執行時的參考畫面:

圖 4.6 ThrowAdviceDemo 專案的執行結果

Page 33: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�33

4.3 Pointcut、Advisor 在前一個小節中,所定義的 Advice 都是直接縫合至代理的介面執行前、後,或者在執行方法過程中例外發生時,事實上還可以定義更細部的縫合時機。Pointcut定義了感興趣的 Jointpoint(Advice的應用時機)。在Spring中,使用 PointcutAdvisor提供 Pointcut 實例,具體結合 Advice,Spring內建的 Pointcut都有對應的 PointcutAdvisor。

4.3.1 NameMatchMethodPointcutAdvisor

Pointcut定義了 Jointpoint,在 Spring中使用 PointcutAdvisor來提供Pointcut、結合 Advice,PointcutAdvisor 為 Advisor 的子介面,Advisor介面於的定義如下:

package org.springframework.aop;

import org.aopalliance.aop.Advice;

public interface Advisor {

boolean isPerInstance();

Advice getAdvice();

}

PointcutAdvisor介面於 Spring中的定義如下:

package org.springframework.aop;

public interface PointcutAdvisor extends Advisor {

Pointcut getPointcut();

}

Spring中內建的 Pointcut都有對應的 PointcutAdvisor,在這邊先來介紹一下,如何使用 Spring 所提供的 org.springframework.aop.support.

Page 34: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�34

NameMatchMethodPointcutAdvisor,這是最基本的 PointcutAdvisor,用以提供 Spring 中靜態 Pointcut 實例,可使用表示式(expression)指定Advice所要應用的目標上之方法名稱,或者是用 * 來指定,例如 hello* 表示執行代理物件上以 hello 作為開頭的方法名稱時,都會應用指定的Advices(在這個主題之前的例子,Advice會被套用至所有代理的方法)。 舉個實際的例子來說,假設您定義了 IHello的介面:

NameMatchDemo IHello.java

package onlyfun.caterpillar;

public interface IHello {

public void helloNewbie(String name);

public void helloMaster(String name);

} 接著定義 HelloSpeaker類別來實作 IHello介面:

NameMatchDemo HelloSpeaker.java

package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {

public void helloNewbie(String name) {

System.out.println("Hello, " + name + " newbie!");

}

public void helloMaster(String name) {

System.out.println("Hello, " + name + " master!");

}

} 接著可以撰寫一個簡單的 Advice,例如這邊會使用到 BeforeAdvice-

Demo 專案中的 LogBeforeAdvice,基於篇幅有限,請參考 4.2.1 中的LogBeforeAdvice類別之撰寫,這邊不再重複列出程式碼。

Page 35: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�3�

接著撰寫以下的 Bean 定義檔,使用 NameMatchMethodPointcut-

Advisor提供 Pointcut實例,並將 Advice結合在一起:

NameMatchDemo beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="logBeforeAdvice"

class="onlyfun.caterpillar.LogBeforeAdvice"/>

<bean id="helloAdvisor"

class="org.springframework.aop.

→ support.NameMatchMethodPointcutAdvisor">

<property name="mappedName" value="*Newbie"/>

<property name="advice" ref="logBeforeAdvice"/>

</bean>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<bean id="helloProxy"

class="org.springframework.aop.framework.ProxyFactoryBean">

<property name="proxyInterfaces"

value="onlyfun.caterpillar.IHello"/>

<property name="target" ref="helloSpeaker"/>

<property name="interceptorNames">

<list>

<value>helloAdvisor</value>

</list>

</property>

</bean>

</beans> 在 NameMatchMethodPointcutAdvisor的 "mappedName" 屬性上,由於指定了 "*Newbie",所以執行 helloNewbie()方法時,由於方法名稱符合

Page 36: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�36

"*Newbie",就會應用"logBeforeAdvice"的服務邏輯,可以撰寫以下的程式來進行測試,看看結果是否符合預期:

NameMatchDemo SpringAOPDemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringAOPDemo {

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

IHello helloProxy =

(IHello) context.getBean("helloProxy");

helloProxy.helloNewbie("Justin");

helloProxy.helloMaster("caterpillar");

}

} 執行時的一個結果參考畫面如下所示:

圖 4.7 NameMatchDemo 專案的執行結果 您也可以在定義 Advisor 時,使用 "mappedNames" 屬性進行多個Pointcut表示式定義,例如可修改 beans-config.xml為以下的內容:

Page 37: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�37

<beans ...>

....

<bean id="helloAdvisor"

class="org.springframework.aop.

→ support.NameMatchMethodPointcutAdvisor">

<property name="mappedNames">

<list>

<value>helloNewbie</value>

<value>helloMaster</value>

</list>

</property>

<property name="advice" ref="logBeforeAdvice"/>

</bean>

....

</beans>

4.3.2 RegExpMethodPointcutAdvisor

Spring 提供的 org.springframework.aop.support.RegexpMethod-

PointcutAdvisor 可以讓您使用 Regular expression 來撰寫 Pointcut 表示式,用以提供 Spring中靜態 Pointcut的實例,在符合 Regular expression的情況下應用 Advices,您可以使用以下的幾個符號: 表 4.1 RegExpMethodPointcutAdvisor定義時可用的符號 符號 描述

. 符合任何單一字元

+ 符合前一個字元一次或多次

* 符合前一個字元零次或多次

\ Escape 任何 Regular expression 使用到的符號

Page 38: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�38

RegexpMethodPointcutAdvisor的 "pattern" 屬性讓您指定所要符合的完整類別名稱(包括套件名稱)加方法名稱,例如若要求符合onlyfun.caterpillar.IHello下的 hello開始的方法名稱,則要如下撰寫:

onlyfun\.caterpillar\.IHello\.hello.* 由於 . 符號已經被 Regular expression使用,所以表示式中要指定 . 符號,則要跳脫(Escape),也就是使用 \. 的方式,如果只打算針對方法名稱比對,而不管套件名稱,則可以這麼撰寫:

.*hello.* 可以使用 NameMatchDemo專案中所使用的類別與 Advice,只要改一下 Bean定義檔就可以了,例如:

RegexpMatchDemo beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="logBeforeAdvice"

class="onlyfun.caterpillar.LogBeforeAdvice"/>

<bean id="regExpAdvisor"

class="org.springframework.aop.

→ support.RegexpMethodPointcutAdvisor">

<property name="pattern" value=".*Newbie"/>

<property name="advice" ref="logBeforeAdvice"/>

</bean>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<bean id="helloProxy"

Page 39: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�3�

class="org.springframework.aop.framework.ProxyFactoryBean">

<property name="proxyInterfaces"

value="onlyfun.caterpillar.IHello"/>

<property name="target" ref="helloSpeaker"/>

<property name="interceptorNames">

<list>

<value>regExpAdvisor</value>

</list>

</property>

</bean>

</beans> 在上面的定義中,設定符合方法名稱為 Newbie 結尾的方法要應用Advice,且無論其前面的套件、類別名稱,所以執行結果與 NameMatch-

Demo專案的執行結果是一樣的。

4.3.3 ControlFlowPointcut

org.springframework.aop.support.ControlFlowPointcut是 Sping所提供的類別,作用為判斷在方法的執行堆疊中,某個指定類別的某方法中,是否曾經要求您的目標物件執行某個動作,由於這是在執行時期才會確定是否介入 Advices,所以是 Spring提供的動態 Pointcut功能。 以 NameMatchMethodPointcutAdvisor 中的 LogBeforeAdvice 類別為例,您想要知道在 onlyfun.caterpillar.Some類別中,是否在某個方法中要求過指定的目標物件執行某些動作,如果有的話,則介入 LogBefore-

Advice來提供日誌服務,可以將 Bean定義檔如下撰寫:

ControlFlowDemo beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

Page 40: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�4�

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="some" class="onlyfun.caterpillar.Some"/>

<bean id="logBeforeAdvice"

class="onlyfun.caterpillar.LogBeforeAdvice"/>

<bean id="helloFlowControlPointcut"

class="org.springframework.aop.support.ControlFlowPointcut">

<constructor-arg value="onlyfun.caterpillar.Some"/>

</bean>

<bean id="helloAdvisor"

class="org.springframework.aop.support.DefaultPointcutAdvisor">

<property name="advice" ref="logBeforeAdvice"/>

<property name="pointcut" ref="helloFlowControlPointcut"/>

</bean>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<bean id="helloProxy"

class="org.springframework.aop.framework.ProxyFactoryBean">

<property name="proxyInterfaces"

value="onlyfun.caterpillar.IHello"/>

<property name="target" ref="helloSpeaker"/>

<property name="interceptorNames">

<list>

<value>helloAdvisor</value>

</list>

</property>

</bean>

</beans> 在 ControlFlowPointcut建構時,指定了 onlyfun.caterpillar.Some類別,表示若在 Some 類別中的某個方法要求了指定的目標物件(也就是"helloSpeaker"實例)執行某些動作,則應用 Before Advice(logBefore-

Advice)提供日誌的服務,Some類別如下撰寫:

Page 41: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�41

ControlFlowDemo Some.java

package onlyfun.caterpillar;

import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;

public class Some implements ApplicationContextAware {

private IHello helloProxy;

public void setApplicationContext(

ApplicationContext context) throws BeansException {

helloProxy = (IHello) context.getBean("helloProxy");

}

public void helloEverybody() {

helloProxy.helloNewbie("Justin");

helloProxy.helloMaster("caterpillar");

}

public void hello() {

System.out.println("Hello!");

}

} 為了方便取得 ApplicationContext 以獲得 helloSpeaker 的代理物件,Some 類別實現了 org.springframework.context.ApplicationContextAware介面,接著可以撰寫一個簡單的程式來測試一下 ControlFlowPointcut的運作,如下所示:

ControlFlowDemo SpringAOPDemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringAOPDemo {

Page 42: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�42

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

Some some = (Some) context.getBean("some");

if(args.length > 0 && "run".equals(args[0])) {

some.helloEverybody();

}

else {

some.hello();

}

}

} 其餘未列出的程式,都與 NameMatchDemo專案中的程式相同,這邊就不再列出,完整的程式內容可以參考 ControlFlowDemo專案,來看一下執行的結果。 如果執行程式時沒有提供 "run" 引數,則只會出現 "Hello!" 的文字訊息,這是因為沒有執行 Some實例的 helloEverybody(),在執行堆疊中並不符合所指定的定義:Some 類別的某方法曾要求"helloSpeaker"實例執行某些動作。 如果執行程式時提供了 "run" 引數,則會執行 Some 類別的helloEverybody() 方法,方法中要求 "helloSpeaker"的代理物件執行helloNewbie() 與 helloMaster() 方法,符合 Bean定義檔中指定的內容,因而會應用 LogBeforeAdvice來提供服務訊息,一個執行的結果如下所示:

Page 43: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�43

圖 4.8 ControlFlowDemo 專案的執行結果 動態 Pointcut的問題就是在於效能上的付出,由於執行堆疊的判斷是在執行時期進行,所以執行時會很慢,在不同的 JDK 上可能會有 5 到 10倍的效能延遲,因此建議在可能的情況下,儘量使用靜態 Pointcut。 在 Eclipse 中要指定執行時的引數,可以執行 Eclipse 選單上的「Run/Run...」指令,然後選擇「Arguments」頁籤,在「Program

arguments」文字框中輸入引數,例如:

圖 4.9 在 Eclipse 中指定執行時的引數

Page 44: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�44

4.3.4 Pointcut 介面

Spring的 Pointcut是透過實作 org.springframework.aop.Pointcut介面來實現,其定義如下:

package org.springframework.aop;

public interface Pointcut {

ClassFilter getClassFilter();

MethodMatcher getMethodMatcher();

Pointcut TRUE = TruePointcut.INSTANCE;

}

Pointcut.TRUE 是 Pointcut 介面的簡單實作,它傳回的 ClassFilter 是ClassFilter.TRUE,而傳回的 MethodMatcher是傳回 MethodMatcher.TRUE。

ClassFilter介面決定一個類別是否要應用 Advice,其定義如下所示:

package org.springframework.aop;

public interface ClassFilter {

boolean matches(Class clazz);

ClassFilter TRUE = TrueClassFilter.INSTANCE;

}

matches() 方法中要決定傳入的類別是不是符合 Pointcut 的定義,ClassFilter.TRUE 是 ClassFilter 介面的簡單實作,它的 matches() 方法總是傳回 true,如果想要建立的 Pointcut只考慮到方法名稱,則可以使用這個方法。 而 MethodMatcher決定某個方法是否要應用 Advice,其定義如下所示:

package org.springframework.aop;

import java.lang.reflect.Method;

Page 45: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�4�

public interface MethodMatcher {

boolean matches(Method method, Class targetClass);

boolean isRuntime();

boolean matches(Method method,

Class targetClass, Object[] args);

MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;

}

matches() 方法決定某個類別的某個方法是否符合 Pointcut定義,有兩個版本,第一個版本使用於靜態 Pointcut,像是 NameMatchMethodPoint-

cutAdvisor、RegExpMethodPointcutAdvisor,第一個方法總是會被執行,如果是靜態 Pointcut,則 isRuntime() 會傳回 false,此時第二個 matches() 方法不會被執行,只有在 isRuntime() 為 true時,第二個版本的 matches() 才會被執行,例如 ControlFlowPointcut。

MethodMatcher.TRUE是 MethodMatcher的簡單實作,它的第一個版本的 matches() 總是傳回 true, isRuntime() 總是傳回 false,表示靜態Pointcut,所以不可以執行第二個版本的 matches() 方法,否則就會丟出UnsupportedOperationException例外。

4.3.5 Pointcut 交集、聯集操作 如果建立了多個 Pointcuts,並且在這之中,您發現有幾個 Pointcuts的定義可以組合在一起,成為另一個 Pointcuts的定義時,您可以直接對現在的 Pointcuts物件,進行交集(Intersection)、聯集(Union)的操作,org.springframework.aop.support.ComposablePointcut 是 Pointcut 的實作類別,可以操作它的 intersection()、union() 方法來完成這個操作:

ComposablePointcut intersection(ClassFilter filter);

ComposablePointcut intersection(MethodMatcher mm)

ComposablePointcut intersection(Pointcut other)

ComposablePointcut union(ClassFilter filter)

Page 46: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�46

ComposablePointcut union(MethodMatcher mm) 例如:

Pointcut pc = new ComposablePointcut()

.union(classFilter)

.intersection(methodMatcher)

.intersection(pointcut); 在 ComposablePointcut中沒有方法可以處理兩個 Pointcut的聯集,您要使用 org.springframework.aop.support.Pointcuts的 union() 方法,例如:

Pointcut union = Pointcuts.union(pointcut1, pointcut2); 當然的,這是必須在程式中直接撰寫才可以進行交集或聯集操作,如果您希望在 Bean定義檔中直接宣告,則可以設計一個類別,然後於 Bean定義檔中宣告該類別的實例,這可以增加 Pointcut操作的重用性,例如:

package onlyfun.caterpillar;

import java.util.List;

import org.springframework.aop.ClassFilter;

import org.springframework.aop.MethodMatcher;

import org.springframework.aop.Pointcut;

import org.springframework.aop.

framework.AopConfigException;

import org.springframework.aop.support.Pointcuts;

public class UnionPoint implements Pointcut {

private Pointcut pointcut;

public ClassFilter getClassFilter() {

return getPointcut().getClassFilter();

}

public MethodMatcher getMethodMatcher() {

return getPointcut().getMethodMatcher();

Page 47: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�47

}

public void setPointcuts(List pointcuts) {

if(pointcuts == null || pointcuts.size == 0) {

throw new AopConfigException(

"至少要設定一個 Pointcut");

}

pointcut = (Pointcut) pointcuts.get(0);

for(int i = 0; i < pointcuts.size(); i++) {

Pointcut next = (Pointcut) pointcuts.get(i);

pointcut = Pointcuts.union(pointcut, next);

}

}

private Pointcut getPointcut() {

if(pointcut == null) {

throw new AopConfigExeption("沒有設定 Pointcut");

}

return pointcut;

}

}

Page 48: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�48

4.4 Introduction 在 Spring中,Introduction是一種特殊的 Advice,從行為上來看,它不像 BeforeAdvice、After Advice等 Advice在方法前後介入服務,而是直接介入整個物件的行為,就好像物件憑空多了一些可操作的行為,為物件動態加入(Mixin)原先所沒有的職責。

4.4.1 IntroductionInterceptor 對於之前介紹過的 Before Advice、After Advice、Around Advice、Throw Advice,從使用者的角度來看,它們「影響了目標物件上某些方法的行為」,例如讓某些方法看來似乎增加了一些日誌的動作。

Introduction 是個特別的 Advice,從使用者的角度來看,它「影響了目標物件的行為定義,直接增加了目標物件的職責(具體來說就是增加了可操作的方法)」,例如讓某個已定義好的物件,在不修改該物件之類別檔案的情況下,卻可以增加一些額外的操作方法到物件之上。 就 Java程式語言類別設計的觀點來說,動態為物件增加可操作的方法顯得不可思議,事實上在 Spring AOP 中,可以透過實作 org.spring-

framework.aop.IntroductionInterceptor來實現 Introduction。 熟悉動態腳本語言的設計人員,例如熟悉 JavaScript 的設計人員,對動態為物件增加可操作的方法(函式)來說,則是很習以為常的功能。

Page 49: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�4�

IntroductionInterceptor 繼承 MethodInterceptor 與 DynamicIntro-

ductionAdvice介面,其中 implementsInterface() 方法(繼承自 Dynamic-

IntroductionAdvice)如果傳回 true的話,表示目前的 IntroductionInterceptor實作了規定的介面(也就是要額外增加行為的介面),此時您要使用

invoke() 執行介面上的方法,讓目標物件執行額外的行為,您不可使用MethodInvocation 的 proceed() 方法,因為要執行的是物件上原來沒有的行為,執行 proceed() 方法沒有意義。 從文字上來理解 Introduction 會比較抽象,舉個實際的例子來說,假設您的系統中已經有以下的類別:

IntroductionDemo ISome.java

package onlyfun.caterpillar;

public interface ISome {

public void doSome();

}

IntroductionDemo Some.java

package onlyfun.caterpillar;

public class Some implements ISome {

public void doSome() {

System.out.println("原來物件的職責。。。");

}

} 您希望在不修改原始檔案的情況下,為 Some 類別增加一些可操作的方法,也許您甚至連原始碼檔案都沒有,只有 .class 檔案,唯一知道的也許是它們的 API說明,在不對它們做出修改的情況下,希望 Some類別可以增加 doOther() 方法。

Page 50: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4���

在 Spring 中,可以藉由實作 IntroductionInterceptor 介面來完成上面的任務,首先為 doOther() 方法建立介面:

IntroductionDemo IOther.java

package onlyfun.caterpillar;

public interface IOther {

public void doOther();

} 接著定義一個 OtherIntroduction類別同時實作 IntroductionInterceptor介面與 IOther介面,例如:

IntroductionDemo OtherIntroduction.java

package onlyfun.caterpillar;

import org.aopalliance.intercept.MethodInvocation;

import org.springframework.aop.IntroductionInterceptor;

public class OtherIntroduction

implements IntroductionInterceptor, IOther {

// 是否實作自 IOther介面

public boolean implementsInterface(Class clazz) {

return clazz.isAssignableFrom(IOther.class);

}

public Object invoke(MethodInvocation methodInvocation)

throws Throwable {

// 如果執行的方法來自 IOther介面的定義

if(implementsInterface(

methodInvocation.getMethod().getDeclaringClass())) {

// 執行額外加入(mixin)的行為

return methodInvocation.getMethod().

invoke(this, methodInvocation.getArguments());

}

else {

return methodInvocation.proceed();

Page 51: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4��1

}

}

public void doOther() {

System.out.println("增加的職責。。。");

}

} 接著要在 Bean定義檔中將 Introduction縫合至 Some物件之上,使用org.springframework.aop.support.DefaultIntroductionAdvisor 就可以了,例如:

IntroductionDemo beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="some"

class="onlyfun.caterpillar.Some"/>

<bean id="otherIntroduction"

class="onlyfun.caterpillar.OtherIntroduction"/>

<bean id="otherAdvisor"

class="org.springframework.aop.

→ support.DefaultIntroductionAdvisor">

<constructor-arg ref="otherIntroduction"/>

<constructor-arg value="onlyfun.caterpillar.IOther"/>

</bean>

<bean id="proxyFactoryBean"

class="org.springframework.aop.framework.ProxyFactoryBean">

<property name="proxyInterfaces"

value="onlyfun.caterpillar.ISome"/>

<property name="target" ref="some"/>

<property name="interceptorNames">

Page 52: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4��2

<list>

<value>otherAdvisor</value>

</list>

</property>

</bean>

</beans>

DefaultIntroductionAdvisor在建構時,需要給它 IntroductionInterceptor的實例,以及所要代理額外行為的介面,現在,來撰寫一個簡單的程式測試,從這個程式當中,可以更進一步了解何謂為物件額外增加行為:

IntroductionDemo SpringAOPDemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringAOPDemo {

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

ISome some =

(ISome) context.getBean("proxyFactoryBean");

some.doSome();

// 看來好像 some物件動態增加了職責

((IOther) some).doOther();

}

}

Page 53: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4��3

對於 some所參考的物件來說,它原先並不會有 doOther() 方法可供操作,然而透過 Spring AOP的 Introduction機制,現在 some所參考的物件多了 doOther() 方法可以操作,可以來看一下執行的結果:

圖 4.10 IntroductionDemo 專案的執行結果

4.4.2 DelegatingIntroductionInterceptor

org.springframework.aop.support.DelegatingIntroductionInterceptor是 Spring AOP中為 IntroductionInterceptor介面所提供的實作類別,您可以直接繼承這個類別,添加希望為目標物件增加的行為,並可以帶有物件自己的狀態,例如讓物件攜帶有「是」、「否」鎖定的狀態,Delegating-

IntroductionInterceptor已經實作了大部份的細節。 舉個例子來說,假設系統中已經有這樣的類別:

DelegatingIntroductionDemo ISome.java

package onlyfun.caterpillar;

public interface ISome {

public void setSome(String some);

public String getSome();

}

Page 54: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4��4

DelegatingIntroductionDemo Some.java

package onlyfun.caterpillar;

public class Some implements ISome {

private String some;

public void setSome(String some) {

this.some = some;

}

public String getSome() {

return some;

}

} 在不修改 Some.java程式內容的情況下,您希望可以增加一個 locked的 boolean型態資料成員,並增加可操作的 lock() 與 unlock() 方法來設定locked成員為 true或 false,如果 locked被設定為 true,則鎖定 setSome() 方法無法被執行,也就是將物件鎖定為不可變動(Immutable)。 可以先定義一個 ILockable 介面,上面定義的是想添加至目標物件的操作方法:

DelegatingIntroductionDemo ILockable.java

package onlyfun.caterpillar;

public interface ILockable {

public void lock();

public void unlock();

public boolean isLocked();

} 接著繼承 DelegatingIntroductionInterceptor類別,並同時實作 ILockable介面:

Page 55: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4���

DelegatingIntroductionDemo LockIntroduction.java

package onlyfun.caterpillar;

import org.springframework.aop.

support.DelegatingIntroductionInterceptor;

import org.aopalliance.intercept.MethodInvocation;

import org.springframework.aop.

framework.AopConfigException;

public class LockIntroduction

extends DelegatingIntroductionInterceptor

implements ILockable {

private boolean locked;

public Object invoke(MethodInvocation invocation)

throws Throwable {

// locked 為 true下不能執行 set方法

if (isLocked() &&

invocation.getMethod().

getName().indexOf("set") == 0) {

throw new AopConfigException(

"物件被鎖定!!");

}

return super.invoke(invocation);

}

public void lock() {

locked = true;

}

public void unlock() {

locked = false;

}

public boolean isLocked() {

return locked;

}

}

Page 56: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4��6

新增的行為是,當物件使用 lock() 方法設定 locked 為 true 時鎖定物件,如果此時有其它物件打算執行 set 方法則丟出例外,通知呼叫者物件已是在鎖定狀態,至於 Bean定義檔的內容可以如下撰寫:

DelegatingIntroductionDemo beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="some"

class="onlyfun.caterpillar.Some"/>

<bean id="lockIntroduction"

class="onlyfun.caterpillar.LockIntroduction"/>

<bean id="lockAdvisor"

class="org.springframework.aop.

→ support.DefaultIntroductionAdvisor">

<constructor-arg ref="lockIntroduction"/>

<constructor-arg value="onlyfun.caterpillar.ILockable"/>

</bean>

<bean id="proxyFactoryBean"

class="org.springframework.aop.framework.ProxyFactoryBean">

<property name="proxyInterfaces"

value="onlyfun.caterpillar.ISome"/>

<property name="target" ref="some"/>

<property name="interceptorNames">

<list>

<value>lockAdvisor</value>

</list>

</property>

</bean>

</beans>

Page 57: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4��7

來撰寫一個簡單的測試程式,看看如何利用增加的行為來進行物件鎖定:

DelegatingIntroductionDemo SpringAOPDemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringAOPDemo {

public static void main(String[] args) throws Exception {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

ISome some =

(ISome) context.getBean("proxyFactoryBean");

// 物件沒有被鎖定,可以執行 set方法

some.setSome("justin");

System.out.println(some.getSome());

try {

// 物件被鎖定

((ILockable) some).lock();

// 無法執行 set方法,丟出例外

some.setSome("momor");

// 由於會丟出例外,所以下面的這行程式無法被執行

System.out.println(some.getSome());

}

catch(Throwable e) {

e.printStackTrace();

}

// Object is unlocked.

((ILockable) some).unlock();

// It's ok to use setter again.

some.setSome("momor");

System.out.println(some.getSome());

}

Page 58: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4��8

} 執行時在 some 所參考的物件上,可以執行新添加的 lock() 方法來進行鎖定,當 some所參考的物件被鎖定時,則執行 set方法會丟出例外,可以執行 unlock() 方法解除鎖定,一個執行結果如下所示:

圖 4.11 DelegatingIntroductionDemo 專案的執行結果 事實上,Some類別上並沒有真正增加行為,從以上兩個專案的例子中可以看出,Introduction事實上是利用委託的方式,當執行非 Some類別上所定義的方法時,在代理物件中再委託 Introduction 物件來執行,而從使用者的角度來看,就像是物件上平白增加了行為。

Page 59: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4���

4.5 Autoproxing 自動代理可以不用為每一個目標物件手動定義代理物件,使用自動代理,您可以透過 Bean 名稱或是 Pointcut 的比對,自動為符合比對條件的目標物件建立代理物件。

4.5.1 BeanNameAutoProxyCreator 如果要為目標物件提供 Advice,則必須為它們建立代理物件,在應用程式規模大時,如果要提供 Advice的目標物件很多,則一個一個為它們建立代理物件會是件麻煩的事,為此,Spring為一些情況提供自動代理。 您可以為目標物件取好適當的 Bean 名稱,例如為某些服務物件取名為 xxxService,這麼一來,可以使用 org.springframework.aop.frame-

work.autoproxy.BeanNameAutoProxyCreator 來為這些 Bean 設定自動代理,例如 DelegatingIntroductionDemo 中就可以改用 BeanNameAuto-

ProxyCreator來建立自動代理,只要改一下 Bean定義檔就可以了:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="someService"

class="onlyfun.caterpillar.Some"/>

<bean id="lockIntroduction"

class="onlyfun.caterpillar.LockIntroduction"/>

<bean id="lockAdvisor"

class="org.springframework.aop.

→ support.DefaultIntroductionAdvisor">

<constructor-arg ref="lockIntroduction"/>

<constructor-arg ref="onlyfun.caterpillar.ILockable"/>

Page 60: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�6�

</bean>

<bean id="introductionProxyCreator"

class="org.springframework.aop.framework.

→ autoproxy.BeanNameAutoProxyCreator">

<property name="beanNames">

<list>

<value>*Service</value>

</list>

</property>

<property name="interceptorNames" value="lockAdvisor"/>

</bean>

</beans> 執行的結果不變,而這樣的設定,如果打算讓某個目標物件套用Advice時,就只要將其名稱取名為 xxxService就可以了,Spring會自動建立代理物件。

4.5.2 DefaultAdvisorAutoProxyCreator

Spring所提供的自動代理建立者(Autoproxy creator)還有 org.spring-

framework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator,只要在定義檔中加上 DefaultAdvisorAutoProxyCreator的設定,在 Bean定義檔被讀取完之後,DefaultAdvisorAutoProxyCreator 會自動搜尋所有的Advisor,並自動將 Advisor應用至符合 Pointcuts的目標物件上。 例如可以將 BeanNameAutoProxyCreator中介紹的 Bean定義檔更改為以下,剩下的什麼都不用改,執行的結果也是相同的:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

Page 61: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�61

<bean id="someService"

class="onlyfun.caterpillar.Some"/>

<bean id="lockIntroduction"

class="onlyfun.caterpillar.LockIntroduction"/>

<bean id="lockAdvisor"

class="org.springframework.aop.

→ support.DefaultIntroductionAdvisor">

<constructor-arg ref="lockIntroduction"/>

<constructor-arg value="onlyfun.caterpillar.ILockable"/>

</bean>

<bean id="autoProxyCreator"

class="org.springframework.aop.framework.

→ autoproxy.DefaultAdvisorAutoProxyCreator"/>

</beans> 例如將 4.3.2 中介紹 RegExpMethodPointcutAdvisor 中 RegexpDemo專案中的 Bean定義檔修改如下,則執行結果不變:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="logBeforeAdvice"

class="onlyfun.caterpillar.LogBeforeAdvice"/>

<bean id="regExpAdvisor"

class="org.springframework.aop.

→ support.RegexpMethodPointcutAdvisor">

<property name="pattern" value=".*Newbie"/>

<property name="advice" ref="logBeforeAdvice"/>

</bean>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

Page 62: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�62

<bean id="autoProxyCreator"

class="org.springframework.aop.framework.

→ autoproxy.DefaultAdvisorAutoProxyCreator"/>

</beans> 自動代理建立是 Spring在撰寫設定檔時一個方便的工具,但相對的,Bean定義檔容易有些不清不楚的設定,因為不像直接自行在定義檔上撰寫來的清楚,並且您要小心的定義 Pointcut,以免 Advice應用到不該應用到的目標物件上。

Page 63: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�63

4.6 Spring 2.0 的 AOP 支援 在之前的小節中可以看到,AOP 的 Advice 必須實作特定介面,而組態設定依賴於 XML繁瑣的設定。在 Spring 2.0之後,對於 AOP功能的實現與設定新增了兩種方式:一種是基於 XML Schema的設定,一種是基於Annotation支援。兩種方式對於 AOP在使用上的簡化都有極大的幫助,這一個小節將介紹 Spring 2.0新的 AOP支援。

4.6.1 Before Advice:基於 XML Schema 在這邊以實際的例子,來說明 Spring 2.0在 AOP的實現與設定上的改變,以改寫 4.2.1 Before Advice 的 BeforeAdviceDemo 專案為例,LogBeforeAdvice現在不需要實現 MethodBeforeAdvice,改寫後的程式碼如下所示:

BeforeAdviceDemo2 LogBeforeAdvice.java

package onlyfun.caterpillar;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;

public class LogBeforeAdvice {

private Logger logger =

Logger.getLogger(this.getClass().getName());

public void before(JoinPoint jointPoint) {

logger.log(Level.INFO,

"method starts..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

}

Page 64: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�64

在程式碼中,before()方法為任意取的名稱,它可以接受 JointPoint實例,也可以省略。可以使用 JointPoint的 getTarget()方法取得目標物件,使用 getArgs()取得呼叫的方法引數、使用 getSignature()方法取得Pointcut簽署等資訊。事實上 Spring 2.0中,任一個 Advice的方法實作都可以自行決定是否接受 JointPoint實例。 為了使用 Spring 2.0基於 XML Schema的 AOP設定方式,您必須在XML設定檔的開頭加入 aop的名稱空間宣告:

<?xml version="1.0" encoding="UTF-8"?>

<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-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

...

</beans> 加入 aop 的名稱空間宣告之後,就可以使用 Spring 2.0 新的<aop>標籤,在這邊先將改寫後的 beans-config.xml 列出,稍後再加以解釋每個設定項目的意義:

BeforeAdviceDemo2 beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<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-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

<bean id="logBeforeAdvice"

class="onlyfun.caterpillar.LogBeforeAdvice"/>

Page 65: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�6�

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<aop:config>

<aop:aspect id="logging" ref="logBeforeAdvice">

<aop:before

pointcut="execution(* onlyfun.caterpillar.IHello.*(..))"

method="before"/>

</aop:aspect>

</aop:config>

</beans> 首先可以看到,不再需要明確宣告使用 ProxyFactoryBean,所有的AOP 組態是在<aop:config>標籤中設定;<aop:aspect>標籤定義 Aspect的實作,也就是 Advice的實例。

<aop:before>標籤表示設定的 Advice 將作為 Before Advice,"pointcut"屬性定義 Pointcut表示式,以上定義了三個部份:傳回值、方法名稱與參數型態。傳回值設定*表示任何傳回值型態都符合,方法名稱設定onlyfun.caterpillar.IHello.*表示 IHello 介面所宣告的任意方法都符合,參數型態設定(..)表示任何參數型態宣告都符合(稍後還會詳細介紹 Pointcut表示式的撰寫)。"method"屬性設定 Advice 上要呼叫的方法,在這邊設定呼叫 LogBeforeAdvice的 before()方法。 設定好 Bean組態檔之後,接著改寫一下 SpringAOPDemo類別,設定檔中沒有明確設定代理物件了,所以只要直接取得"helloSpeaker"實例,Spring 就會自動依組態設定建立代理物件,並在呼叫對應方法時套用Advice,SpringAOPDemo改寫如下:

Page 66: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�66

BeforeAdviceDemo2 SpringAOPDemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringAOPDemo {

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

IHello helloSpeaker =

(IHello) context.getBean("helloSpeaker");

helloSpeaker.hello("Justin");

}

} 執行的結果如下所示:

圖 4.12 BeforeAdviceDemo2 專案的執行結果 如果需要重用 Pointcut 定義,可以使用<aop:pointcut>標籤,例如這個範例的<aop:config>中也可以改寫為以下的內容:

<aop:config>

<aop:pointcut id="logHello"

expression="execution(* onlyfun.caterpillar.IHello.*(..))"/>

<aop:aspect id="logging" ref="logBeforeAdvice">

<aop:before

pointcut-ref="logHello"

method="before"/>

Page 67: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�67

</aop:aspect>

</aop:config> 相關的 .jar 檔案必須加入 Classpath,您可以直接使用spring.jar,另外還需要 aspectjweaver.jar、asm-*.jar 與 asm-

commons-*.jar。

4.6.2 Before Advice:基於 Annotation

Spring 2.0中還可以基於 Annotation來設定 AOP的 Advice,在 XML的設定上可以更加簡化,這邊改寫 BeforeAdviceDemo2專案,將當中相關的設定改為使用 Annotation的方式,首先可以改寫 LogBeforeAdvice如下:

BeforeAdviceDemo3 LogBeforeAdvice.java

package onlyfun.caterpillar;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

@Aspect

public class LogBeforeAdvice {

private Logger logger =

Logger.getLogger(this.getClass().getName());

@Before("execution(* onlyfun.caterpillar.IHello.*(..))" )

public void before(JoinPoint jointPoint) {

logger.log(Level.INFO,

"method starts..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

Page 68: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�68

}

} 要將一個類別設定為 Aspect,只要使用@Aspect 在類別上標示它是Aspect的實作即可;@Before表示該方法將應用為 Before Advice時呼叫,當中直接撰寫 Pointcut表示式,同樣的,before()方法中的 JointPoint參數可有可無,主要目的是讓您取得一些 JointPoint的相關資訊。 接著在 XML 設定檔中,使用<aop:aspectj-autoproxy/>標籤啟用@Aspect的 Annotation支援,beans-config.xml改寫如下:

BeforeAdviceDemo3 beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<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-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

<bean id="logBeforeAdvice"

class="onlyfun.caterpillar.LogBeforeAdvice"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<aop:aspectj-autoproxy/>

</beans> 可以看到,基於 Annotation的 AOP設定方式,在 XML上幾乎不用設定,只要實例化 Advice 與您的目標物件,接著一切就讓 Spring 自動取得Annotation 資訊、進行代理物件建立等,無須再作任意的設定。這個範例的執行結果與上一個範例是相同的,可以參考圖 4.12的執行結果畫面。

Page 69: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�6�

Spring 中三種 AOP 的實現與設定方式,哪個比較好?在 Spring參考文件的 6.4. Choosing which AOP declaration style to use給了些建議,您可以參考看看。

4.6.3 Spring 2.0 的 Pointcut 定義 在 Spring 2.0中要宣告 Pointcut,主要包括兩個部份:Pointcut表示式(expression)與 Pointcut簽署(signature)。首先來看到 Pointcut表示式,在之前的範例就已經使用過的是 executeion表示式,例如:

execution(* onlyfun.caterpillar.IHello.*(..)) 在 Spring中,execution表示式會是最常使用的 Pointcut表示式,它的語法組成格式如下所示:

execution(modifiers-pattern?

ret-type-pattern

declaring-type-pattern?

name-pattern(param-pattern)

throws-pattern?) 分別表示存取修飾匹配(modifiers-pattern?)、傳回值型態匹配(ret-type-pattern)、類別型態匹配(declaring-type-pattern?)、方法名稱匹配(參數型態匹配)(name-pattern(param-pattern))、例外型態匹配(throws-pattern?),有問號的部份,表示可以省略不宣告。 大部份的情況下,傳回值型態匹配會使用*,表示所有傳回值型態都符合,也可以設定完整型態名稱(fully-qualified type name)。方法名稱匹配也可以使用*。參數型態匹配的話,()表示沒有參數,(type)表示帶有一個型態為 type的參數;(*)表示帶有一個參數,而該參數可以是任意型態;

Page 70: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�7�

也可以設定如(*, String),表示有兩個參數,第一個參數可以是任意型態,而第二個參數是 String;(..)表示零個或多個參數。 接下來舉幾個 execution表示式的實際例子:

� execution(public * *(..)) 符合任何的公開方法。

� execution(* hello*(..)) 符合任何 hello 開頭的方法。

� execution(* onlyfun.caterpillar.IHello.*(..)) 符合 IHello 介面所宣告的任何方法(之前的範例就是這麼宣告)。

� execution(* onlyfun.caterpillar.service.*.*(..)) 符合 onlyfun.caterpillar.service 套件(package)下宣告的任何方法。

� execution(* onlyfun.caterpillar.service..*.*(..)) 符合 onlyfun.caterpillar.service 套件或子套件下宣告的任何方法。 如果只是要定義符合某些型態,則可以使用 within表示式,直接以實例來示範:

� within(onlyfun.caterpillar.service.*) 符合 onlyfun.caterpillar.service 套件下宣告的任何方法。

� within(onlyfun.caterpillar.service..*) 符合 onlyfun.caterpillar.service 套件或子套件下宣告的任何方法。 另外還有 this、target、args 等 Pointcut表示式的使用,若有興趣,可以參考 Spring參考文件,基於篇幅,這邊並不一一加以說明。

Page 71: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�71

在 Spring 2.0 中,結合@Pointcut 的 Annotation 定義方式,可以讓Pointcut在定義上更加容易,定義包括兩個部份:Pointcut表示式與 Pointcut簽署(signature)。例如以下定義一個符合 hello開頭方法的 Pointcut:

@Pointcut("execution(* hello*(..))") // Pointcut 表示式

private void anyHello(); // Pointcut 簽署 若要使用所定義的 Pointcut,則可以指定 Pointcut簽署,例如:

@Before("anyHello()" ) 以上的例子,相當於直接在@Before中定義以下的 Pointcut表示式:

@Before("execution(* hello*(..))" )

Pointcut 定義時,還可以使用&&、 ||、!運算,例如以下的定義中,anyHelloWelcome()簽署將符合任何 hello與 welcome開頭的方法:

@Pointcut("execution(* hello*(..))")

private void anyHello();

@Pointcut("execution(* welcome*(..))")

private void anyWelcome();

@Pointcut("anyHello() && anyWelcome()")

private void anyHelloWelcome(); 還可以將共用的一些 Pointcut組織起來,以供整個應用程式使用,例如可以定義一個 CommonPointcut:

package onlyfun.caterpillar;

public class CommonPointcut {

@Pointcut("execution(* hello*(..))")

private void anyHello();

@Pointcut("execution(* welcome*(..))")

private void anyWelcome();

@Pointcut("execution(* onlyfun.caterpillar.service..*.*(..))")

Page 72: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�72

private void service();

@Pointcut("anyHello() && anyWelcome()")

private void anyHelloWelcome();

...

} 則要使用所定義的 Pointcut 時,可以指定完整類別名稱加上 Pointcut簽署,例如:

...

@Aspect

public class LogBeforeAdvice {

...

@Before("onlyfun.caterpillar.CommonPointcut.service()")

public void before() {

...

}

} 或者是在使用 aop標籤時,指定其"pointcut"屬性,例如:

<aop:before pointcut="onlyfun.caterpillar.CommonPointcut.service()"

method="before"/>

4.6.4 After Returning Advice:基於 XML Schema 在 Spring 2.0 中,可以使用<aop:after-returning>標籤來定義 After

Returning Advice,而不用實作 AfterReturningAdvice介面,例如可以改寫4.2.2 After Advice的 AfterAdviceDemo專案,在當時是將 Before Advice與 After Advice分開撰寫為兩個類別,這邊則將之合併為一個類別,如下所示:

Page 73: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�73

AfterAdviceDemo2 LogAspect.java

package onlyfun.caterpillar;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;

public class LogAspect {

private Logger logger =

Logger.getLogger(this.getClass().getName());

public void before(JoinPoint jointPoint) {

logger.log(Level.INFO, "method starts..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

public void afterReturning(JoinPoint jointPoint) {

logger.log(Level.INFO, "method ends..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

}

LogAspect 中, before()方法將作為 Before Advice 來呼叫,而afterReturing()方法將作為 After Returning Advice來呼叫,After Returning

Advice 還可以設定目標方法呼叫的傳回值,這稍後介紹基於 Annotation的 After Returning Advice時還會示範。接著改寫 Bean定義檔的內容,使用<aop:after-returning>標籤:

AfterAdviceDemo2 beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<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-2.0.xsd

Page 74: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�74

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

<bean id="logAspect"

class="onlyfun.caterpillar.LogAspect"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<aop:config>

<aop:aspect id="logging" ref="logAspect">

<aop:pointcut id="logHello" expression=

"execution(* onlyfun.caterpillar.IHello.*(..))"/>

<aop:before pointcut-ref="logHello" method="before"/>

<aop:after-returning

pointcut-ref="logHello"

method="afterReturning"/>

</aop:aspect>

</aop:config>

</beans>

<aop:after-returning>標籤上的屬性作用,與<aop:before>標籤是類似的,請參考先前介紹<aop:before>標籤時的說明。設定檔中沒有明確設定代理物件了,所以只要取得"helloSpeaker"實例,Spring就會自動依組態設定建立代理物件,並在呼叫對應方法時套用 Advice,SpringAOPDemo 的改寫與先前 BeforeAdviceDemo2專案是相同的,這邊不再列出。以下是執行時的參考畫面:

圖 4.13 AfterAdviceDemo2 專案的執行結果

Page 75: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�7�

4.6.5 After Returning Advice:基於 Annotation

如果要使用 Spring 2.0基於 Annotation的 After Returning Advice定義方式,則要使用@AfterReturing,以下改寫 AfterAdviceDemo2 專案的LogAspect類別:

AfterAdviceDemo3 LogAspect.java

package onlyfun.caterpillar;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.AfterReturning;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.aspectj.lang.annotation.Pointcut;

@Aspect

public class LogAspect {

private Logger logger =

Logger.getLogger(this.getClass().getName());

@Pointcut("execution(* onlyfun.caterpillar.IHello.*(..))")

private void logging() {}

@Before("logging()" )

public void before(JoinPoint jointPoint) {

logger.log(Level.INFO, "method starts..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

@AfterReturning(pointcut="logging()", returning="retVal")

public void afterReturning(JoinPoint jointPoint, Object retVal) {

logger.log(Level.INFO, "method ends..." +

jointPoint.getSignature().getDeclaringTypeName() +

Page 76: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�76

"." + jointPoint.getSignature().getName());

}

} 在這個 LogAspect中,還示範了如何接受目標方法執行完畢後的傳回值,也就是設定"returning"屬性,使用 retVal名稱來接受傳回值,也就是說,有機會修改其所參考的物件。接著再修改一下 beans-config.xml:

AfterAdviceDemo3 beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<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-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

<bean id="logAdvice"

class="onlyfun.caterpillar.LogAspect"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<aop:aspectj-autoproxy/>

</beans> 同樣的,只要使用<aop:aspect-autoproxy>標籤,並設定 Aspect 的實例即可。執行的結果與上一個範例結果是相同的。

4.6.6 After Throwing Advice:基於 XML Schema

After Throwing Advice 的作用即 4.2.4 所介紹的 Throw Advice 之作用,可以使用<aop:after-throwing>來定義,而不用實作 ThrowsAdvice介

Page 77: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�77

面,例如改寫先前 ThrowAdviceDemo 專案的內容,以下的 LogAspect 還結合了 Before Advice、After Returning Advice:

ThrowAdviceDemo2 LogAspect.java

package onlyfun.caterpillar;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;

public class LogAspect {

private Logger logger =

Logger.getLogger(this.getClass().getName());

public void before(JoinPoint jointPoint) {

logger.log(Level.INFO, "method starts..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

public void afterReturning(JoinPoint jointPoint) {

logger.log(Level.INFO, "method ends..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

public void afterThrowing(JoinPoint jointPoint,

Throwable throwable) {

logger.log(Level.INFO, "Logging that a " + throwable +

"\nException was thrown in..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

}

afterThrowing()方法將在發生 Throwable 或其子類例外時被呼叫,在這邊,您也可以設定其它子類別型態,Spring 將自動更新所引發的例外型

Page 78: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�78

態,決定是否呼叫 afterThrowing()方法, "throwable"參數名必須在<aop:after-throwing>的"throwing"屬性當中設定,如下所示:

ThrowAdviceDemo3 beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<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-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

<bean id="logAspect"

class="onlyfun.caterpillar.LogAspect"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<aop:config>

<aop:aspect id="logging" ref="logAspect">

<aop:pointcut id="logHello" expression=

"execution(* onlyfun.caterpillar.IHello.*(..))"/>

<aop:before pointcut-ref="logHello" method="before"/>

<aop:after-returning

pointcut-ref="logHello"

method="afterReturning"/>

<aop:after-throwing

pointcut-ref="logHello"

throwing="throwable"

method="afterThrowing"/>

</aop:aspect>

</aop:config>

</beans>

Page 79: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�7�

而原本 ThrowAdviceDemo專案的 SpringAOPDemo類別則修改如下:

ThrowAdviceDemo3 SpringAOPDemo.java

package onlyfun.caterpillar;

import org.springframework.context.ApplicationContext;

import org.springframework.context.

support.ClassPathXmlApplicationContext;

public class SpringAOPDemo {

public static void main(String[] args) {

ApplicationContext context =

new ClassPathXmlApplicationContext(

"beans-config.xml");

IHello helloSpeaker =

(IHello) context.getBean("helloSpeaker");

try {

helloSpeaker.hello("Justin");

}

catch(Throwable throwable) {

// 應用程式的例外處理

System.err.println(throwable);

}

}

} 執行的結果如下圖所示:

圖 4.14 ThrowAdviceDemo2 專案的執行結果

Page 80: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�8�

從執行結果中也可以看到,由於方法執行過程中發生例外了,方法並未正確執行完畢,所以 After Returning Advice不會執行。

4.6.7 After Throwing Advice:基於 Annotation 基 於 Annotation 的 After Throwing Advice 方 式 , 則 要 使 用@AfterThrowing,這邊直接將之前 ThrowAdviceDemo2 專案的內容修改為基於 Annotation的方式,首先是 LogAspect類別的修改:

ThrowAdviceDemo3 LogAspect.java

package onlyfun.caterpillar;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.AfterReturning;

import org.aspectj.lang.annotation.AfterThrowing;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.aspectj.lang.annotation.Pointcut;

@Aspect

public class LogAspect {

private Logger logger =

Logger.getLogger(this.getClass().getName());

@Pointcut("execution(* onlyfun.caterpillar.IHello.*(..))")

private void logging() {}

@Before("logging()" )

public void before(JoinPoint jointPoint) {

logger.log(Level.INFO, "method starts..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

Page 81: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�81

@AfterReturning(pointcut="logging()", returning="retVal")

public void afterReturning(JoinPoint jointPoint, Object retVal) {

logger.log(Level.INFO, "method ends..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

@AfterThrowing(pointcut="logging()", throwing="throwable")

public void afterThrowing(JoinPoint jointPoint,

Throwable throwable) {

logger.log(Level.INFO, "Logging that a " + throwable +

"\nException was thrown in..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

}

} 接著是 beans-config.xml的修改:

ThrowAdviceDemo3 beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<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-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

<bean id="logAdvice"

class="onlyfun.caterpillar.LogAspect"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<aop:aspectj-autoproxy/>

</beans>

Page 82: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�82

由於這邊將所有的 Advice 撰寫在 LogAspect 類別中,所以事實上beans-config.xml 與先前 AfterAdviceDemo3 專案中是相同的設定,這邊再次列出只是為了強調。執行的結果則與 ThrowAdviceDemo2 專案是相同的。

4.6.8 Around Advice:基於 XML Schema

Spring 2.0中要定義 Around Advice,不用實作 MethodInterceptor介面,Advice 的方法名稱可以任意,但必須傳回物件,必須定義一個接受ProceedingJointPoint實例的參數,例如:

public Object invoke(ProceedingJoinPoint jointPoint) throws Throwable {

...

} 就 如 4.2.3 Around Advice 中 所 介 紹 的 MethodInvocation ,ProceedingJointPoint也有一個 proceed(),在真正呼叫 proceed()方法之前,目標物件的方法不會被執行,而 proceed()方法執行完畢後傳回一個物件,表示目標物件的方法執行完畢後的傳回值,在 Around Advice方法執行完畢後可以傳回這個物件,甚至傳回另一個物件。 以下將先前 AfterAdviceDemo2 專案的內容,修改為使用 Around

Advice,首先是 LogAspect類別:

AroundAdviceDemo2 LogAspect.java

package onlyfun.caterpillar;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.aspectj.lang.ProceedingJoinPoint;

Page 83: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�83

public class LogAspect {

private Logger logger =

Logger.getLogger(this.getClass().getName());

public Object invoke(

ProceedingJoinPoint jointPoint) throws Throwable {

logger.log(Level.INFO, "method starts..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

Object retVal = jointPoint.proceed();

logger.log(Level.INFO, "method ends..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

return retVal;

}

}

beans-config.xml修改如下:

AroundAdviceDemo2 beans-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<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-2.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

<bean id="logAspect"

class="onlyfun.caterpillar.LogAspect"/>

<bean id="helloSpeaker"

class="onlyfun.caterpillar.HelloSpeaker"/>

<aop:config>

<aop:aspect id="logging" ref="logAspect">

<aop:around pointcut=

Page 84: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�84

"execution(* onlyfun.caterpillar.IHello.*(..))"

method="invoke"/>

</aop:aspect>

</aop:config>

</beans> 執行結果與AfterAdviceDemo2專案是相同的,請參考先前AfterAdvice-

Demo2專案的執行畫面。

4.6.9 Around Advice:基於 Annotation

Around Advice基於 Annotation的設定方式,則要使用@Around,例如可以直接修改 AfterAdviceDemo3專案的 LogAspect如下:

AroundAdviceDemo3 LogAspect.java

package onlyfun.caterpillar;

import java.util.logging.Level;

import java.util.logging.Logger;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

@Aspect

public class LogAspect {

private Logger logger =

Logger.getLogger(this.getClass().getName());

@Around("execution(* onlyfun.caterpillar.IHello.*(..))")

public Object invoke(

ProceedingJoinPoint jointPoint) throws Throwable {

logger.log(Level.INFO, "method starts..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

Object retVal = jointPoint.proceed();

Page 85: Spring 2.0 技術手冊第四章 - Spring AOP

Chapter 4 Spring AOP

4�8�

logger.log(Level.INFO, "method ends..." +

jointPoint.getSignature().getDeclaringTypeName() +

"." + jointPoint.getSignature().getName());

return retVal;

}

} 在 Spring進入 2.0版本之後,於 AOP這方面所增加的功能也是相當的多,由於書籍篇幅有限,無法一一為您作介紹,建議可以查閱 Spring參考文件,以獲得更多進階內容的說明。

Page 86: Spring 2.0 技術手冊第四章 - Spring AOP

Spring 2.0 技術手冊(林信良 – http://openhome.cc)

4�86

4.7 接下來的主題

Spring中所有子框架基礎的就是 Spring的 IoC容器與 Spring的 AOP支援,在第 3章與第 4章中已經介紹完畢,接下來就可以開始探索 Spring中所提供的子框架或 API,首先要來認識的是 Spring在資料庫存取方面的支援,包括了對 JDBC的封裝、交易管理等議題。

圖 4.15 Spring AOP 主要的原理就是代理