Spring AOP 使用介紹,從前世到今生

JavaDoop發表於2018-10-24

前面寫過 Spring IOC 的原始碼分析,很多讀者希望可以出一個 Spring AOP 的原始碼分析,不過 Spring AOP 的原始碼還是比較多的,寫出來不免篇幅會大些。

本文不介紹原始碼分析,而是介紹 Spring AOP 中的一些概念,以及它的各種配置方法,涵蓋了 Spring AOP 發展到現在出現的全部 3 種配置方式。

由於 Spring 強大的向後相容性,實際程式碼中往往會出現很多配置混雜的情況,而且居然還能工作,本文希望幫助大家理清楚這些知識。

本文使用的測試原始碼已上傳到 Github: hongjiev/spring-aop-learning

AOP, AspectJ, Spring AOP

我們先來把它們的概念和關係說說清楚。

AOP 要實現的是在我們原來寫的程式碼的基礎上,進行一定的包裝,如在方法執行前、方法返回後、方法丟擲異常後等地方進行一定的攔截處理或者叫增強處理。

AOP 的實現並不是因為 Java 提供了什麼神奇的鉤子,可以把方法的幾個生命週期告訴我們,而是我們要實現一個代理,實際執行的例項其實是生成的代理類的例項。

作為 Java 開發者,我們都很熟悉 AspectJ 這個詞,甚至於我們提到 AOP 的時候,想到的往往就是 AspectJ,即使你可能不太懂它是怎麼工作的。這裡,我們把 AspectJ 和 Spring AOP 做個簡單的對比:

Spring AOP:

  • 它基於動態代理來實現。預設地,如果使用介面的,用 JDK 提供的動態代理實現,如果沒有介面,使用 CGLIB 實現。大家一定要明白背後的意思,包括什麼時候會不用 JDK 提供的動態代理,而用 CGLIB 實現。

  • Spring 3.2 以後,spring-core 直接就把 CGLIB 和 ASM 的原始碼包括進來了,這也是為什麼我們不需要顯式引入這兩個依賴

  • Spring 的 IOC 容器和 AOP 都很重要,Spring AOP 需要依賴於 IOC 容器來管理。
  • 如果你是 web 開發者,有些時候,你可能需要的是一個 Filter 或一個 Interceptor,而不一定是 AOP。
  • Spring AOP 只能作用於 Spring 容器中的 Bean,它是使用純粹的 Java 程式碼實現的,只能作用於 bean 的方法。
  • Spring 提供了 AspectJ 的支援,後面我們會單獨介紹怎麼使用,一般來說我們用純的 Spring AOP 就夠了。
  • 很多人會對比 Spring AOP 和 AspectJ 的效能,Spring AOP 是基於代理實現的,在容器啟動的時候需要生成代理例項,在方法呼叫上也會增加棧的深度,使得 Spring AOP 的效能不如 AspectJ 那麼好。

AspectJ:

  • 屬於靜態織入,它是通過修改程式碼來實現的,它的織入時機可以是:
    • Compile-time weaving:編譯期織入,如類 A 使用 AspectJ 新增了一個屬性,類 B 引用了它,這個場景就需要編譯期的時候就進行織入,否則沒法編譯類 B。
    • Post-compile weaving:也就是已經生成了 .class 檔案,或已經打成 jar 包了,這種情況我們需要增強處理的話,就要用到編譯後織入。
    • Load-time weaving:指的是在載入類的時候進行織入,要實現這個時期的織入,有幾種常見的方法。1、自定義類載入器來幹這個,這個應該是最容易想到的辦法,在被織入類載入到 JVM 前去對它進行載入,這樣就可以在載入的時候定義行為了。2、在 JVM 啟動的時候指定 AspectJ 提供的 agent:-javaagent:xxx/xxx/aspectjweaver.jar
  • AspectJ 能幹很多 Spring AOP 幹不了的事情,它是 AOP 程式設計的完全解決方案。Spring AOP 致力於解決的是企業級開發中最普遍的 AOP 需求(方法織入),而不是力求成為一個像 AspectJ 一樣的 AOP 程式設計完全解決方案。

  • 因為 AspectJ 在實際程式碼執行前完成了織入,所以大家會說它生成的類是沒有額外執行時開銷的。

  • 很快我會專門寫一篇文章介紹 AspectJ 的使用,以及怎麼在 Spring 應用中使用 AspectJ。

