1. 程式人生 > 實用技巧 >Spring 面向切面的程式設計

Spring 面向切面的程式設計

5.使用 Spring 進行面向方面的程式設計

面向方面的程式設計(AOP)通過提供另一種思考程式結構的方式來補充面向物件的程式設計(OOP)。 OOP 中模組化的關鍵單元是類,而在 AOP 中模組化是方面。可以跨越多種型別和物件。

Spring 2.0 引入了一種使用schema-based approach@AspectJ 註解樣式來編寫自定義切面的更簡單,更強大的方法。這兩種樣式都提供了完全型別化的建議,並使用了 AspectJ 切入點語言,同時仍使用 Spring AOP 進行編織。

本章討論基於 Spring 2.0 模式和基於@AspectJ 的 AOP 支援。

AOP 在 Spring Framework 中用於:

  • 提供宣告性企業服務,尤其是作為 EJB 宣告性服務的替代品。最重要的此類服務是宣告式 TransactionManagement

  • 讓使用者實現自定義方面,以 AOP 補充其對 OOP 的使用。

5.1. AOP 概念

  • 切面(aspect):通知+切入點。

  • 通知(advice):除了目標方法執行之外的操作都稱為通知。比如:事務通知,記錄目標方法執行時長的通知。通知一般由開發者開發。

  • 切入點(pointcut):指定專案中的哪些類中的哪些方法應用通知,切入點是配置得到的。

  • 連線點(Join point):例如:servlet中的longin()就是連線點;所以連線點在spring中它永遠是一個方法。也可以說'目標物件中的方法就是一個連線點‘。
  • 目標物件(Target object):原始物件。

  • 代理物件(AOP proxy): 包含了原始物件的程式碼和增強後的程式碼的那個物件。

應用場景:日誌記錄,許可權驗證,效率檢查,事務管理......

Spring AOP 包括以下型別的建議:

  • 在建議之前:在連線點之前執行的建議,但是它不能阻止執行流程前進到連線點(除非它引發異常)。

  • 返回建議後:在連線點正常完成後要執行的建議(例如,如果方法返回而沒有引發異常)。

  • 丟擲建議後:如果方法因丟擲異常而退出,則執行建議。

  • 建議之後(最終):無論連線點退出的方式如何(正常或特殊返回),均應執行建議。

  • 圍繞建議:圍繞連線點的建議,例如方法呼叫。這是最有力的建議。周圍建議可以在方法呼叫之前和之後執行自定義行為。它還負責選擇是返回連線點還是通過返回其自身的返回值或引發異常來捷徑建議的方法執行。

圍繞建議是最通用的建議。由於 Spring AOP 與 AspectJ 一樣,提供了各種建議型別,因此我們建議您使用功能最弱的建議型別,以實現所需的行為。例如,如果您只需要使用方法的返回值更新快取,則最好使用返回後的建議而不是周圍的建議,儘管周圍的建議可以完成相同的事情。使用最具體的建議型別可以提供更簡單的程式設計模型,並減少出錯的可能性。

在 Spring 2.0 中,所有建議引數都是靜態型別的,因此您可以使用適當型別(例如,從方法執行返回值的型別)而不是Object陣列的建議引數。

切入點匹配的連線點的概念是 AOP 的關鍵,它與僅提供攔截功能的舊技術有所不同。切入點使建議的目標獨立於面向物件的層次結構。例如,您可以將提供宣告性事務 Management 的環繞建議應用於跨越多個物件(例如服務層中的所有業務操作)的一組方法。

5.2. SpringAOP 能力和目標

Spring AOP 是用純 Java 實現的。不需要特殊的編譯過程。 Spring AOP 不需要控制類載入器的層次結構,因此適合在 Servlet 容器或應用程式伺服器中使用。

5.3. AOP 代理

Spring AOP 預設將標準 JDK 動態代理用於 AOP 代理。這使得可以代理任何介面(或一組介面)。

Spring AOP 也可以使用 CGLIB 代理。這對於代理類是必需的。預設情況下,如果業務物件未實現介面,則使用 CGLIB。由於對介面進行程式設計是一種好習慣,因此業務類通常實現一個或多個業務介面。在某些情況下(可能極少發生),您需要通知未在介面上宣告的方法,或者需要將代理物件作為具體型別傳遞給方法,則可以使用強制使用 CGLIB

5.4. @AspectJ 支援

@AspectJ 是一種將切面宣告為帶有註解的常規 Java 類的樣式。 @AspectJ 樣式是AspectJ project作為 AspectJ 5 版本的一部分引入的。 Spring 使用 AspectJ 提供的用於切入點解析和匹配的庫來解釋與 AspectJ 5 相同的 註解。但是,AOP 執行時仍然是純 Spring AOP,並且不依賴於 AspectJ 編譯器或編織器。

5.4.1. 啟用@AspectJ 支援

要在Spring配置中使用@AspectJ切面,您需要啟用Spring支援,以便基於@AspectJ切面配置Spring AOP,並基於這些切面是否通知自動代理beans。我們的意思是,通過自動代理,如果 Spring 確定一個或多個切面通知一個 bean,它會自動為該 bean 生成一個代理來攔截方法調,用並確保按需執行通知。

可以使用 XML 或 Java 樣式的配置來啟用@AspectJ 支援。無論哪種情況,都需要確保 AspectJ 的aspectjweaver.jar庫位於應用程式的 Classpath(版本 1.8 或更高版本)上。該庫在 AspectJ 發行版的lib目錄中或從 Maven Central 儲存庫中可用。

通過 Java 配置啟用@AspectJ 支援

要使用 Java@Configuration啟用@AspectJ 支援,請新增@EnableAspectJAutoProxy註解,如以下示例所示:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
通過 XML 配置啟用@AspectJ 支援

要通過基於 XML 的配置啟用@AspectJ 支援,請使用aop:aspectj-autoproxy元素,如以下示例所示:  

<aop:aspectj-autoproxy/>

5.4.2. 宣告一個切面

啟用@AspectJ 支援後,Spring 會自動檢測到在應用程式上下文中使用@AspectJ 方面(具有@Aspect註解)的類定義的任何 bean,並用於配置 Spring AOP。接下來的兩個示例顯示了一個不太有用的切面所需的最小定義。  

兩個示例中的第一個示例顯示了應用程式上下文中的常規 bean 定義,該定義指向具有@Aspect註解的 bean 類:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of the aspect here -->
</bean>

