Spring AOP 詳解

Spring 有三大核心思想,其目的都是為了解耦。

我們日常開發中總能不知不覺用到其中兩種,分別是控制反轉(Inversion of Control, IOC)和依賴注入(Dependency Injection, DI)。

而面向切面編程(Aspect Oriented Programming, AOP)卻時常被人所忽略,但它的作用卻不可忽視。

AOP 的實現的目標是保證開發者在不修改原程式碼的前提下,去為系統中的業務組件添加某種通用功能。

在使用AOP時,我們能常常聽到以下術語:Aspect、Pointcut、Advice、JoinPoint、Weaving。

  • Pointcut:切點,表示一組JoinPoint,它定義了Advice將要發生的地方,
  • JoinPoint:連接點,表示程式執行過程中能夠插入切面的點,可以是方法的調用或異常的拋出。
  • Advice:增強,包括處理時機和處理內容。通俗的將就是什麼時候該做什麼事。
  • Aspect:切面,由同一類Pointcut和Advice組成。
  • Weaving:織入,就是通過動態代理,在目標對象中執行處理內容的過程。

如果術語描述的不太明白,請允許我在介紹AOP之前講一個故事:

在很久很久以前,有一個國家叫M國,其國力昌盛,但皇帝大限將至,便將皇位傳給太子。

不久後,皇帝駕崩,太子正式登基。但太子念其國力昌盛,整日無所作為,朝事荒廢。

十幾年後,邊境事犯,皇帝欲選取文武兼備的人率軍去平定叛亂,但此時朝廷人才寥落,只有一人能擔此任。

皇帝為了犒勞該將軍,在其出征之前,將其親子委以朝廷命官,輔佐在自己身邊。

經過數年苦戰,該將軍榮耀而歸,平定叛亂。皇帝高興萬分,賞其千金,封為萬戶侯。

經此事後,皇帝重整朝廷,不久後,國家便重回巔峰。

聽完如上故事,請思考並類比術語的含義:

Pointcut:文武兼備的人

JoinPoint:上文中的將軍

Advice:將軍出征前皇帝留任其親子在身邊、將軍回來後皇帝對其進行賞賜。

Aspect:文武兼備的人出征這個事件

Weaving:對文武兼備的人出征前後乾的事的過程。

本故事只能使你理解它這些術語的含義,並不能描述AOP的目標,也就是解耦,相信解耦大家都清除,這裡就不說了。

接下來開始詳細介紹AOP的概念以及使用,首先介紹Pointcut,下面是一段官方介紹:

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

上面的例子定義了一個名為’anyOldTransfer’的切入點,它將匹配任何名為’transfer’的方法的執行。

Spring AOP支援以下AspectJ切入點指示符(PCD),用於切入點表達式中:

切入點指示符 描述
execution 限制匹配連接點的方法
within 限制匹配連接點的包或者類
@within 限制匹配連接點的類帶有指定註解
arg 限制匹配連接點的參數類型
@args 限制匹配連接點的參數帶有指定註解
target 限制匹配連接點目標對象的類型
@target 與@within的功能類似,但註解的保留策略須為RUNTIME
this 限制匹配連接點的AOP代理類的類型
@annotation 限制匹配連接點的方法帶有指定的註解
bean 限制連接點是指定的Bean,或一組命名Bean(使用通配符時)

看完上述切點表達式不理解的話很正常,下面給出詳細介紹:

1、@Pointcut是創建切入點,切入點不用寫程式碼,返回類型為void。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
            throws-pattern?)
execution(方法修飾符(可選)  返回類型  類路徑 方法名  參數  異常模式(可選))
  • 修飾符匹配(modifiers-pattern?)
  • 返回值匹配(ret-type-pattern)
  • 類路徑匹配(declaring-type-pattern?)
  • 方法名匹配(name-pattern)
  • 參數匹配(param-pattern),可以指定具體參數類型,多個參數用”,”隔開。
  • 異常類型匹配(throws-pattern?)
  • 以上匹配中後面跟著”?”的表示是可選項。