AOP 術語解釋

在這裡,不準備解釋那麼多 AOP 程式設計中的術語了,我們碰到一個說一個吧。

Advice、Advisor、Pointcut、Aspect、Joinpoint 等等。

Spring AOP

首先要說明的是,這裡介紹的 Spring AOP 是純的 Spring 程式碼,和 AspectJ 沒什麼關係,但是 Spring 延用了 AspectJ 中的概念,包括使用了 AspectJ 提供的 jar 包中的註解,但是不依賴於其實現功能。

後面介紹的如 @Aspect、@Pointcut、@Before、@After 等註解都是來自於 AspectJ,但是功能的實現是純 Spring AOP 自己實現的。

下面我們來介紹 Spring AOP 的使用方法,先從最簡單的配置方式開始說起,這樣讀者想看原始碼也會比較容易。

目前 Spring AOP 一共有三種配置方式,Spring 做到了很好地向下相容,所以大家可以放心使用。

  • Spring 1.2 基於介面的配置:最早的 Spring AOP 是完全基於幾個介面的,想看原始碼的同學可以從這裡起步。
  • Spring 2.0 schema-based 配置:Spring 2.0 以後使用 XML 的方式來配置,使用 名稱空間 <aop />
  • Spring 2.0 @AspectJ 配置:使用註解的方式來配置,這種方式感覺是最方便的,還有,這裡雖然叫做 @AspectJ,但是這個和 AspectJ 其實沒啥關係。

Spring 1.2 中的配置

這節我們將介紹 Spring 1.2 中的配置,這是最古老的配置,但是由於 Spring 提供了很好的向後相容,以及很多人根本不知道什麼配置是什麼版本的,以及是否有更新更好的配置方法替代,所以還是會有很多程式碼是採用這種古老的配置方式的,這裡說的古老並沒有貶義的意思。

下面用一個簡單的例子來演示怎麼使用 Spring 1.2 的配置方式。

首先,我們先定義兩個介面 UserServiceOrderService,以及它們的實現類 UserServiceImplOrderServiceImpl

3

3

接下來,我們定義兩個 advice,分別用於攔截方法執行前方法返回後

advice 是我們接觸的第一個概念,記住它是幹什麼用的。

4

上面的兩個 Advice 分別用於方法呼叫前輸出引數和方法呼叫後輸出結果。

現在可以開始配置了,我們配置一個名為 spring_1_2.xml 的檔案:

5

接下來,我們跑起來看看:

6

檢視輸出結果:

準備執行方法: createUser, 引數列表:[Tom, Cruise, 55]
方法返回:User{firstName='Tom', lastName='Cruise', age=55, address='null'}
準備執行方法: queryUser, 引數列表:[]
方法返回:User{firstName='Tom', lastName='Cruise', age=55, address='null'}
複製程式碼

從結果可以看到,對 UserService 中的兩個方法都做了前、後攔截。這個例子理解起來應該非常簡單,就是一個代理實現。

代理模式需要一個介面、一個具體實現類,然後就是定義一個代理類,用來包裝實現類,新增自定義邏輯,在使用的時候,需要用代理類來生成例項。

此中方法有個致命的問題,如果我們需要攔截 OrderService 中的方法,那麼我們還需要定義一個 OrderService 的代理。如果還要攔截 PostService,得定義一個 PostService 的代理......

而且,我們看到,我們的攔截器的粒度只控制到了類級別,類中所有的方法都進行了攔截。接下來,我們看看怎麼樣只攔截特定的方法

在上面的配置中,配置攔截器的時候,interceptorNames 除了指定為 Advice,是還可以指定為 Interceptor 和 Advisor 的。

這裡我們來理解 Advisor 的概念,它也比較簡單,它內部需要指定一個 Advice,Advisor 決定該攔截哪些方法,攔截後需要完成的工作還是內部的 Advice 來做。

它有好幾個實現類,這裡我們使用實現類 NameMatchMethodPointcutAdvisor 來演示,從名字上就可以看出來,它需要我們給它提供方法名字,這樣符合該配置的方法才會做攔截。

1

我們可以看到,userServiceProxy 這個 bean 配置了一個 advisor,advisor 內部有一個 advice。advisor 負責匹配方法,內部的 advice 負責實現方法包裝。