這兩個示例中的第二個示例顯示了NotVeryUsefulAspect類定義,該類定義帶有org.aspectj.lang.annotation.Aspect註解;  

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

切面(帶有@Aspect註解 的類)可以具有方法和欄位,與任何其他類相同。它們還可以包含切入點,通知和引入(型別間)宣告。  

通過元件掃描自動檢測方面

可以將切面類註冊為 Spring XML 配置中的常規 bean,也可以通過 Classpath 掃描來自動檢測它們-與其他任何 SpringManagement 的 bean 一樣。但是,請注意,@Aspect註解 不足以在 Classpath 中進行自動檢測。為此,您需要新增一個單獨的@Component註解(或者,或者,按照 Spring 的元件掃描程式的規則,有條件的自定義構造型註解)。

在 Spring AOP 中,切面本身不能成為其他切面的通知目標。類上的@Aspect註解 將其標記為一個切面,因此將其從自動代理中排除。

5.4.3. 宣告切入點

切入點確定了感興趣的連線點,從而使我們能夠控制執行建議的時間。 Spring AOP 僅支援 Spring Bean 的方法執行連線點,因此您可以將切入點視為與 Spring Bean 上的方法執行相匹配。切入點宣告由兩部分組成:一個包含名稱和任何引數的簽名,以及一個切入點表示式,該切入點表示式準確確定我們感興趣的方法執行。在 AOP 的@AspectJ 註解樣式中,常規方法定義提供了切入點簽名。 並通過使用@Pointcut註解 指示切入點表示式(用作切入點簽名的方法必須具有void返回型別)。

一個示例可能有助於使切入點簽名和切入點表示式之間的區別變得清晰。下面的示例定義一個名為anyOldTransfer的切入點,該切入點與任何名為transfer的方法的執行相匹配:

@Pointcut("execution(* transfer(..))")// 切入點表示式
private void anyOldTransfer() {}// 切入點簽名

形成@Pointcut註解的值的切入點表示式是一個常規的 AspectJ 5 切入點表示式。  

您可以在需要切入點表示式的任何地方引用在這樣的方面中定義的切入點。例如,要使服務層具有事務性,您可以編寫以下內容:

<aop:config>
    <aop:advisor
        pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
        advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

基於架構的 AOP 支援中討論了<aop:config><aop:advisor>元素。Transaction 元素在Transaction Management中討論。  

5.4.4. 通知

通知與切入點表示式關聯,並且在切入點匹配的方法執行之前,之後或周圍執行。切入點表示式可以是對命名切入點的簡單引用,也可以是就地宣告的切入點表示式。

Before Advice

您可以使用@Before註解 在方面中在建議之前宣告:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

如果使用就地切入點表示式,則可以將前面的示例重寫為以下示例:  

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }

}
返回建議後

返回建議後,當匹配的方法執行正常返回時,執行建議。您可以使用@AfterReturning註解進行宣告:  

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

Note

您可以在同一切面內擁有多個通知宣告(以及其他成員)。在這些示例中,我們僅顯示單個通知宣告,以集中每個通知的效果。

有時,您需要在建議正文中訪問返回的實際值。您可以使用@AfterReturning的形式繫結返回值以獲取該訪問許可權,如以下示例所示:

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

returning屬性中使用的名稱必須與 advice 方法中的引數名稱相對應。當方法執行返回時,該返回值將作為相應的引數值傳遞到通知方法。returning子句還將匹配僅限制為返回指定型別值(在這種情況下為Object,該值與任何返回值匹配)的那些方法執行。  

丟擲建議後  

丟擲建議後,當匹配的方法執行通過丟擲異常退出時執行建議。您可以使用@AfterThrowing註解進行宣告,如以下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

}

通常,您希望通知僅在引發給定型別的異常時才執行,並且您通常還需要訪問通知正文中的異常。您可以使用throwing屬性來限制匹配(如果需要)(否則,請使用Throwable作為異常型別),並將丟擲的異常繫結到 advice 引數。以下示例顯示瞭如何執行此操作:  

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

throwing屬性中使用的名稱必須與 advice 方法中的引數名稱相對應。當通過丟擲異常退出方法執行時,該異常將作為相應的引數值傳遞給通知方法。throwing子句還將匹配僅限制為丟擲指定型別(在本例中為DataAccessException)的異常的方法執行。  

(最後)建議後

當匹配的方法執行退出時,通知(最終)執行。通過使用@After註解 進行宣告。之後必須準備處理正常和異常返回條件的建議。它通常用於釋放資源和類似目的。以下示例顯示了最終建議後的用法:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }

}
Around Advice

最後一種建議是圍繞建議。圍繞建議在匹配方法的執行過程中“圍繞”執行。它有機會在方法執行之前和之後進行工作,並確定何時,如何以及甚至根本不執行該方法。如果需要以執行緒安全的方式(例如,啟動和停止計時器)在方法執行之前和之後共享狀態,則通常使用繞行建議。始終使用最不符合要求的建議形式(即,在建議可以使用之前,不要在建議周圍使用)。

周圍的建議通過使用@Around註解 來宣告。諮詢方法的第一個引數必須為ProceedingJoinPoint型別。在建議的正文中,在ProceedingJoinPoint上呼叫proceed()會使底層方法執行。proceed方法也可以傳入Object[]。陣列中的值用作方法執行時的引數。

Note

對於由 AspectJ 編譯器編譯的周圍建議,使用Object[]呼叫時proceed的行為與proceed的行為稍有不同。對於使用傳統的 AspectJ 語言編寫的環繞通知,傳遞給proceed的引數數量必須與傳遞給環繞通知的引數數量(而不是基礎連線點採用的引數數量)相匹配,並且傳遞給值的值必須與給定的引數位置會取代該值繫結到的實體的連線點處的原始值(不要擔心,如果這現在沒有意義)。 Spring 採取的方法更簡單,並且更適合其基於代理的,僅執行的語義。如果您編譯為 Spring 編寫的@AspectJ 方面,並將proceed與 AspectJ 編譯器和 weaver 的引數一起使用,則只需要意識到這種區別。有一種方法可以編寫在 Spring AOP 和 AspectJ 之間 100%相容的方面。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}

周圍建議返回的值是該方法的呼叫者看到的返回值。例如,如果一個簡單的快取方面有一個值,則可以從快取中返回一個值;如果沒有,則呼叫proceed()。請注意,proceed可能在周圍建議的正文中被呼叫一次,多次或完全不被呼叫。所有這些都是合法的。  

通知引數

Spring 提供了完全型別化的建議,這意味著您可以在建議簽名中宣告所需的引數(如我們先前在返回和丟擲示例中所見),而不是一直使用Object[]陣列。

本節的後面部分介紹如何使引數和其他上下文值可用於建議主體。首先,我們看一下如何編寫通用建議,以瞭解當前建議的方法。  

訪問當前的 JoinPoint

任何通知方法都可以將型別org.aspectj.lang.JoinPoint的引數宣告為第一個引數(請注意,在周圍的通知中必須宣告型別JoinPoint的子類ProceedingJoinPoint的第一個引數。JoinPoint介面提供了許多有用的方法:

  • getArgs():返回方法引數。

  • getThis():返回代理物件。

  • getTarget():返回目標物件。

  • getSignature():返回建議使用的方法的描述。

  • toString():列印有關所建議方法的有用描述。

將引數傳遞給通知

我們已經看到了如何繫結返回的值或異常值(在返回之後和引發建議之後使用)。要使引數值可用於建議正文,可以使用args的繫結形式。如果在 args 表示式中使用引數名稱代替型別名稱,則在呼叫建議時會將相應引數的值作為引數值傳遞。一個例子應該使這一點更清楚。假設您要建議執行以Account物件作為第一個引數的 DAO 操作,並且您需要訪問建議正文中的帳戶。您可以編寫以下內容:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

切入點表示式的args(account,..)部分有兩個作用。首先,它將匹配限制為僅方法採用至少一個引數並且傳遞給該引數的引數是Account的例項的方法執行。其次,它通過account引數使實際的Account物件可用於通知。 

編寫此檔案的另一種方法是宣告一個切入點,當切入點Account物件值與連線點匹配時,該切入點“提供”,然後從建議中引用命名切入點。如下所示: 

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)") 
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)") public void validateAccount(Account account) { // ... }

  

代理物件(this),目標物件(target)和 註解(@within@target@annotation@args)都可以以類似的方式繫結。接下來的兩個示例顯示如何匹配使用@Auditable註解的方法的執行並提取稽核程式碼:

這兩個示例中的第一個顯示了@Auditable註解的定義:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

這兩個示例中的第二個示例顯示與@Auditable方法的執行相匹配的建議:  

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}

通知引數和泛型  

Spring AOP 可以處理類宣告和方法引數中使用的泛型。假設您具有如下通用型別:

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

您可以通過在要攔截方法的引數型別中鍵入 advice 引數,將方法型別的攔截限制為某些引數型別  

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

這種方法不適用於通用集合。因此,您不能按以下方式定義切入點:  

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

  

確定引數名稱

通知呼叫中的引數繫結依賴於切入點表示式中使用的名稱與通知和切入點方法簽名中宣告的引數名稱的匹配。通過 Java 反射無法獲得引數名稱,因此 Spring AOP 使用以下策略來確定引數名稱:

  • 如果使用者已明確指定引數名稱,則使用指定的引數名稱。通知和切入點註解都具有可選的argNames屬性,您可以使用該屬性來指定帶註解方法的引數名稱。這些引數名稱在執行時可用。下面的示例演示如何使用argNames屬性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

如果第一個引數是JoinPointProceedingJoinPointJoinPoint.StaticPart型別,則可以從argNames屬性的值中省略引數的名稱。例如,如果您修改前面的建議以接收連線點物件,則argNames屬性不需要包括它:  

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

對於不收集任何其他連線點上下文的建議例項,對JoinPointProceedingJoinPointJoinPoint.StaticPart型別的第一個引數進行特殊處理特別方便。在這種情況下,您可以省略argNames屬性。例如,以下建議無需宣告argNames屬性:  

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

Note

如果即使沒有除錯資訊,AspectJ 編譯器(ajc)都已編譯@AspectJ 方面,則無需新增argNames屬性,因為編譯器會保留所需的資訊。

處理引數  

前面我們提到過,我們將描述如何使用在 Spring AOP 和 AspectJ 上始終有效的引數編寫proceed呼叫。解決方案是確保建議簽名按 Sequences 繫結每個方法引數。以下示例顯示瞭如何執行此操作:

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}

Advice Ordering

當多條建議都希望在同一連線點上執行時會發生什麼? Spring AOP 遵循與 AspectJ 相同的優先順序規則來確定建議執行的 Sequences。優先順序最高的建議首先“在途中”執行(因此,給定兩條優先建議,則優先順序最高的建議首先執行)。從連線點“出路”中,優先順序最高的建議將最後執行(因此,給定兩條後置通知,優先順序最高的建議將第二次執行)。

當在不同方面定義的兩條建議都需要在同一連線點上執行時,除非另行指定,否則執行 Sequences 是不確定的。您可以通過指定優先順序來控制執行 Sequences。通過在 Aspect 類中實現org.springframework.core.Ordered介面或使用Order註解對其進行 註解,可以通過普通的 Spring 方法來完成。給定兩個方面,從Ordered.getValue()返回較低值(或註解值)的方面具有較高的優先順序。

當在相同方面定義的兩條建議都需要在同一連線點上執行時,其 Sequences 是未定義的(因為無法通過反射來獲取 javac 編譯類的宣告 Sequences)。考慮將這些建議方法摺疊成每個方面類中每個連線點的一個建議方法,或將建議重構為單獨的方面類,您可以在方面級別進行 Order。

  

5.4.5. Introductions

簡介使切面可以宣告建議物件實現給定的介面,並代表那些物件提供該介面的實現。

您可以使用@DeclareParents註解 進行介紹。此註解用於宣告匹配型別具有新的父代(因此而得名)。例如,在給定名為UsageTracked的介面和該介面名為DefaultUsageTracked的實現的情況下,以下方面宣告服務介面的所有實現者也都實現UsageTracked介面(例如,通過 JMX 公開統計資訊):

