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

前面寫過 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:

  • AspectJ 出身也是名門,來自於 Eclipse 基金會,link:https://www.eclipse.org/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 編程中的術語了,我們碰到一個說一個吧。

Spring AOP

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

後面介紹的如 @Aspect、@Pointcut、@Before、@After 等註解

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

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

  • Spring 1.2 基於接口的配置:最早的 Spring AOP 是完全基於幾個接口的,想看源碼的同學可以從這裡起步。

  • Spring 2.0 schema-based 配置:Spring 2.0 以後使用 XML 的方式來配置,使用 命名空間

  • Spring 2.0 @AspectJ 配置:使用註解的方式來配置,這種方式感覺是最方便的,還有,這裡雖然叫做 @AspectJ,但是這個和 AspectJ 其實沒啥關係。

Spring 1.2 中的配置

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

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

首先,我們先定義兩個接口 UserService 和 OrderService,以及它們的實現類 UserServiceImpl 和 OrderServiceImpl:

Spring AOP 使用介绍,从前世到今生
Spring AOP 使用介绍,从前世到今生

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

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

Spring AOP 使用介绍,从前世到今生

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

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

Spring AOP 使用介绍,从前世到今生

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

Spring AOP 使用介绍,从前世到今生

查看輸出結果:

準備執行方法: createUser, 參數列表:[Tom, Cruise, 55]

方法返回:User{firstName='Tom', lastName='Cruise', age=55, address=''}

準備執行方法: queryUser, 參數列表:

方法返回:User{firstName='Tom', lastName='Cruise', age=55, address=''}

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

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

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

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

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

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

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

Spring AOP 使用介绍,从前世到今生

注意,這裡的 mappedNames 配置是可以指定多個的,用逗號分隔,可以是不同類中的方法。

Spring AOP 使用介绍,从前世到今生

輸出結果如下,只有 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;

}

}

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

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

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

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

Spring AOP 使用介绍,从前世到今生

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

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

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

Spring AOP 使用介绍,从前世到今生

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

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

準備執行方法: createUser, 參數列表:[Tom, Cruise, 55]

方法返回:User{firstName='Tom', lastName='Cruise', age=55, address=''}

準備執行方法: queryUser, 參數列表:

方法返回:User{firstName='Tom', lastName='Cruise', age=55, address=''}

準備執行方法: createOrder, 參數列表:[Leo, 隨便買點什麼]

方法返回:Order{username='Leo', product='隨便買點什麼'}

準備執行方法: queryOrder, 參數列表:[Leo]

方法返回:Order{username='Leo', product='隨便買點什麼'}

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

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

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

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

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

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

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

Spring AOP 使用介绍,从前世到今生

然後我們運行一下:

Spring AOP 使用介绍,从前世到今生

輸出:

準備執行方法: createUser, 參數列表:[Tom, Cruise, 55]

方法返回:User{firstName='Tom', lastName='Cruise', age=55, address=''}

準備執行方法: createOrder, 參數列表:[Leo, 隨便買點什麼]

方法返回:Order{username='Leo', product='隨便買點什麼'}

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

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

Spring 2.0 @AspectJ 配置

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

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

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

org.aspectj

aspectjweaver

1.8.11

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

org.springframework.boot

spring-boot-starter-aop

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

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

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

1、在 xml 中配置:

2、使用 @EnableAspectJAutoProxy

@Configuration

@EnableAspectJAutoProxy

public class AppConfig {


}

親測,好像不開啟也是可以的,測試了 Spring 3.2-5.0,這我就不太理解了,難道是我的環境問題。

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

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

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

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 配置方式中的 Pointcut 和 Advice 的配置。對於開發者來說,其實最重要的就是這兩個了,定義 Pointcut 和使用合適的 Advice 在各個 Pointcut 上。

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

Spring AOP 使用介绍,从前世到今生

xml 的配置非常簡單:

Spring AOP 使用介绍,从前世到今生

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

測試一下:

Spring AOP 使用介绍,从前世到今生

輸出結果:

方法執行前,打印入參:[Tom, Cruise, 55]

User{firstName='Tom', lastName='Cruise', age=55, address=''}

方法執行前,打印入參:

User{firstName='Tom', lastName='Cruise', age=55, address=''}

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

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

Spring 2.0 schema-based 配置

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

介紹 IOC 的時候也介紹過 Spring 是怎麼解析各個命名空間的(各種 *NamespaceHandler),你要的源碼在 org.springframework.aop.config.AopNamespaceHandler 中。

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

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

...


...

中需要指定一個 bean,和前面介紹的 LogArgsAspect 和 LogResultAspect 一樣,我們知道該 bean 中我們需要寫處理代碼。


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

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


expression="execution(* com.javadoop.springaoplearning.service.*.*(..))"/>


expression="com.javadoop.SystemArchitecture.businessService"/>


作為 的直接子元素,將作為全局 Pointcut。

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

expression="com.javadoop.SystemArchitecture.businessService" />

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

Spring AOP 使用介绍,从前世到今生

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

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

小結

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

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

附錄

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

建議讀者 clone 下來以後,通過命令行進行測試,而不是依賴於 IDE:

1. mvn clean package

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

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


分享到:


相關文章: