在進行AOP(Aspect-Oriented Programming)設計時,使用Spring AOP是個快捷的方案,它隱藏了底層原理,可使用標註來設計Aspect中各種Advice。不過,若能認識底層的實現機制,對於是否採用AOP框架,或者是只需某個底層技術,在決策考量上,也會有所幫助。

Java動態代理

想要認識AOP的基本概念,先前專欄〈從攔截過濾器到AOP〉曾經談過,AOP最重要的是辨識出與主要商務流程橫切的服務,因此就算是Servlet中的過濾器服務,廣義來說,也算是支援AOP技術的實現,當然,Servlet的過濾器是攔截過濾器(Intercepting Filter)模式之實現,粒度大了許多,因為服務的接入點,是在Servlet處理請求前後,如果想要橫切的點,是某個方法執行的前後呢?

修改元件的原始碼,在每個方法開頭與結尾都加入日誌程式碼,當然是不切實際的,一個簡單的想法是使用介面定義元件行為,目標物件必須實作該介面,並且有個代理(Proxy)物件也實作相同介面,代理物件會包裹目標物件,並在每個方法呼叫前後加入服務,如此就能在不修改目標物件原始碼的情況下,達成任務。

面對簡單的需求,其實這麼實作也無妨,不過若經常有這類需求,在這種靜態代理方式下,特定代理物件會是專用於某個目標物件,而且如果要代理的方法很多,每個方法都要實作,也是個麻煩。

Java原生的反射API提供動態代理相關類別,可以不必為特定介面實作特定的代理物件,使用動態代理機制,服務元件必須實作InvocationHandler介面,包裹目標物件並在invoke方法中實作服務,接著就可以使用Proxy.newProxyInstance方法建立代理物件,呼叫時必須指定類別載入器,告知要代理的介面,以及介面上定義方法被呼叫時的處理者(範例可參考〈動態代理(https://goo.gl/23HfMJ)〉)。

有些語言支援Mixin、物件個體化、開放類別機制等,然而對Java來說,一旦類別定義完成,就語法上來說,就沒有方式可以動態地增加或修補行為,對類別的實例也是如此,想要達到看似增添行為之目的,概念上,也是透過實作代理物件,同樣地,簡單的需求透過靜態代理就足夠了,如果想要設計的代理物件具備較高的通用性,也可以使用Java動態代理來解決,只不過要新增的行為必須定義為介面,而處理器除了實作InvocationHandler介面,也要實作新行為的介面(範例可參考〈動態代理(https://goo.gl/23HfMJ)〉)。

cglib與AOP Alliance

Java本身的動態代理,必須要基於介面定義,若類別並沒有實作特定介面,就無法使用Java動態代理機制,這時可以透過cglib(Code Generation Library),它的底層基於 ASM,可於執行時期修改位元組碼來生成代理物件(而不是透過反射API);在實作上,服務物件會設計為一個攔截器,實現MethodInterceptor介面,接著透過Enhancer實例設定父類、攔截器實例,然後使用Enhancer的create來動態建立代理物件(範例可參考〈增添行為(https://goo.gl/miYfxw)〉)。

在進行動態代理時,Spring底層預設會採用Java動態代理,若目標對象沒有實作介面則改用cglib,為了封裝細節,Spring提供了自己的BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等介面。

然而實際上,AOP是個概念,各廠商會有各自的實現,為了有一致的行為,AOP Alliance定義了一套介面標準,例如,MethodInterceptor在AOP Alliance中的行為,是Object invoke(MethodInvocation methodInvocation) throws Throwable,早期在Spring AOP中實作Around Advice時,可以實作AOP Alliance定義的MethodInterceptor,該Advice就可以使用於遵守AOP Alliance規範的其他AOP框架。

靜態時期織入的AspectJ

基於Java動態代理或者是cglib,是在執行時期動態生成目標物件之子類作為代理類別,也就是在執行時期將關切點織入(Weaving)主要流程,不過,也有一些是在載入時期或編譯時期織入的技術。

而AspectJ就是支援編譯時期織入的方案,它對Java程式語言做了些擴充,可以透過aspect、before等語法來定義橫切入主要流程的關切點,例如定義一個aspect,其中包含了before advice:

public aspect LoggingAspect {
before() : execution(* cc.openhome.model.Hello.*(..)) {
Object target = thisJoinPoint.getTarget();
// 略...
}
}

使用AspectJ設計Aspect元件時,可以使用aspect,不用實作介面或繼承類別(副檔名.java或.aj);橫切進主要流程的服務在AspectJ中稱為Advice,若希望某方法被呼叫前執行,可以使用before來定義,表示這是個Before Advice,Advice會在程式執行時的某些點上接入,這些點稱為Join Point,用來定義是否符合Join Point的斷言稱為Pointcut。

execution(* cc.openhome.model.Hello.*(..))就是用來定義是否符合Join Point的斷言,它表示的是,會在Hello的任何方法執行時進行日誌,第一個*,表示任何傳回型態,第二個*,則表示所有方法,而..表示的是任何引數。

如果你先前曾經接觸過Spring AOP,對於Aspect、Advice、Join Point、Pointcut等名詞,應不陌生,AspectJ中的觀念與設計,也影響了不少AOP框架的實現,例如,Spring AOP就是其中之一。

在上面看到的aspect、before、execution,甚至是thisJointPoint等關鍵字,是AspectJ對Java語言的擴充,因而不能直接使用javac來編譯,而必須透過AspectJ的ajc編譯出.class檔案,目標類別會在靜態時期就織入Advice的位元組碼。

在原生的Java語言上,現在也可以使用AspectJ的標註來進行相關設計,像是上例,可以修改如下,並透過ajc編譯(可參考〈關於AspectJ〉文件(https://goo.gl/nfwfPi)):

@Aspect
public class LoggingAspect {
@Before("execution(* cc.openhome.model.Hello.*(..))")
public void before(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
// 略
}
}

Spring AOP

過去,如果你曾在Spring AOP當中,使用過標註(Annotation)來設計Aspect等元件,現在,對於上面的@Aspect等設定方式,應該不會陌生,這是因為,早期Spring AOP要實作某介面,並以XML定義的方式,太過麻煩,後來從AspectJ吸收了不少經驗,並努力維持與AspectJ的一致性。雖說如此,Spring AOP的文件指出,它所支援的做法,仍只是執行時期動態代理。

對於接觸AOP的開發者來說,AOP充滿了各式術語,不過重點仍在關切點分離,只不過這關切點是與主要流程橫切,在簡單的需求下,採用靜態代理就可以簡單解決的話,也不失為一個好的方案,在事情變得複雜之時,從動態代理到Spring AOP的這個過程,有助於適時地從中採用符合需求的技術,而不用一開始就為了AOP而AOP!

作者簡介


Advertisement

更多 iThome相關內容