@Aspect
public class UsageTracking {

    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

要實現的介面由帶註解的欄位的型別確定。@DeclareParents註解的value屬性是 AspectJ 型別的模式。任何匹配型別的 bean 都實現UsageTracked介面。請注意,在前面示例的建議中,服務 Bean 可以直接用作UsageTracked介面的實現。如果以程式設計方式訪問 bean,則應編寫以下內容:  

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

5.4.7. AOP 示例

有時由於併發問題(例如,死鎖失敗者),業務服務的執行可能會失敗。如果重試該操作,則很可能在下一次嘗試中成功。對於適合在這種情況下重試的業務服務(不需要為解決衝突而需要返回給使用者的冪等操作),我們希望透明地重試該操作以避免 Client 端看到PessimisticLockingFailureException。這項要求清楚地跨越了服務層中的多個服務,因此非常適合通過一個方面實施。

因為我們想重試該操作,所以我們需要使用“周圍”建議,以便我們可以多次呼叫proceed。以下清單顯示了基本方面的實現:

@Aspect
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;
    }

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    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介面,因此我們可以將切面的優先順序設定為高於事務建議(每次重試時都希望有新的事務)。maxRetriesorder屬性均由 Spring 配置。主要動作發生在doConcurrentOperation周圍建議中。請注意,目前,我們將重試邏輯應用於每個businessService()。我們嘗試 continue,如果失敗了PessimisticLockingFailureException,我們將重試,除非我們用盡了所有的重試嘗試。  

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

為了優化切面,使其僅重試冪等操作,我們可以定義以下Idempotent註解:  

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

然後,我們可以使用註解來註解服務操作的實現。方面的更改僅重試冪等操作涉及精簡切入點表示式,以便只有@Idempotent個操作匹配,如下所示:  

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    ...
}

5.5. 基於架構的 AOP 支援

如果您更喜歡基於 XML 的格式,Spring 還支援使用新的aop名稱空間標籤定義方面。支援與使用@AspectJ 樣式時完全相同的切入點表示式和建議型別。  

要使用本節中描述的 aop 名稱空間標籤,您需要匯入spring-aop模式,如基於 XML 模式的配置中所述。有關如何在aop名稱空間中匯入標籤的資訊,請參見AOP 模式

在您的 Spring 配置中,所有切面和顧問元素都必須放在<aop:config>元素內(在應用程式上下文配置中可以有多個<aop:config>元素)。<aop:config>元素可以包含切入點,顧問和切面元素(請注意,這些元素必須按此 Sequences 宣告)。

Warning

<aop:config>樣式的配置大量使用了 Spring 的auto-proxying機制。如果您已經通過使用BeanNameAutoProxyCreator或類似方法來使用顯式自動代理,則可能會導致問題(例如未編制建議)。推薦的用法模式是僅使用<aop:config>樣式或僅AutoProxyCreator樣式,並且不要混合使用。

5.5.1. 宣告一個切面

使用模式支援時,切面是在 Spring 應用程式上下文中定義為 Bean 的常規 Java 物件。狀態和行為在物件的欄位和方法中捕獲,切入點和建議資訊在 XML 中捕獲。

您可以使用\ <>元素宣告一個方面,並使用ref屬性引用該支援 bean,如以下示例所示:

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

<bean id="aBean" class="...">
    ...
</bean>

支援方面(在這種情況下為aBean)的 bean 當然可以像配置任何其他 Spring bean 一樣進行配置並注入依賴項。  

5.5.2. 宣告切入點

您可以在<aop:config>元素中宣告命名的切入點,從而使切入點定義在多個方面和顧問程式之間共享。

可以定義代表服務層中任何業務服務的執行的切入點

<aop:config>

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

</aop:config>

注意,切入點表示式本身使用的是與@AspectJ support中所述的 AspectJ 切入點表示式語言。

如果使用基於架構的宣告樣式,則可以引用在切入點表示式中的型別(@Aspects)中定義的命名切入點。定義上述切入點的另一種方法如下:  

<aop:config>

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

</aop:config>

  

假定您具有共享通用切入點定義中所述的SystemArchitecture外觀。

然後,在方面中宣告切入點與宣告頂級切入點非常相似,如以下示例所示:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

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

        ...

    </aop:aspect>

</aop:config>

與@AspectJ 方面幾乎相同,使用基於架構的定義樣式宣告的切入點可以收集連線點上下文。例如,以下切入點收集this物件作為連線點上下文,並將其傳遞給建議:  

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

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

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

        ...

    </aop:aspect>

</aop:config>

必須宣告通知,以通過包含匹配名稱的引數來接收收集的連線點上下文,如下所示:  

public void monitor(Object service) {
    ...
}

組合切入點子表示式時,XML 文件中的&&很尷尬,因此可以分別使用andornot關鍵字代替&&||!。例如,上一個切入點可以更好地編寫如下:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

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

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

        ...
    </aop:aspect>
</aop:config>

  

5.5.3. 宣告建議

基於模式的 AOP 支援使用與@AspectJ 樣式相同的五種建議,並且它們具有完全相同的語義。

Before Advice

在執行匹配的方法之前,建議執行之前。使用\ <>元素在<aop:aspect>內部宣告它,如以下示例所示:

<aop:aspect id="beforeExample" ref="aBean">

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

    ...

</aop:aspect>

  

在這裡,dataAccessOperation是在最高(<aop:config>)級別定義的切入點的id。要定義切入點內聯,請用pointcut屬性替換pointcut-ref屬性,如下所示:

<aop:aspect id="beforeExample" ref="aBean">

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

    ...

</aop:aspect>

method屬性標識提供建議正文的方法(doAccessCheck)。必須為包含建議的 Aspect 元素所引用的 bean 定義此方法。在執行資料訪問操作(與切入點表示式匹配的方法執行連線點)之前,將呼叫 Aspect Bean 上的doAccessCheck方法。  

返回建議後

返回的建議在匹配的方法執行正常完成時執行。在<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) {...

提出建議後

丟擲建議後,當匹配的方法執行通過丟擲異常退出時執行建議。通過使用擲後元素在<aop:aspect>內部宣告它,如以下示例所示:  

<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"
        throwing="dataAccessEx"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

doRecoveryActions方法必須宣告一個名為dataAccessEx的引數。該引數的型別以與@AfterThrowing相同的方式約束匹配。例如,方法簽名可以宣告如下:  

public void doRecoveryActions(DataAccessException dataAccessEx) {...

  

(最後)建議後

無論最終如何執行匹配的方法,建議(最終)都會執行。您可以使用after元素對其進行宣告,如以下示例所示:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>

  

環繞通知

最後一種建議是圍繞建議。圍繞建議在匹配的方法執行過程中“圍繞”執行。它有機會在方法執行之前和之後進行工作,並確定何時,如何以及什至根本不執行該方法。周圍建議通常用於以執行緒安全的方式(例如,啟動和停止計時器)在方法執行之前和之後共享狀態。始終使用最不強大的建議形式,以滿足您的要求。如果建議可以完成這項工作,請不要在建議周圍使用。

您可以使用aop:around元素在建議周圍進行宣告。諮詢方法的第一個引數必須為ProceedingJoinPoint型別。在建議的正文中,在ProceedingJoinPoint上呼叫proceed()會導致基礎方法執行。proceed方法也可以用Object[]呼叫。陣列中的值用作方法執行時的引數。有關使用Object[]呼叫proceed的說明,請參見Around Advice。以下示例顯示瞭如何在 XML 中圍繞建議進行宣告:

<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;
}

  

通知引數  

基於架構的宣告樣式以與@AspectJ 支援相同的方式支援完全型別的建議,即通過名稱與建議方法引數匹配切入點引數。有關詳情,請參見Advice Parameters。如果您希望顯式指定建議方法的引數名稱(不依賴於先前描述的檢測策略),則可以通過使用建議元素的arg-names屬性來實現,該屬性與argNames屬性的處理方式相同。建議 註解(如確定引數名稱中所述)。以下示例顯示如何在 XML 中指定引數名稱:

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

arg-names屬性接受逗號分隔的引數名稱列表。

以下基於 XSD 的方法中涉及程度稍高的示例顯示了一些與一些強型別引數結合使用的建議:  

package x.y.service;

public interface PersonService {

    Person getPerson(String personName, int age);
}

public class DefaultFooService implements FooService {

    public Person getPerson(String name, int age) {
        return new Person(name, age);
    }
}

接下來是方面。請注意,profile(..)方法接受許多強型別的引數,第一個恰好是用於進行方法呼叫的連線點。此引數的存在表明profile(..)用作around建議,如以下示例所示:  

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.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
    <bean id="personService" class="x.y.service.DefaultPersonService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config>
        <aop:aspect ref="profiler">

            <aop:pointcut id="theExecutionOfSomePersonServiceMethod"
                expression="execution(* x.y.service.PersonService.getPerson(String,int))
                and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
                method="profile"/>

        </aop:aspect>
    </aop:config>

</beans>

考慮以下驅動程式指令碼:  

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        PersonService person = (PersonService) ctx.getBean("personService");
        person.getPerson("Pengo", 12);
    }
}

  

通知順序

當需要在同一連線點(執行方法)上執行多個建議時,排序規則如Advice Ordering中所述。方面之間的優先順序是通過將Order註解新增到支援方面的 Bean 或通過使 Bean 實現Ordered介面來確定的。

5.5.4. Introductions

簡介(在 AspectJ 中稱為型別間宣告)使方面可以宣告建議的物件實現給定的介面,並代表那些物件提供該介面的實現。

您可以使用aop:aspect內的aop:declare-parents元素進行介紹。您可以使用aop:declare-parents元素來宣告匹配型別具有新的父代(因此而得名)。例如,給定名為UsageTracked的介面和該名為DefaultUsageTracked的介面的實現,以下方面宣告服務介面的所有實現者也都實現UsageTracked介面。 (例如,為了通過 JMX 公開統計資訊.)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before
        pointcut="com.xyz.myapp.SystemArchitecture.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

支援usageTrackingbean 的類將包含以下方法:  

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

要實現的介面由implement-interface屬性確定。types-matching屬性的值是 AspectJ 型別的模式。任何匹配型別的 bean 都實現UsageTracked介面。請注意,在前面示例的建議中,服務 Bean 可以直接用作UsageTracked介面的實現。要以程式設計方式訪問 bean,可以編寫以下程式碼:  

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

5.5.6. Advisors

“顧問”的概念來自 Spring 中定義的 AOP 支援,並且在 AspectJ 中沒有直接等效的概念。顧問就像一個獨立的小方面,只有一條建議。通知本身由 bean 表示,並且必須實現Spring 的建議型別中描述的建議介面之一。顧問可以利用 AspectJ 切入點表示式。

Spring 通過<aop:advisor>元素支援顧問程式概念。您通常會看到它與事務建議結合使用,事務建議在 Spring 中也有自己的名稱空間支援。以下示例顯示顧問程式:

<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屬性來內聯定義切入點表示式。

要定義顧問程式的優先順序,以便該建議書可以參與 Order,請使用order屬性來定義顧問程式的Ordered值。

5.5.7. AOP 模式示例

有時由於併發問題(例如,死鎖失敗者),業務服務的執行可能會失敗。如果重試該操作,則很可能在下一次嘗試中成功。對於適合在這種情況下重試的業務服務(不需要為解決衝突而需要返回給使用者的冪等操作),我們希望透明地重試該操作以避免 Client 端看到PessimisticLockingFailureException。這項要求清楚地跨越了服務層中的多個服務,因此非常適合通過一個方面實施。

因為我們想重試該操作,所以我們需要使用“周圍”建議,以便我們可以多次呼叫proceed。以下清單顯示了基本方面的實現(這是使用模式支援的常規 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介面,因此我們可以將方面的優先順序設定為高於事務建議(每次重試時都希望有新的事務)。maxRetriesorder屬性均由 Spring 配置。主要動作發生在doConcurrentOperation周圍建議方法中。我們嘗試 continue。如果我們失敗了PessimisticLockingFailureException,我們將重試,除非我們用盡了所有的重試嘗試。  

  

Note

該類與@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>

請注意,目前我們假設所有業務服務都是冪等的。如果不是這種情況,我們可以通過引入Idempotent註解 並使用該註解來註解服務操作的實現,來改進方面,使其僅重試 true 的冪等操作,如以下示例所示:  

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

方面的更改僅重試冪等操作涉及精簡切入點表示式,以便只有@Idempotent個操作匹配,如下所示:  

<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>

  

5.6. 選擇要使用的 AOP 宣告樣式

一旦確定方面是實現給定需求的最佳方法,您如何在使用 Spring AOP 或 AspectJ 以及在 Aspect 語言(程式碼)樣式,@ AspectJ 註解樣式或 Spring XML 樣式之間做出選擇?這些決定受許多因素影響,包括應用程式需求,開發工具以及團隊對 AOP 的熟悉程度。  

5.6.1. Spring AOP 還是 Full AspectJ?

使用最簡單的方法即可。 Spring AOP 比使用完整的 AspectJ 更簡單,因為不需要在開發和構建過程中引入 AspectJ 編譯器/編織器。如果您只需要建議在 Spring bean 上執行操作,則 Spring AOP 是正確的選擇。如果您需要建議不受 Spring 容器 Management 的物件(通常是域物件),則需要使用 AspectJ。如果您希望建議除簡單方法執行以外的連線點(例如,欄位 get 或設定連線點等),則還需要使用 AspectJ。

使用 AspectJ 時,可以選擇 AspectJ 語言語法(也稱為“程式碼樣式”)或@AspectJ註解 樣式。顯然,如果您不使用 Java 5,那麼將為您做出選擇:使用程式碼樣式。如果方面在您的設計中起著重要作用,並且您能夠將AspectJ 開發工具(AJDT)外掛用於 Eclipse,則 AspectJ 語言語法是首選。它更乾淨,更簡單,因為該語言是專為編寫方面而設計的。如果您不使用 Eclipse 或只有少數幾個方面在您的應用程式中不起作用,那麼您可能要考慮使用@AspectJ 樣式,在 IDE 中堅持常規 Java 編譯,並向其中新增方面編織階段您的構建指令碼。

5.6.2. @AspectJ 或 Spring AOP 的 XML?

如果選擇使用 Spring AOP,則可以選擇@AspectJ 或 XML 樣式。有各種折衷考慮。

XML 樣式可能是現有 Spring 使用者最熟悉的,並且得到了 true 的 POJO 的支援。當使用 AOP 作為配置企業服務的工具時,XML 可能是一個不錯的選擇(一個很好的測試是您是否將切入點表示式視為配置的一部分,您可能希望獨立更改)。使用 XML 樣式,可以說從您的配置中可以更清楚地瞭解系統中存在哪些方面。

XML 樣式有兩個缺點。首先,它沒有完全將要解決的需求的實現封裝在一個地方。 DRY 原則說,系統中的任何知識都應該有單一,明確,Authority 的表示形式。當使用 XML 樣式時,關於如何實現需求的知識會在配置檔案中的後備 bean 類的宣告和 XML 中分散。當您使用@AspectJ 樣式時,此資訊將封裝在一個單獨的模組中:方面。其次,與@AspectJ 樣式相比,XML 樣式在表達能力上有更多限制:僅支援“單例”方面例項化模型,並且無法組合以 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+ *(..))"/>

XML 方法的缺點是無法通過組合這些定義來定義accountPropertyAccess切入點。

@AspectJ 樣式支援其他例項化模型和更豐富的切入點組合。它具有將方面保持為模組化單元的優勢。它還具有的優點是,Spring AOP 和 AspectJ 都可以理解@AspectJ 方面。因此,如果您以後決定需要 AspectJ 的功能來實現其他要求,則可以輕鬆地遷移到基於 AspectJ 的方法。總而言之,只要您擁有比簡單地配置企業服務更多的功能,Spring 團隊就會喜歡@AspectJ 樣式。

  

5.7. 混合方面型別

通過使用自動代理支援,模式定義的<aop:aspect>方面,<aop:advisor>宣告的顧問程式,甚至是在同一配置中使用 Spring 1.2 樣式定義的代理和攔截器,完全可以混合使用@AspectJ 樣式的方面。所有這些都是通過使用相同的基礎支援機制實現的,並且可以毫無困難地共存。

5.8. 代理機制

Spring AOP 使用 JDK 動態代理或 CGLIB 建立給定目標物件的代理。 (只要有選擇,首選 JDK 動態代理)。

如果要代理的目標物件實現至少一個介面,則使用 JDK 動態代理。代理了由目標型別實現的所有介面。如果目標物件未實現任何介面,則將建立 CGLIB 代理。

如果要強制使用 CGLIB 代理(例如,代理為目標物件定義的每個方法,而不僅是由其介面實現的方法),都可以這樣做。但是,您應該考慮以下問題:

  • 不能建議final方法,因為它們不能被覆蓋。

  • 從 Spring 3.2 開始,不再需要將 CGLIB 新增到您的專案 Classpath 中,因為 CGLIB 類在org.springframework下重新打包並直接包含在 spring-core JAR 中。這意味著基於 CGLIB 的代理支援“有效”,就像 JDK 動態代理始終具有的一樣。

  • 從 Spring 4.0 開始,由於 CGLIB 代理例項是通過 Objenesis 建立的,因此不再呼叫代理物件的建構函式兩次。僅當您的 JVM 不允許繞過建構函式時,您才可能從 Spring 的 AOP 支援中看到兩次呼叫和相應的除錯日誌條目。

要強制使用 CGLIB 代理,請將<aop:config>元素的proxy-target-class屬性的值設定為 true,如下所示:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

要在使用@AspectJ 自動代理支援時強制 CGLIB 代理,請將<aop:aspectj-autoproxy>元素的proxy-target-class屬性設定為true,如下所示:  

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

  

Note

多個<aop:config/>節在執行時摺疊到一個統一的自動代理建立器中,該建立器將應用<aop:config/>節中的任何(通常來自不同 XML bean 定義檔案)指定的* strong *代理設定。這也適用於<tx:annotation-driven/><aop:aspectj-autoproxy/>元素。

明確地說,在<tx:annotation-driven/><aop:aspectj-autoproxy/><aop:config/>元素上使用proxy-target-class="true"會強制對所有三個元素*使用 CGLIB 代理。

5.8.1. 瞭解 AOP 代理

Spring AOP 是基於代理的。在編寫自己的方面或使用 Spring Framework 隨附的任何基於 Spring AOP 的方面之前,掌握最後一條語句實際含義的語義至關重要。

首先考慮您有一個普通的,未經代理的,沒有什麼特別的,直接的物件引用的情況,如以下程式碼片段所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

如果在物件引用上呼叫方法,則直接在該物件引用上呼叫該方法,如下圖和清單所示:  

public class Main {

    public static void main(String[] args) {

        Pojo pojo = new SimplePojo();

        // this is a direct method call on the 'pojo' reference
        pojo.foo();
    }
}

當 Client 端程式碼具有的引用是代理時,情況會稍有變化。考慮以下圖表和程式碼片段:  

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }
}

這裡要理解的關鍵是,Main類的main(..)方法內部的 Client 端程式碼具有對代理的引用。這意味著該物件引用上的方法呼叫是代理上的呼叫。結果,代理可以委派給與該特定方法呼叫相關的所有攔截器(建議)。但是,一旦呼叫最終到達目標物件(在這種情況下為SimplePojo,則為this.bar()this.foo()),則將針對this引用而不是對this引用呼叫它可能對其自身進行的任何方法呼叫。代理。這具有重要的意義。這意味著自呼叫不會導致與方法呼叫相關的建議得到執行的機會。  