多個匹配之間我們可以使用鏈接符 &&||來表示 「且」、「或」、「非」的關係。

但是在使用 XML 文件配置時,這些符號有特殊的含義,所以我們使用 「and」、「or」、「not」來表示。

示例:

// 匹配AccountService的任意方法
execution(* com.xyz.service.AccountService.*(..))
// 匹配服務包下的任意方法
execution(* com.xyz.service.*.*(..))
//匹配服務包或其子包下的任意方法
execution(* com.xyz.service..*.*(..))
// 匹配位於service包下任意類型
within(com.xyz.service.*)
// 匹配代理實現AccountSercice介面的任意類
this(com.xyz.service.AccountService)
// 匹配目標對象實現AccountService介面的任意類
target(com.xyz.service.AccountService)

2、Advice是增強通知,其有五種通知類型,分別如下:

  • @Before,在目標方法調用前執行
  • @After,在目標方法調用後執行
  • @AfterReturning,在目標方法返回後調用
  • @AfterThrowing,在目標方法拋出異常後調用
  • @Around,將目標方法封裝起來

 首先介紹一下@AfterReturning,官網帶參數示例介紹如下:

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

@Aspect
public class AfterReturningExample {

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

}

returning屬性中使用的名稱必須與通知方法中的參數名稱相對應。

當方法執行返回時,該返回值將作為相應的參數值傳遞到通知方法。

另外returning子句也限制了只能匹配返回指定類型的值,如果是Object類型,將可以匹配任何返回值。

 其次是@AfterThrowing的帶參數示例:

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屬性中使用的名稱必須和通知方法中的參數名稱相對應。

當方法拋出異常時,該異常將作為相應的參數傳遞給通知方法。

另外throwing子句也限制了只能匹配到指定異常類型。

接下來介紹@Around示例:

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

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

@Around可以在方法執行之前和之後工作,並能決定該方法何時執行以及如何調用。

該通知方法的第一個參數必須為ProceedingJoinPoint類型。

在通知方法中,調用ProceedingJoinPoint的proceed方法來執行匹配的方法。

proceed方法也可能被調用解析Object[]數組,它被用於匹配方法執行的參數。

通知方法返回的值將是匹配方法調用者看到的返回值。

最後,任何通知方法都可以將JoinPoint作為它的第一個參數,@Around除外,它第一個參數必須是ProceedingJoinPoint。

JoinPoint可以提供許多有用的參數,比如getArgs、getThis、getTarget等等。

目前我們已經看到了如何綁定返回值或異常值。如果要使參數用於通知接收,可以使用綁定形式的args。

如果在args表達式中使用參數名替代類型名稱,則在調用通知方法時,將相應參數的值作為參數值傳遞就可以了。

示例如下:

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

arg(account,..)切入點表達式有兩個作用,第一它將匹配限制為方法需要有至少一個參數。

第二它限制了參數的類型為Account,並且使該對象可以用於通知方法。

另外它也可以用如下方式表示:

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

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

代理對象、目標對象、和註解等都可以像如下示例使用:

首先定義一個註解:

@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();
    // ...
}

通知參數和泛型:(不適用於集合泛型)

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

通知方法中的參數綁定依賴於切入點表達式中使用的名稱。

因為參數名稱無法通過java反射活動,因此Sping AOP使用如下策略確定參數名稱:

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

如果通知方法第一個參數是JoinPoint,ProceedingJoinPoint等,則可以省去參數。

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

JoinPoint、ProceedingJoinPoint類型特別方便,可以通過它的getArgs方法獲取參數。

當多個通知都希望在同一連接點運行時,除非另外指定,否則執行順序是不確定的。

可以通過org.springframework.core.Ordered的Order註解來確定順序,值越低,優先順序越高。

3、引入(Introductions)

這個註解感覺用起來作用不大,就不介紹了。