天天看點

Schema-based AOP support

如果你無法使用Java 5,或者你比較喜歡使用XML格式,Spring2.0也提供了使用新的"aop"命名空間來定義一個切面。 和使用@AspectJ風格完全一樣,切入點表達式和通知類型同樣得到了支援,是以在這一節中我們将着重介紹新的 文法 和回顧前面我們所讨論的如何寫一個切入點表達式和通知參數的綁定(Section 6.2, “@AspectJ支援”)。

使用本章所介紹的aop命名空間标簽(aop namespace tag),你需要引入Appendix A, XML Schema-based configuration中提及的spring-aop schema。 參見Section A.2.6, “The aop schema”。

在Spring的配置檔案中,所有的切面和通知器都必須定義在 <aop:config> 元素内部。 一個application context可以包含多個 <aop:config>。 一個 <aop:config> 可以包含pointcut,advisor和aspect元素(注意它們必須按照這樣的順序進行聲明)。

Schema-based AOP support
Warning
<aop:config>風格的配置使得對Spring auto-proxying 機制的使用變得很笨重。如果你已經通過BeanNameAutoProxyCreator或類似的東西使用顯式的auto-proxying将會引發問題 (例如通知沒有被織入)。推薦的使用模式是隻使用<aop:config>風格或隻使用 AutoProxyCreator風格

6.3.1. 聲明一個切面

有了schema的支援,切面就和正常的Java對象一樣被定義成application context中的一個bean。 對象的字段和方法提供了狀态和行為資訊,XML檔案則提供了切入點和通知資訊。

切面使用<aop:aspect>來聲明,backing bean(支援bean)通過 ref 屬性來引用:

<aop:config>  <aop:aspect id="myAspect" ref="aBean">...  </aop:aspect></aop:config><bean id="aBean" class="...">  ...</bean>      

切面的支援bean(上例中的"aBean")可以象其他Spring bean一樣被容器管理配置以及依賴注入。

6.3.2. 聲明一個切入點

切入點可以在切面裡面聲明,這種情況下切入點隻在切面内部可見。切入點也可以直接在<aop:config>下定義,這樣就可以使多個切面和通知器共享該切入點。

一個描述service層中表示所有service執行的切入點可以如下定義:

<aop:config>  <aop:pointcut id="businessService"expression="execution(* com.xyz.myapp.service.*.*(..))"/></aop:config>      

注意切入點表達式本身使用了 Section 6.2, “@AspectJ支援” 中描述的AspectJ 切入點表達式語言。 如果你在Java 5環境下使用基于schema的聲明風格,可參考切入點表達式類型中定義的命名式切入點,不過這在JDK1.4及以下版本中是不被支援的(因為依賴于Java 5中的AspectJ反射API)。 是以在JDK 1.5中,上面的切入點的另外一種定義形式如下:

<aop:config>  <aop:pointcut id="businessService"expression="com.xyz.myapp.SystemArchitecture.businessService()"/></aop:config>      

假定你有 Section 6.2.3.3, “共享常見的切入點(pointcut)定義”中說描述的 SystemArchitecture 切面。

在切面裡面聲明一個切入點和聲明一個頂級的切入點非常類似:

<aop:config>  <aop:aspect id="myAspect" ref="aBean"><aop:pointcut id="businessService"  expression="execution(* com.xyz.myapp.service.*.*(..))"/>...  </aop:aspect></aop:config>      

當需要連接配接子表達式的時候,'&'在XML中用起來非常不友善,是以關鍵字'and', 'or' 和 'not'可以分别用來代替'&', '||' 和 '!'。

注意這種方式定義的切入點通過XML id來查找,并且不能定義切入點參數。在基于schema的定義風格中命名切入點支援較之@AspectJ風格受到了很多的限制。

6.3.3. 聲明通知

和@AspectJ風格一樣,基于schema的風格也支援5種通知類型并且兩者具有同樣的語義。

6.3.3.1. 通知(Advice)

Before通知在比對方法執行前進入。在<aop:aspect>裡面使用<aop:before>元素進行聲明。

<aop:aspect id="beforeExample" ref="aBean"><aop:before  pointcut-ref="dataAccessOperation"  method="doAccessCheck"/>...</aop:aspect>      

這裡 dataAccessOperation 是一個頂級(<aop:config>)切入點的id。 要定義内置切入點,可将 pointcut-ref 屬性替換為 pointcut 屬性:

<aop:aspect id="beforeExample" ref="aBean"><aop:before  pointcut="execution(* com.xyz.myapp.dao.*.*(..))"  method="doAccessCheck"/>...</aop:aspect>      

我們已經在@AspectJ風格章節中讨論過了,使用命名切入點能夠明顯的提高代碼的可讀性。

Method屬性辨別了提供了通知的主體的方法(doAccessCheck)。這個方法必須定義在包含通知的切面元素所引用的bean中。 在一個資料通路操作執行之前(執行連接配接點和切入點表達式比對),切面中的"doAccessCheck"會被調用。

6.3.3.2. 傳回後通知(After returning advice)

After returning通知在比對的方法完全執行後運作。和Before通知一樣,可以在<aop:aspect>裡面聲明。例如:

<aop:aspect id="afterReturningExample" ref="aBean"><aop:after-returning  pointcut-ref="dataAccessOperation"  method="doAccessCheck"/>...</aop:aspect>      

和@AspectJ風格一樣,通知主體可以接收傳回值。使用returning屬性來指定接收傳回值的參數名:

<aop:aspect id="afterReturningExample" ref="aBean"><aop:after-returning  pointcut-ref="dataAccessOperation"  returning="retVal"  method="doAccessCheck"/>...</aop:aspect>      

doAccessCheck方法必須聲明一個名字叫 retVal 的參數。 參數的類型強制比對,和先前我們在@AfterReturning中講到的一樣。例如,方法簽名可以這樣聲明:

public void doAccessCheck(Object retVal) {...      

6.3.3.3. 抛出異常後通知(After throwing advice)

After throwing通知在比對方法抛出異常退出時執行。在 <aop:aspect> 中使用after-throwing元素來聲明:

<aop:aspect id="afterThrowingExample" ref="aBean"><aop:after-throwing  pointcut-ref="dataAccessOperation"  method="doRecoveryActions"/>...</aop:aspect>      

和@AspectJ風格一樣,可以從通知體中擷取抛出的異常。 使用throwing屬性來指定異常的名稱,用這個名稱來擷取異常:

<aop:aspect id="afterThrowingExample" ref="aBean"><aop:after-throwing  pointcut-ref="dataAccessOperation"  thowing="dataAccessEx"  method="doRecoveryActions"/>...</aop:aspect>      

doRecoveryActions方法必須聲明一個名字為 dataAccessEx 的參數。 參數的類型強制比對,和先前我們在@AfterThrowing中講到的一樣。例如:方法簽名可以如下這般聲明:

public void doRecoveryActions(DataAccessException dataAccessEx) {...      

6.3.3.4. 後通知(After (finally) advice)

After (finally)通知在比對方法退出後執行。使用 after 元素來聲明:

<aop:aspect id="afterFinallyExample" ref="aBean"><aop:after  pointcut-ref="dataAccessOperation"  method="doReleaseLock"/>...</aop:aspect>      

6.3.3.5. 通知

Around通知是最後一種通知類型。Around通知在比對方法運作期的“周圍”執行。 它有機會在目标方法的前面和後面執行,并決定什麼時候運作,怎麼運作,甚至是否運作。 Around通知經常在需要在一個方法執行前或後共享狀态資訊,并且是線程安全的情況下使用(啟動和停止一個計時器就是一個例子)。 注意選擇能滿足你需求的最簡單的通知類型(i.e.如果簡單的before通知就能做的事情絕對不要使用around通知)。

Around通知使用 aop:around 元素來聲明。 通知方法的第一個參數的類型必須是 ProceedingJoinPoint 類型。 在通知的主體中,調用 ProceedingJoinPoint的proceed() 方法來執行真正的方法。 proceed 方法也可能會被調用并且傳入一個 Object[] 對象 - 該數組将作為方法執行時候的參數。 參見 Section 6.2.4.5, “環繞通知(Around Advice)” 中提到的一些注意點。

<aop:aspect id="aroundExample" ref="aBean"><aop:around  pointcut-ref="businessService"  method="doBasicProfiling"/>...</aop:aspect>      

doBasicProfiling 通知的實作和@AspectJ中的例子完全一樣(當然要去掉注解):

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {    // start stopwatch    Object retVal = pjp.proceed();    // stop stopwatch    return retVal;}      

6.3.3.6. 通知參數

Schema-based聲明風格和@AspectJ支援一樣,支援通知的全名形式 - 通過通知方法參數名字來比對切入點參數。 參見 Section 6.2.4.6, “通知參數(Advice parameters)” 擷取詳細資訊。

如果你希望顯式指定通知方法的參數名(而不是依靠先前提及的偵測政策),可以通過 arg-names 屬性來實作。示例如下:

<aop:before  pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"  method="audit"  arg-names="auditable"/>      

The arg-names attribute accepts a comma-delimited list of parameter names.

arg-names屬性接受由逗号分割的參數名清單。

請看下面這個基于XSD風格的更複雜一些的執行個體,它展示了關聯多個強類型參數的環繞通知的使用。

首先,服務接口及它的實作将被通知:

package x.y.service;public interface FooService {   Foo getFoo(String fooName, int age);}// the attendant implementation (defined in another file of course)public class DefaultFooService implements FooService {   public Foo getFoo(String name, int age) {      return new Foo(name, age);   }}      

下一步(無可否認的)是切面。注意實際上profile(..)方法 接受多個強類型(strongly-typed)參數,第一個參數是方法調用時要執行的連接配接點,該參數指明了 profile(..)方法被用作一個環繞通知:

package x.y;import org.aspectj.lang.ProceedingJoinPoint;import org.springframework.util.StopWatch;public class SimpleProfiler {   public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {      StopWatch clock = new StopWatch(            "Profiling for '" + name + "' and '" + age + "'");      try {         clock.start(call.toShortString());         return call.proceed();      } finally {         clock.stop();         System.out.println(clock.prettyPrint());      }   }}      

最後,下面是為一個特定的連接配接點執行上面的通知所必需的XML配置:

<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.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">   <!-- this is the object that will be proxied by Spring's AOP infrastructure -->   <bean id="fooService" class="x.y.service.DefaultFooService"/>   <!-- this is the actual advice itself -->   <bean id="profiler" class="x.y.SimpleProfiler"/>   <aop:config>      <aop:aspect ref="profiler">         <aop:pointcut id="theExecutionOfSomeFooServiceMethod"                    expression="execution(* x.y.service.FooService.getFoo(String,int))                    and args(name, age)"/>         <aop:around pointcut-ref="theExecutionOfSomeFooServiceMethod"                  method="profile"/>      </aop:aspect>   </aop:config></beans>      

如果使用下面的驅動腳本,我們将在标準輸出上得到如下的輸出:

import org.springframework.beans.factory.BeanFactory;import org.springframework.context.support.ClassPathXmlApplicationContext;import x.y.service.FooService;public final class Boot {   public static void main(final String[] args) throws Exception {      BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");      FooService foo = (FooService) ctx.getBean("fooService");      foo.getFoo("Pengo", 12);   }}      
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0-----------------------------------------ms     %     Task name-----------------------------------------00000  ?  execution(getFoo)      

6.3.3.7. 通知順序

當同一個切入點(執行方法)上有多個通知需要執行時,執行順序規則在 Section 6.2.4.7, “通知(Advice)順序” 已經提及了。 切面的優先級通過切面的支援bean是否實作了Ordered接口來決定。

6.3.4. 引入

Intrduction (在AspectJ中成為inter-type聲明)允許一個切面聲明一個通知對象實作指定接口,并且提供了一個接口實作類來代表這些對象。

在 aop:aspect 内部使用 aop:declare-parents 元素定義Introduction。 該元素用于用來聲明所比對的類型有了一個新的父類型(是以有了這個名字)。 例如,給定接口 UsageTracked,以及這個接口的一個實作類 DefaultUsageTracked, 下面聲明的切面所有實作service接口的類同時實作 UsageTracked 接口。(比如為了通過JMX暴露statistics。)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">  <aop:declare-parents  types-matching="com.xzy.myapp.service.*+",  implement-inter  default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>  <aop:beforepointcut="com.xyz.myapp.SystemArchitecture.businessService()  and this(usageTracked)"method="recordUsage"/></aop:aspect>      

usageTracking bean的支援類可以包含下面的方法:

public void recordUsage(UsageTracked usageTracked) {    usageTracked.incrementUseCount();}      

欲實作的接口由 implement-interface 屬性來指定。 types-matching 屬性的值是一個AspectJ類型模式:- 任何比對類型的bean會實作 UsageTracked 接口。 注意在Before通知的例子中,srevice bean可以用作 UsageTracked 接口的實作。 如果程式設計形式通路一個bean,你可以這樣來寫:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");      

6.3.5. 切面執行個體化模型

Schema-defined切面僅支援一種執行個體化模型就是singlton模型。其他的執行個體化模型或許在未來版本中将得到支援。

6.3.6. Advisors

"advisors"這個概念來自Spring1.2對AOP的支援,在AspectJ中是沒有等價的概念。 advisor就像一個小的自包含的切面,這個切面隻有一個通知。 切面自身通過一個bean表示,并且必須實作一個通知接口, 在 Section 7.3.2, “Spring裡的通知類型” 中我們會讨論相應的接口。Advisors可以很好的利用AspectJ切入點表達式。

Spring 2.0 通過 <aop:advisor> 元素來支援advisor 概念。 你将會發現它大多數情況下會和transactional advice一起使用,transactional advice在Spring 2.0中有自己的命名空間。格式如下:

<aop:config>  <aop:pointcut id="businessService"expression="execution(* com.xyz.myapp.service.*.*(..))"/>  <aop:advisor  pointcut-ref="businessService"  advice-ref="tx-advice"/></aop:config><tx:advice id="tx-advice"><tx:attributes><tx:method name="*" propagation="REQUIRED"/>  </tx:attributes></tx:advice>      

和在上面使用的 pointcut-ref 屬性一樣,你還可以使用 pointcut 屬性來定義一個内聯的切入點表達式。

為了定義一個advisord的優先級以便讓通知可以有序,使用 order 屬性來定義 advisor的值 Ordered 。

6.3.7. 例子

讓我們來看看在 Section 6.2.7, “例子” 提過并發鎖失敗重試的例子,如果使用schema對這個例子進行重寫是什麼效果。

因為并發鎖的關系,有時候business services可能會失敗(例如,死鎖失敗)。 如果重新嘗試一下,很有可能就會成功。對于business services來說,重試幾次是很正常的(Idempotent操作不需要使用者參與,否則會得出沖突的結論) 我們可能需要透明的重試操作以避免讓客戶看見 PessimisticLockingFailureException 例外被抛出。 很明顯,在一個橫切多層的情況下,這是非常有必要的,是以通過切面來實作是很理想的。

因為我們想要重試操作,我們會需要使用到環繞通知,這樣我們就可以多次調用proceed()方法。 下面是簡單的切面實作(隻是一個schema支援的普通Java 類):

public class ConcurrentOperationExecutor implements Ordered {      private static final int DEFAULT_MAX_RETRIES = 2;   private int maxRetries = DEFAULT_MAX_RETRIES;   private int order = 1;   public void setMaxRetries(int maxRetries) {      this.maxRetries = maxRetries;   }      public int getOrder() {      return this.order;   }      public void setOrder(int order) {      this.order = order;   }      public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {       int numAttempts = 0;      PessimisticLockingFailureException lockFailureException;      do {         numAttempts++;         try {             return pjp.proceed();         }         catch(PessimisticLockingFailureException ex) {            lockFailureException = ex;         }      }      while(numAttempts <= this.maxRetries);      throw lockFailureException;   }}      

請注意切面實作了 Ordered 接口,這樣我們就可以把切面的優先級設定為高于事務通知(我們每次重試的時候都想要在一個全新的事務中進行)。 maxRetries 和 order 屬性都可以在Spring中配置。 主要的動作在 doConcurrentOperation 這個環繞通知中發生。 請注意這個時候我們所有的 businessService() 方法都會使用這個重試政策。 我們首先會嘗試處理,然後如果我們得到一個 PessimisticLockingFailureException 異常,我們隻需要簡單的重試,直到我們耗盡所有預設的重試次數。

這個類跟我們在@AspectJ的例子中使用的是相同的,隻是沒有使用注解。

對應的Spring配置如下:

<aop:config>  <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">    <aop:pointcut id="idempotentOperation"        expression="execution(* com.xyz.myapp.service.*.*(..))"/>           <aop:around       pointcut-ref="idempotentOperation"       method="doConcurrentOperation"/>    </aop:aspect></aop:config><bean id="concurrentOperationExecutor"  class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">     <property name="maxRetries" value="3"/>     <property name="order" value="100"/>  </bean>      

請注意我們現在假設所有的bussiness services都是idempotent。如果不是這樣,我們可以改寫切面,加上 Idempotent 注解,讓它隻調用idempotent:

@Retention(RetentionPolicy.RUNTIME)public @interface Idempotent {  // marker annotation}      

并且對service操作的實作進行注解。這樣如果你隻希望改變切面使得idempotent的操作會嘗試多次,你隻需要改寫切入點表達式,這樣隻有 @Idempotent 操作會比對:

<aop:pointcut id="idempotentOperation"expression="execution(* com.xyz.myapp.service.*.*(..)) [email protected](com.xyz.myapp.service.Idempotent)"/>      

6.4. AOP聲明風格的選擇

當你确定切面是實作一個給定需求的最佳方法時,你如何選擇是使用Spring AOP還是AspectJ,以及選擇 Aspect語言(代碼)風格、@AspectJ聲明風格或XML風格?這個決定會受到多個因素的影響,包括應用的需求、 開發工具和小組對AOP的精通程度。

6.4.1. Spring AOP還是完全用AspectJ?

做能起作用的最簡單的事。Spring AOP比完全使用AspectJ更加簡單,因為它不需要引入AspectJ的編譯器/織入器到你開發和建構過程中。 如果你僅僅需要在Spring bean上通知執行操作,那麼Spring AOP是合适的選擇。如果你需要通知domain對象或其它沒有在Spring容器中 管理的任意對象,那麼你需要使用AspectJ。如果你想通知除了簡單的方法執行之外的連接配接點(如:調用連接配接點、字段get或set的連接配接點等等), 也需要使用AspectJ。

當使用AspectJ時,你可以選擇使用AspectJ語言(也稱為“代碼風格”)或@AspectJ注解風格。 如果切面在你的設計中扮演一個很大的角色,并且你能在Eclipse中使用AspectJ Development Tools (AJDT), 那麼首選AspectJ語言 :- 因為該語言專門被設計用來編寫切面,是以會更清晰、更簡單。如果你沒有使用 Eclipse,或者在你的應用中隻有很少的切面并沒有作為一個主要的角色,你或許應該考慮使用@AspectJ風格 并在你的IDE中附加一個普通的Java編輯器,并且在你的建構腳本中增加切面織入(連結)的段落。

6.4.2. Spring AOP中使用@AspectJ還是XML?

如果你選擇使用Spring AOP,那麼你可以選擇@AspectJ或者XML風格。總的來說,如果你使用Java 5, 我們建議使用@AspectJ風格。顯然如果你不是運作在Java 5上,XML風格是最佳選擇。XML和@AspectJ 之間權衡的細節将在下面進行讨論。

XML風格對現有的Spring使用者來說更加習慣。它可以使用在任何Java級别中(參考連接配接點表達式内部的命名連接配接點,雖然它也需要Java 5) 并且通過純粹的POJO來支援。當使用AOP作為工具來配置企業服務時(一個好的例子是當你認為連接配接點表達式是你的配置中的一部分時, 你可能想單獨更改它)XML會是一個很好的選擇。對于XML風格,從你的配置中可以清晰的表明在系統中存在那些切面。

XML風格有兩個缺點。第一是它不能完全将需求實作的地方封裝到一個位置。DRY原則中說系統中的每一項知識都必須具有單一、無歧義、權威的表示。 當使用XML風格時,如何實作一個需求的知識被分割到支撐類的聲明中以及XML配置檔案中。當使用@AspectJ風格時就隻有一個單獨的子產品 -切面- 資訊被封裝了起來。 第二是XML風格同@AspectJ風格所能表達的内容相比有更多的限制:僅僅支援"singleton"切面執行個體模型,并且不能在XML中組合命名連接配接點的聲明。 例如,在@AspectJ風格中我們可以編寫如下的内容:

@Pointcut(execution(* get*()))  public void propertyAccess() {}  @Pointcut(execution(org.xyz.Account+ *(..))  public void operationReturningAnAccount() {}  @Pointcut(propertyAccess() && operationReturningAnAccount())  public void accountPropertyAccess() {}      

在XML風格中能聲明開頭的兩個連接配接點:

<aop:pointcut id="propertyAccess"       expression="execution(* get*())"/>  <aop:pointcut id="operationReturningAnAccount"       expression="execution(org.xyz.Account+ *(..))"/>      

但是不能通過組合這些來定義accountPropertyAccess連接配接點

@AspectJ風格支援其它的執行個體模型以及更豐富的連接配接點組合。它具有将将切面保持為一個子產品單元的優點。 還有一個優點就是@AspectJ切面能被Spring AOP和AspectJ兩者都了解 - 是以如果稍後你認為你需要AspectJ 的能力去實作附加的需求,那麼你非常容易轉移到基于AspectJ的途徑。總而言之,我們更喜歡@AspectJ風格隻要你有切面 去做超出簡單的“配置”企業服務之外的事情。

6.5. 混合切面類型

我們完全可以混合使用以下幾種風格的切面定義:使用自動代理的@AspectJ 風格的切面,schema-defined <aop:aspect> 的切面,和用 <aop:advisor> 聲明的advisor,甚至是使用Spring 1.2風格的代理和攔截器。 由于以上幾種風格的切面定義的都使用了相同的底層機制,是以可以很好的共存。

6.6. 代理機制

Spring AOP部分使用JDK動态代理或者CGLIB來為目标對象建立代理。(建議盡量使用JDK的動态代理)

如果被代理的目标對象實作了至少一個接口,則會使用JDK動态代理。所有該目标類型實作的接口都将被代理。若該目标對象沒有實作任何接口,則建立一個CGLIB代理。

如果你希望強制使用CGLIB代理,(例如:希望代理目标對象的所有方法,而不隻是實作自接口的方法)那也可以。但是需要考慮以下問題:

  • 無法通知(advise)Final 方法,因為他們不能被覆寫。
  • 你需要将CGLIB 2二進制發行包放在classpath下面,與之相較JDK本身就提供了動态代理

強制使用CGLIB代理需要将 <aop:config> 的 proxy-target-class 屬性設為true:

<aop:config proxy-target-class="true">...</aop:config>      

當需要使用CGLIB代理和@AspectJ自動代理支援,請按照如下的方式設定 <aop:aspectj-autoproxy> 的 proxy-target-class 屬性:

<aop:aspectj-autoproxy proxy-target-class="true"/>