前面寫過 Spring IOC 的源碼分析,不少讀者但願能夠出一個 Spring AOP 的源碼分析,不過 Spring AOP 的源碼仍是比較多的,寫出來難免篇幅會大些。java
本文不介紹源碼分析,而是介紹 Spring AOP 中的一些概念,以及它的各類配置方法,涵蓋了 Spring AOP 發展到如今出現的所有 3 種配置方式。git
因爲 Spring 強大的向後兼容性,實際代碼中每每會出現不少配置混雜的狀況,並且竟然還能工做,本文但願幫助你們理清楚這些知識。github
本文使用的測試源碼已上傳到 Github: hongjiev/spring-aop-learning。web
咱們先來把它們的概念和關係說說清楚。spring
AOP 要實現的是在咱們原來寫的代碼的基礎上,進行必定的包裝,如在方法執行前、方法返回後、方法拋出異常後等地方進行必定的攔截處理或者叫加強處理。express
AOP 的實現並非由於 Java 提供了什麼神奇的鉤子,能夠把方法的幾個生命週期告訴咱們,而是咱們要實現一個代理,實際運行的實例實際上是生成的代理類的實例。編程
做爲 Java 開發者,咱們都很熟悉 AspectJ 這個詞,甚至於咱們提到 AOP 的時候,想到的每每就是 AspectJ,即便你可能不太懂它是怎麼工做的。這裏,咱們把 AspectJ 和 Spring AOP 作個簡單的對比:bash
Spring AOP:app
它基於動態代理來實現。默認地,若是使用接口的,用 JDK 提供的動態代理實現,若是沒有接口,使用 CGLIB 實現。你們必定要明白背後的意思,包括何時會不用 JDK 提供的動態代理,而用 CGLIB 實現。eclipse
Spring 3.2 之後,spring-core 直接就把 CGLIB 和 ASM 的源碼包括進來了,這也是爲何咱們不須要顯式引入這兩個依賴
AspectJ:
-javaagent:xxx/xxx/aspectjweaver.jar
。AspectJ 能幹不少 Spring AOP 幹不了的事情,它是 AOP 編程的徹底解決方案。Spring AOP 致力於解決的是企業級開發中最廣泛的 AOP 需求(方法織入),而不是力求成爲一個像 AspectJ 同樣的 AOP 編程徹底解決方案。
由於 AspectJ 在實際代碼運行前完成了織入,因此你們會說它生成的類是沒有額外運行時開銷的。
在這裏,不許備解釋那麼多 AOP 編程中的術語了,咱們碰到一個說一個吧。
Advice、Advisor、Pointcut、Aspect、Joinpoint 等等。
首先要說明的是,這裏介紹的 Spring AOP 是純的 Spring 代碼,和 AspectJ 沒什麼關係,可是 Spring 延用了 AspectJ 中的概念,包括使用了 AspectJ 提供的 jar 包中的註解,可是不依賴於其實現功能。
後面介紹的如 @Aspect、@Pointcut、@Before、@After 等註解都是來自於 AspectJ,可是功能的實現是純 Spring AOP 本身實現的。
下面咱們來介紹 Spring AOP 的使用方法,先從最簡單的配置方式開始提及,這樣讀者想看源碼也會比較容易。
目前 Spring AOP 一共有三種配置方式,Spring 作到了很好地向下兼容,因此你們能夠放心使用。
<aop />
@AspectJ
,可是這個和 AspectJ 其實沒啥關係。這節咱們將介紹 Spring 1.2 中的配置,這是最古老的配置,可是因爲 Spring 提供了很好的向後兼容,以及不少人根本不知道什麼配置是什麼版本的,以及是否有更新更好的配置方法替代,因此仍是會有不少代碼是採用這種古老的配置方式的,這裏說的古老並無貶義的意思。
下面用一個簡單的例子來演示怎麼使用 Spring 1.2 的配置方式。
首先,咱們先定義兩個接口 UserService
和 OrderService
,以及它們的實現類 UserServiceImpl
和 OrderServiceImpl
:
接下來,咱們定義兩個 advice,分別用於攔截方法執行前和方法返回後:
advice 是咱們接觸的第一個概念,記住它是幹什麼用的。
上面的兩個 Advice 分別用於方法調用前輸出參數和方法調用後輸出結果。
如今能夠開始配置了,咱們配置一個名爲 spring_1_2.xml 的文件:
接下來,咱們跑起來看看:
查看輸出結果:
準備執行方法: 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 來演示,從名字上就能夠看出來,它須要咱們給它提供方法名字,這樣符合該配置的方法纔會作攔截。
咱們能夠看到,userServiceProxy 這個 bean 配置了一個 advisor,advisor 內部有一個 advice。advisor 負責匹配方法,內部的 advice 負責實現方法包裝。
注意,這裏的 mappedNames 配置是能夠指定多個的,用逗號分隔,能夠是不一樣類中的方法。相比直接指定 advice,advisor 實現了更細粒度的控制,由於在這裏配置 advice 的話,全部方法都會被攔截。
輸出結果以下,只有 createUser 方法被攔截:
準備執行方法: createUser, 參數列表:[Tom, Cruise, 55]
複製代碼
到這裏,咱們已經瞭解了 Advice 和 Advisor 了,前面也說了還能夠配置 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 來配置:
配置很簡單,beanNames 中可使用正則來匹配 bean 的名字。這樣配置出來之後,userServiceBeforeAdvice 和 userServiceAfterAdvice 這兩個攔截器就不只僅能夠做用於 UserServiceImpl 了,也能夠做用於 OrderServiceImpl、PostServiceImpl、ArticleServiceImpl......等等,也就是說再也不是配置某個 bean 的代理了。
注意,這裏的 InterceptorNames 和前面同樣,也是能夠配置成 Advisor 和 Interceptor 的。
而後咱們修改下使用的地方:
發現沒有,咱們在使用的時候,徹底不須要關心代理了,直接使用原來的類型就能夠了,這是很是方便的。
輸出結果就是 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 來執行攔截處理。
一、咱們須要再回頭看下 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 執行邏輯處理。
二、以後,咱們須要配置 DefaultAdvisorAutoProxyCreator,它的配置很是簡單,直接使用下面這段配置就能夠了,它就會使得全部的 Advisor 自動生效,無須其餘配置。
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
複製代碼
而後咱們運行一下:
輸出:
準備執行方法: createUser, 參數列表:[Tom, Cruise, 55]
方法返回:User{firstName='Tom', lastName='Cruise', age=55, address='null'}
準備執行方法: createOrder, 參數列表:[Leo, 隨便買點什麼]
方法返回:Order{username='Leo', product='隨便買點什麼'}
複製代碼
從結果能夠看出,create
到這裏,Spring 1.2 的配置就要介紹完了。本文不會介紹得面面俱到,主要是關注最核心的配置,若是讀者感興趣,要學會本身去摸索,好比這裏的 Advisor 就不僅有我這裏介紹的 NameMatchMethodPointcutAdvisor 和 RegexpMethodPointcutAdvisor,AutoProxyCreator 也不只僅是 BeanNameAutoProxyCreator 和 DefaultAdvisorAutoProxyCreator。
讀到這裏,我想對於不少人來講,就知道怎麼去閱讀 Spring AOP 源碼了。
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 的註解配置方式,有兩種方式:
一、在 xml 中配置:
<aop:aspectj-autoproxy/>
複製代碼
@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 配置方式中的 Pointcut 和 Advice 的配置。對於開發者來講,其實最重要的就是這兩個了,定義 Pointcut 和使用合適的 Advice 在各個 Pointcut 上。
下面,咱們用這一節介紹的 @AspectJ 來實現上一節實現的記錄方法傳參和記錄方法返回值。
xml 的配置很是簡單:
這裏是示例,因此 bean 的配置仍是使用了 xml 的配置方式。
測試一下:
輸出結果:
方法執行前,打印入參:[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 之後提供的基於 <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 方式配置的搬過來:
上面的例子中,咱們配置了兩個 LogArgsAspect 和一個 LogResultAspect。
其實基於 XML 的配置也是很是靈活的,這裏沒辦法給你們演示各類搭配,你們抓住基本的 Pointcut、Advice 和 Aspect 這幾個概念,就很容易配置了。
到這裏,本文介紹了 Spring AOP 的三種配置方式,咱們要知道的是,到目前爲止,咱們使用的都是 Spring AOP,和 AspectJ 沒什麼關係。
下一篇文章,將會介紹 AspectJ 的使用方式,以及怎樣在 Spring 應用中使用 AspectJ。以後差很少就能夠出 Spring AOP 源碼分析了。
本文使用的測試源碼已上傳到 Github: hongjiev/spring-aop-learning。
建議讀者 clone 下來之後,經過命令行進行測試,而不是依賴於 IDE,由於 IDE 太"智能"了:
mvn clean package
java -jar target/spring-aop-learning-1.0-jar-with-dependencies.jar
pom.xml 中配置了 assembly 插件,打包的時候會將全部 jar 包依賴打到一塊兒。
修改 Application.java 中的代碼,或者其餘代碼,而後重複 1 和 2
(全文完)