注意,這裡的 mappedNames 配置是可以指定多個的,用逗號分隔,可以是不同類中的方法。相比直接指定 advice,advisor 實現了更細粒度的控制,因為在這裡配置 advice 的話,所有方法都會被攔截。

2

輸出結果如下,只有 createUser 方法被攔截:

準備執行方法: createUser, 引數列表:[Tom, Cruise, 55]
複製程式碼

到這裡,我們已經瞭解了 AdviceAdvisor 了,前面也說了還可以配置 Interceptor

對於 Java 開發者來說,對 Interceptor 這個概念肯定都很熟悉了,這裡就不做演示了,貼一下實現程式碼:

public class DebugInterceptor implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Before: invocation=[" + invocation + "]");
        // 執行 真實實現類 的方法
        Object rval = invocation.proceed();
        System.out.println("Invocation returned");
        return rval;
    }
}
複製程式碼

上面,我們介紹完了 Advice、Advisor、Interceptor 三個概念,相信大家應該很容易就看懂它們了。

它們有個共同的問題,那就是我們得為每個 bean 都配置一個代理,之後獲取 bean 的時候需要獲取這個代理類的 bean 例項(如 (UserService) context.getBean("userServiceProxy")),這顯然非常不方便,不利於我們之後要使用的自動根據型別注入。下面介紹 autoproxy 的解決方案。

autoproxy:從名字我們也可以看出來,它是實現自動代理,也就是說當 Spring 發現一個 bean 需要被切面織入的時候,Spring 會自動生成這個 bean 的一個代理來攔截方法的執行,確保定義的切面能被執行。

這裡強調自動,也就是說 Spring 會自動做這件事,而不用像前面介紹的,我們需要顯式地指定代理類的 bean。

我們去掉原來的 ProxyFactoryBean 的配置,改為使用 BeanNameAutoProxyCreator 來配置:

9

配置很簡單,beanNames 中可以使用正則來匹配 bean 的名字。這樣配置出來以後,userServiceBeforeAdvice 和 userServiceAfterAdvice 這兩個攔截器就不僅僅可以作用於 UserServiceImpl 了,也可以作用於 OrderServiceImpl、PostServiceImpl、ArticleServiceImpl......等等,也就是說不再是配置某個 bean 的代理了。

注意,這裡的 InterceptorNames 和前面一樣,也是可以配置成 Advisor 和 Interceptor 的。

然後我們修改下使用的地方:

10

發現沒有,我們在使用的時候,完全不需要關心代理了,直接使用原來的型別就可以了,這是非常方便的。

輸出結果就是 OrderService 和 UserService 中的每個方法都得到了攔截:

準備執行方法: createUser, 引數列表:[Tom, Cruise, 55]
方法返回:User{firstName='Tom', lastName='Cruise', age=55, address='null'}
準備執行方法: queryUser, 引數列表:[]
方法返回:User{firstName='Tom', lastName='Cruise', age=55, address='null'}
準備執行方法: createOrder, 引數列表:[Leo, 隨便買點什麼]
方法返回:Order{username='Leo', product='隨便買點什麼'}
準備執行方法: queryOrder, 引數列表:[Leo]
方法返回:Order{username='Leo', product='隨便買點什麼'}
複製程式碼

到這裡,是不是發現 BeanNameAutoProxyCreator 非常好用,它需要指定被攔截類名的模式(如 *ServiceImpl),它可以配置多次,這樣就可以用來匹配不同模式的類了。

另外,在 BeanNameAutoProxyCreator 同一個包中,還有一個非常有用的類 DefaultAdvisorAutoProxyCreator,比上面的 BeanNameAutoProxyCreator 還要方便。

之前我們說過,advisor 內部包裝了 advice,advisor 負責決定攔截哪些方法,內部 advice 定義攔截後的邏輯。所以,仔細想想其實就是隻要讓我們的 advisor 全域性生效就能實現我們需要的自定義攔截功能、攔截後的邏輯處理。

BeanNameAutoProxyCreator 是自己匹配方法,然後交由內部配置 advice 來攔截處理;

而 DefaultAdvisorAutoProxyCreator 是讓 ioc 容器中的所有 advisor 來匹配方法,advisor 內部都是有 advice 的,讓它們內部的 advice 來執行攔截處理。

1、我們需要再回頭看下 Advisor 的配置,上面我們用了 NameMatchMethodPointcutAdvisor 這個類:

<bean id="logCreateAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
    <property name="advice" ref="logArgsAdvice" />
    <property name="mappedNames" value="createUser,createOrder" />
</bean>
複製程式碼

其實 Advisor 還有一個更加靈活的實現類 RegexpMethodPointcutAdvisor,它能實現正則匹配,如:

<bean id="logArgsAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <property name="advice" ref="logArgsAdvice" />
    <property name="pattern" value="com.javadoop.*.service.*.create.*" />
</bean>
複製程式碼

也就是說,我們能通過配置 Advisor,精確定位到需要被攔截的方法,然後使用內部的 Advice 執行邏輯處理。

2、之後,我們需要配置 DefaultAdvisorAutoProxyCreator,它的配置非常簡單,直接使用下面這段配置就可以了,它就會使得所有的 Advisor 自動生效,無須其他配置。

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
複製程式碼

11

然後我們執行一下:

12

輸出:

準備執行方法: createUser, 引數列表:[Tom, Cruise, 55]
方法返回:User{firstName='Tom', lastName='Cruise', age=55, address='null'}
準備執行方法: createOrder, 引數列表:[Leo, 隨便買點什麼]
方法返回:Order{username='Leo', product='隨便買點什麼'}
複製程式碼

從結果可以看出,create

方法使用了 logArgsAdvisor 進行傳參輸出,query
方法使用了 logResultAdvisor 進行了返回結果輸出。

到這裡,Spring 1.2 的配置就要介紹完了。本文不會介紹得面面俱到,主要是關注最核心的配置,如果讀者感興趣,要學會自己去摸索,比如這裡的 Advisor 就不只有我這裡介紹的 NameMatchMethodPointcutAdvisor 和 RegexpMethodPointcutAdvisor,AutoProxyCreator 也不僅僅是 BeanNameAutoProxyCreator 和 DefaultAdvisorAutoProxyCreator。

讀到這裡,我想對於很多人來說,就知道怎麼去閱讀 Spring AOP 原始碼了。

Spring 2.0 @AspectJ 配置

Spring 2.0 以後,引入了 @AspectJ 和 Schema-based 的兩種配置方式,我們先來介紹 @AspectJ 的配置方式,之後我們再來看使用 xml 的配置方式。

注意了,@AspectJ 和 AspectJ 沒多大關係,並不是說基於 AspectJ 實現的,而僅僅是使用了 AspectJ 中的概念,包括使用的註解也是直接來自於 AspectJ 的包。

首先,我們需要依賴 aspectjweaver.jar 這個包,這個包來自於 AspectJ:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.11</version>
</dependency>
複製程式碼

如果是使用 Spring Boot 的話,新增以下依賴即可:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
複製程式碼

在 @AspectJ 的配置方式中,之所以要引入 aspectjweaver 並不是因為我們需要使用 AspectJ 的處理功能,而是因為 Spring 使用了 AspectJ 提供的一些註解,實際上還是純的 Spring AOP 程式碼

說了這麼多,明確一點,@AspectJ 採用註解的方式來配置使用 Spring AOP。

首先,我們需要開啟 @AspectJ 的註解配置方式,有兩種方式:

1、在 xml 中配置:

<aop:aspectj-autoproxy/>
複製程式碼
  1. 使用 @EnableAspectJAutoProxy
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
複製程式碼

一旦開啟了上面的配置,那麼所有使用 @Aspect 註解的 bean 都會被 Spring 當做用來實現 AOP 的配置類,我們稱之為一個 Aspect

注意了,@Aspect 註解要作用在 bean 上面,不管是使用 @Component 等註解方式,還是在 xml 中配置 bean,首先它需要是一個 bean。

比如下面這個 bean,它的類名上使用了 @Aspect,它就會被當做 Spring AOP 的配置。

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of aspect here as normal -->
</bean>
複製程式碼
package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}
複製程式碼

接下來,我們需要關心的是 @Aspect 註解的 bean 中,我們需要配置哪些內容。

首先,我們需要配置 Pointcut,Pointcut 在大部分地方被翻譯成切點,用於定義哪些方法需要被增強或者說需要被攔截,有點類似於之前介紹的 Advisor 的方法匹配。

Spring AOP 只支援 bean 中的方法(不像 AspectJ 那麼強大),所以我們可以認為 Pointcut 就是用來匹配 Spring 容器中的所有 bean 的方法的。

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
複製程式碼

我們看到,@Pointcut 中使用了 execution 來正則匹配方法簽名,這也是最常用的,除了 execution,我們再看看其他的幾個比較常用的匹配方式:

  • within:指定所在類或所在包下面的方法(Spring AOP 獨有)

    如 @Pointcut("within(com.javadoop.springaoplearning.service..*)")

  • @annotation:方法上具有特定的註解,如 @Subscribe 用於訂閱特定的事件。

    如 @Pointcut("execution(

    .*(..)) && @annotation(com.javadoop.annotation.Subscribe)")

  • bean(idOrNameOfBean):匹配 bean 的名字(Spring AOP 獨有)

    如 @Pointcut("bean(*Service)")

Tips:上面匹配中,通常 "." 代表一個包名,".." 代表包及其子包,方法引數任意匹配使用兩個點 ".."。

對於 web 開發者,Spring 有個很好的建議,就是定義一個 SystemArchitecture

@Aspect
public class SystemArchitecture {

    // web 層
    @Pointcut("within(com.javadoop.web..*)")
    public void inWebLayer() {}

    // service 層
    @Pointcut("within(com.javadoop.service..*)")
    public void inServiceLayer() {}

    // dao 層
    @Pointcut("within(com.javadoop.dao..*)")
    public void inDataAccessLayer() {}

    // service 實現,注意這裡指的是方法實現,其實通常也可以使用 bean(*ServiceImpl)
    @Pointcut("execution(* com.javadoop..service.*.*(..))")
    public void businessService() {}

    // dao 實現
    @Pointcut("execution(* com.javadoop.dao.*.*(..))")
    public void dataAccessOperation() {}
}
複製程式碼

上面這個 SystemArchitecture 很好理解,該 Aspect 定義了一堆的 Pointcut,隨後在任何需要 Pointcut 的地方都可以直接引用(如 xml 中的 pointcut-ref="")。

配置 pointcut 就是配置我們需要攔截哪些方法,接下來,我們要配置需要對這些被攔截的方法做什麼,也就是前面介紹的 Advice。

接下來,我們要配置 Advice。

下面這塊程式碼示例了各種常用的情況:

注意,實際寫程式碼的時候,不要把所有的切面都揉在一個 class 中。

@Aspect
public class AdviceExample {

    // 這裡會用到我們前面說的 SystemArchitecture
    // 下面方法就是寫攔截 "dao層實現"
    @Before("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ... 實現程式碼
    }

    // 當然,我們也可以直接"內聯"Pointcut,直接在這裡定義 Pointcut
    // 把 Advice 和 Pointcut 合在一起了,但是這兩個概念我們還是要區分清楚的
    @Before("execution(* com.javadoop.dao.*.*(..))")
    public void doAccessCheck() {
        // ... 實現程式碼
    }

    @AfterReturning("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

    @AfterReturning(
        pointcut="com.javadoop.aop.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // 這樣,進來這個方法的處理時候,retVal 就是相應方法的返回值,是不是非常方便
        //  ... 實現程式碼
    }

    // 異常返回
    @AfterThrowing("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ... 實現程式碼
    }

    @AfterThrowing(
        pointcut="com.javadoop.aop.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ... 實現程式碼
    }

    // 注意理解它和 @AfterReturning 之間的區別,這裡會攔截正常返回和異常的情況
    @After("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // 通常就像 finally 塊一樣使用,用來釋放資源。
        // 無論正常返回還是異常退出,都會被攔截到
    }

    // 感覺這個很有用吧,既能做 @Before 的事情,也可以做 @AfterReturning 的事情
    @Around("com.javadoop.aop.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}
複製程式碼

細心的讀者可能發現了有些 Advice 缺少方法傳參,如在 @Before 場景中引數往往是非常有用的,比如我們要用日誌記錄下來被攔截方法的入參情況。

Spring 提供了非常簡單的獲取入參的方法,使用 org.aspectj.lang.JoinPoint 作為 Advice 的第一個引數即可,如:

@Before("com.javadoop.springaoplearning.aop_spring_2_aspectj.SystemArchitecture.businessService()")
public void logArgs(JoinPoint joinPoint) {
    System.out.println("方法執行前,列印入參:" + Arrays.toString(joinPoint.getArgs()));
}
複製程式碼

注意:第一,必須放置在第一個引數上;第二,如果是 @Around,我們通常會使用其子類 ProceedingJoinPoint,因為它有 procceed()/procceed(args[]) 方法。

到這裡,我們介紹完了 @AspectJ 配置方式中的 PointcutAdvice 的配置。對於開發者來說,其實最重要的就是這兩個了,定義 Pointcut 和使用合適的 Advice 在各個 Pointcut 上。

下面,我們用這一節介紹的 @AspectJ 來實現上一節實現的記錄方法傳參記錄方法返回值

13

xml 的配置非常簡單:

14

這裡是示例,所以 bean 的配置還是使用了 xml 的配置方式。

測試一下:

15

輸出結果:

方法執行前,列印入參:[Tom, Cruise, 55]
User{firstName='Tom', lastName='Cruise', age=55, address='null'}
方法執行前,列印入參:[]
User{firstName='Tom', lastName='Cruise', age=55, address='null'}
複製程式碼

JoinPoint 除了 getArgs() 外還有一些有用的方法,大家可以進去稍微看一眼。

最後提一點,@Aspect 中的配置不會作用於使用 @Aspect 註解的 bean。

Spring 2.0 schema-based 配置

本節將介紹的是 Spring 2.0 以後提供的基於 <aop /> 名稱空間的 XML 配置。這裡說的 schema-based 就是指基於 aop這個 schema。

介紹 IOC 的時候也介紹過 Spring 是怎麼解析各個名稱空間的(各種 *NamespaceHandler),解析 <aop /> 的原始碼在 org.springframework.aop.config.AopNamespaceHandler 中。

有了前面的 @AspectJ 的配置方式的知識,理解 xml 方式的配置非常簡單,所以我們就可以廢話少一點了。

這裡先介紹配置 Aspect,便於後續理解:

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

<bean id="aBean" class="...">
    ...
</bean>
複製程式碼

所有的配置都在 <aop:config> 下面。

<aop:aspect > 中需要指定一個 bean,和前面介紹的 LogArgsAspect 和 LogResultAspect 一樣,我們知道該 bean 中我們需要寫處理程式碼。

然後,我們寫好 Aspect 程式碼後,將其“織入”到合適的 Pointcut 中,這就是面向切面。

然後,我們需要配置 Pointcut,非常簡單,如下:

<aop:config>

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

    <!--也可以像下面這樣-->
    <aop:pointcut id="businessService2"
        expression="com.javadoop.SystemArchitecture.businessService()"/>

</aop:config>
複製程式碼

<aop:pointcut> 作為 <aop:config> 的直接子元素,將作為全域性 Pointcut。

我們也可以在 <aop:aspect />內部配置 Pointcut,這樣該 Pointcut 僅用於該 Aspect:

<aop:config>
    <aop:aspect ref="logArgsAspect">
        <aop:pointcut id="internalPointcut"
                expression="com.javadoop.SystemArchitecture.businessService()" />
    </aop:aspect>
</aop:config>
複製程式碼

接下來,我們應該配置 Advice 了,為了避免廢話過多,我們直接上例項吧,非常好理解,將上一節用 @AspectJ 方式配置的搬過來:

16

上面的例子中,我們配置了兩個 LogArgsAspect 和一個 LogResultAspect。

其實基於 XML 的配置也是非常靈活的,這裡沒辦法給大家演示各種搭配,大家抓住基本的 Pointcut、Advice 和 Aspect 這幾個概念,就很容易配置了。

小結

到這裡,本文介紹了 Spring AOP 的三種配置方式,我們要知道的是,到目前為止,我們使用的都是 Spring AOP,和 AspectJ 沒什麼關係。

下一篇文章,將會介紹 AspectJ 的使用方式,以及怎樣在 Spring 應用中使用 AspectJ。之後差不多就可以出 Spring AOP 原始碼分析了。

附錄

本文使用的測試原始碼已上傳到 Github: hongjiev/spring-aop-learning

建議讀者 clone 下來以後,通過命令列進行測試,而不是依賴於 IDE,因為 IDE 太"智慧"了:

  1. mvn clean package

  2. java -jar target/spring-aop-learning-1.0-jar-with-dependencies.jar

    pom.xml 中配置了 assembly 外掛,打包的時候會將所有 jar 包依賴打到一起。

  3. 修改 Application.java 中的程式碼,或者其他程式碼,然後重複 1 和 2

(全文完)


相關文章