好吧,那麼該怎麼辦?最好的方法是重構程式碼,以免發生自呼叫。這確實需要您做一些工作,但這是最好的,侵入性最小的方法。

下一種方法絕對可怕,我們正要指出這一點,恰恰是因為它是如此可怕。您可以完全將類中的邏輯與 Spring AOP 繫結在一起,如以下示例所示:

public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

這將您的程式碼完全耦合到 Spring AOP,並且使類本身意識到在 AOP 上下文中使用它這一事實,而 AOP 卻是事實。建立代理時,還需要一些其他配置,如以下示例所示:  

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.adddInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!
        pojo.foo();
    }
}

最後,必須注意,AspectJ 沒有此自呼叫問題,因為它不是基於代理的 AOP 框架。

5.9. 以程式設計方式建立@AspectJ 代理

除了使用<aop:config><aop:aspectj-autoproxy>宣告配置中的各個方面外,還可以通過程式設計方式建立建議目標物件的代理。

您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory類為一個或多個@AspectJ 方面建議的目標物件建立代理。此類的基本用法非常簡單,如以下示例所示:

// create a factory that can generate a proxy for the given target object建立一個可以為給定目標物件生成代理的工廠
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// 新增一個方面,類必須是@AspectJ方面
// 對於不同的方面,您可以根據需要多次呼叫它
factory.addAspect(SecurityManager.class);

// 您還可以新增現有的方面例項,提供的物件型別必須是@AspectJ方面
factory.addAspect(usageTracker);

// 現在獲取代理物件...
MyInterfaceType proxy = factory.getProxy();

  

5.10. 在 Spring 應用程式中使用 AspectJ

到目前為止,本章介紹的所有內容都是純 Spring AOP。在本節中,我們將研究如果您的需求超出了 Spring AOP 所提供的功能,那麼如何使用 AspectJ 編譯器或 weaver 代替 Spring AOP 或除 Spring AOP 之外使用。

Spring 附帶了一個小的 AspectJ 方面庫,該庫在您的發行版中可以作為spring-aspects.jar獨立使用。您需要將其新增到 Classpath 中才能使用其中的方面。

5.10.1. 使用 AspectJ 通過 Spring 依賴注入域物件

Spring 容器例項化並配置在您的應用程式上下文中定義的 bean。給定包含要應用的配置的 Bean 定義的名稱,也可以要求 Bean 工廠配置預先存在的物件。spring-aspects.jar包含註解驅動的切面,該切面利用此功能允許任何物件的依賴項注入。該支架旨在用於在任何容器的控制範圍之外建立的物件。域物件通常屬於此類,因為它們通常是通過new運算子或通過 ORM 工具以資料庫查詢的方式通過程式建立的。

@Configurable註解 將一個類標記為符合 Spring 驅動的配置。在最簡單的情況下,您可以將其純粹用作標記 註解,如以下示例所示:

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
    // ...
}

當以這種方式用作標記介面時,Spring 通過使用具有與完全限定型別名稱(com.xyz.myapp.domain.Account)同名的 bean 定義(通常為原型作用域)來配置帶註解型別的新例項(在本例中為Account)。 。由於 Bean 的預設名稱是其型別的完全限定名稱,因此宣告原型定義的便捷方法是省略id屬性,如以下示例所示:  

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要顯式指定要使用的原型 bean 定義的名稱,則可以直接在註解中這樣做,如以下示例所示:  

package com.xyz.myapp.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable("account")
public class Account {
    // ...
}

Spring 現在查詢名為account的 bean 定義,並將其用作配置新Account例項的定義。

您也可以使用自動裝配來避免完全指定專用的 bean 定義。要讓 Spring 應用自動裝配,請使用@Configurable註解的autowire屬性。您可以分別按型別或名稱指定@Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME自動佈線。或者,從 Spring 2.5 開始,最好在欄位或方法級別使用@Autowired@Inject@Configurablebean 指定顯式的,註解 驅動的依賴項注入

最後,您可以使用dependencyCheck屬性(例如@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true))為新建立和配置的物件中的物件引用啟用 Spring 依賴項檢查。如果此屬性設定為true,則 Spring 在配置後驗證是否已設定所有屬性(不是基元或集合)。  

請注意,單獨使用註解不會執行任何操作。註解 中存在的是spring-aspects.jar中的AnnotationBeanConfigurerAspect。從本質上講,方面說:“在從帶有@Configurable註解 的型別的新物件的初始化返回之後,根據註解的屬性使用 Spring 配置新建立的物件”。在這種情況下,“初始化”是指新例項化的物件(例如,用new運算子例項化的物件)以及正在反序列化(例如,通過readResolve())的Serializable物件。

Note

上段中的關鍵短語之一是“本質上”。對於大多數情況,“從新物件的初始化返回後”的確切語義是可以的。在這種情況下,“初始化之後”是指在構造物件之後注入依賴項。這意味著該依賴項不可在類的建構函式體中使用。

如果要在建構函式主體執行之前注入依賴項,從而可以在建構函式主體中使用這些依賴項,則需要在@Configurable宣告中定義此變數,如下所示:

@Configurable(preConstruction=true)

  

為此,必須將帶註解的型別與 AspectJ 編織器編織在一起。您可以使用構建時 Ant 或 Maven 任務來執行此操作,也可以使用載入時編織。AnnotationBeanConfigurerAspect本身需要由 Spring 配置(以獲取對將用於配置新物件的 Bean 工廠的引用)。

如果使用基於 Java 的配置,則可以將@EnableSpringConfigured新增到任何@Configuration類中,如下所示:

@Configuration
@EnableSpringConfigured
public class AppConfig {

}

如果您喜歡基於 XML 的配置,Springcontext namespace定義了一個方便的context:spring-configured元素,您可以按以下方式使用它:  

<context:spring-configured/>

  

在配置方面之前建立的@Configurable個物件的例項導致向除錯日誌發出一條訊息,並且未進行任何物件配置。一個示例可能是 Spring 配置中的 bean,當它由 Spring 初始化時會建立域物件。在這種情況下,可以使用depends-onbean 屬性來手動指定該 bean 取決於配置方面。下面的示例演示如何使用depends-on屬性:

<bean id="myService"
        class="com.xzy.myapp.service.MyService"
        depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>

Note

除非您真的想在執行時依賴它的語義,否則不要通過 bean configurer 方面啟用@Configurable處理。特別是,請確保不要在已通過容器註冊為常規 Spring bean 的 bean 類上使用@Configurable。這樣做將導致兩次初始化,一次是通過容器,一次是通過方面。

  

單元測試@Configurable 物件

@Configurable支援的目標之一是實現域物件的獨立單元測試,而不會遇到與硬編碼查詢相關的困難。如果 AspectJ 尚未編織@Configurable型別,則註解在單元測試期間不起作用。您可以在被測物件中設定模擬或存根屬性引用,然後照常進行。如果@Configurable型別是 AspectJ 編織的,您仍然可以像往常一樣在容器外部進行單元測試,但是每次構造@Configurable物件時,都會看到一條警告訊息,指示該物件尚未由 Spring 配置。

處理多個應用程式上下文

用於實現@Configurable支援的AnnotationBeanConfigurerAspect是 AspectJ 單例方面。單例方面的範圍與static成員的範圍相同:每個類載入器都有一個方面例項來定義型別。這意味著,如果您在同一個類載入器層次結構中定義多個應用程式上下文,則需要考慮在哪裡定義@EnableSpringConfiguredbean 以及在哪裡將spring-aspects.jar放在 Classpath 上。

考慮一個典型的 Spring Web 應用程式配置,該配置具有一個共享的父應用程式上下文,該上下文定義了通用的業務服務,支援那些服務所需的一切,以及每個 Servlet 的一個子應用程式上下文(其中包含該 Servlet 的特定定義)。所有這些上下文共存於相同的類載入器層次結構中,因此AnnotationBeanConfigurerAspect只能保留對其中一個的引用。在這種情況下,我們建議在共享(父)應用程式上下文中定義@EnableSpringConfiguredbean。這定義了您可能想注入域物件的服務。結果是,您無法使用@Configurable 機制來配置域物件,該域物件引用的是在子(特定於 servlet 的)上下文中定義的 bean 的引用(無論如何,這可能不是您想做的事情)。

在同一容器中部署多個 Web 應用程式時,請確保每個 Web 應用程式使用自己的類載入器(例如,將spring-aspects.jar放在'WEB-INF/lib'中)載入spring-aspects.jar中的型別。如果spring-aspects.jar僅新增到容器級的 Classpath 中(並因此由共享的父類載入器載入),則所有 Web 應用程式都共享相同的方面例項(這可能不是您想要的)。

5.10.2. AspectJ 的其他 Spring 方面

除了@Configurable方面之外,spring-aspects.jar還包含一個 AspectJ 方面,您可以使用它來驅動 Spring 的事務 Management,該事務 Management 使用@Transactional註解 進行註解的型別和方法。這主要適用於希望在 Spring 容器之外使用 Spring Framework 的事務支援的使用者。

解釋@Transactional註解 的方面是AnnotationTransactionAspect。使用此方面時,必須註解實現類(或該類中的方法或兩者),而不是註解該類所實現的介面(如果有)。 AspectJ 遵循 Java 的規則,即不繼承介面上的 註解。

類上的@Transactional註解 指定用於執行該類中任何公共操作的預設事務語義。

類內方法上的@Transactional註解 將覆蓋類 註解(如果存在)給出的預設事務語義。可以標註任何可見性的方法,包括私有方法。直接註解非公共方法是執行此類方法而獲得事務劃分的唯一方法。

Tip

從 Spring Framework 4.2 開始,spring-aspects提供了類似的方面,為標準javax.transaction.Transactional註解 提供了完全相同的功能。檢查JtaAnnotationTransactionAspect瞭解更多詳細資訊。

對於希望使用 Spring 配置和事務 Management 支援但又不想(或不能)使用註解的 AspectJ 程式設計師,spring-aspects.jar還包含abstract個方面,您可以擴充套件它們以提供自己的切入點定義。有關更多資訊,請參見AbstractBeanConfigurerAspectAbstractTransactionAspect方面的資源。例如,以下摘錄顯示瞭如何編寫方面來使用與完全限定的類名匹配的原型 Bean 定義來配置域模型中定義的物件的所有例項:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {

    public DomainObjectConfiguration() {
        setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
    }

    // the creation of a new bean (any object in the domain model)
    protected pointcut beanCreation(Object beanInstance) :
        initialization(new(..)) &&
        SystemArchitecture.inDomainModel() &&
        this(beanInstance);

}

  

5.10.3. 使用 Spring IoC 配置 AspectJ Aspects

當您將 AspectJ 方面與 Spring 應用程式一起使用時,既自然又希望能夠使用 Spring 配置這些方面。 AspectJ 執行時本身負責方面的建立,並且通過 Spring 配置 AspectJ 建立的方面的方法取決於方面所使用的 AspectJ 例項化模型(per-xxx子句)。

AspectJ 的大多數方面都是單例方面。這些方面的配置很容易。您可以建立一個正常引用外觀型別幷包含factory-method="aspectOf"bean 屬性的 bean 定義。這可以確保 Spring 通過向 AspectJ 索要長寬比例項,而不是嘗試自己建立例項來獲得長寬比例項。下面的示例演示如何使用factory-method="aspectOf"屬性:

<bean id="profiler" class="com.xyz.profiler.Profiler"
        factory-method="aspectOf"> (1)

    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
  • (1)請注意factory-method="aspectOf"屬性

非單一方面很難配置。但是,可以通過建立原型 bean 定義並使用spring-aspects.jar@Configurable支援來配置方面例項(一旦 AspectJ 執行時建立了 bean)來實現。

如果您有一些要與 AspectJ 編織的@AspectJ 方面(例如,對域模型型別使用載入時編織)以及要與 Spring AOP 一起使用的其他@AspectJ 方面,那麼這些方面都已在 Spring 中配置,您需要告訴 Spring AOP @AspectJ 自動代理支援,應使用配置中定義的@AspectJ 方面的確切子集進行自動代理。您可以使用<aop:aspectj-autoproxy/>宣告中的一個或多個<include/>元素來完成此操作。每個<include/>元素都指定一個名稱模式,並且只有名稱與至少一個模式匹配的 bean 才可用於 Spring AOP 自動代理配置。以下示例顯示瞭如何使用<include/>元素:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

Note

不要被<aop:aspectj-autoproxy/>元素的名稱所迷惑。使用它可以建立 Spring AOP 代理。此處使用了@AspectJ 樣式的宣告,但是不涉及 AspectJ 執行時。