================
(接上文《Spring/Boot/Cloud系列知識(6)——Spring EL(2)》)
3.3、Spring EL 與 AOP(Aspectj)
3.3.1、Spring 和 AOP的關係
AOP是面向切面編程的簡稱,Spring的設計思路受到這個思想的指導。所以我們在使用Spring各種組建的時候都能看到這個設計思路的影子。
再舉一些實際的例子:我們使用Spring託管hibernate就是一個典型的AOP例子,事務的開啓、提交、回滾操作無需業務開發人員進行,全部在業務方法之外自動完成;Spring Cache組件的使用也是一個典型的AOP實例,完成Spring Cache EL的配置後,對Redis/Memcache或者Google Cache的操作完全不需要書寫額外代碼,全部在業務方法以外完成;而我們之前介紹的Spring中使用的兩種代理模式,也是基於AOP思想,無論是JAVA原生動態代理還是Cglib動態代理,都無需業務開發者書寫一行額外代碼即可完成代理過程。由此可見AOP思想在Spring中的體現是深入的、延續的且廣泛的。
上一段文字已經清楚的表明,AOP是一種思想。既然是一種思想就需要具體的手段來進行實現,在Spring中對於AOP思想的實現可以歸納爲兩種主要手段:
1. 基於JDK動態代理或者Cglib動態代理實現的AOP
實際上本專題之前花了若干篇文章進行介紹的,Spring中基於JDK原生的動態代理和基於Cglib的動態代理實現就是一種實現AOP思想的手段:正常的業務過程方法被代理後,可以在業務執行前、正常執行後、拋出異常後觸動一些其它方法過程的執行。
2. 基於AspectJ實現的AOP:
Spring中還集成了一個第三方組件AspectJ,這是一款獨立的面向切面框架,Spring在自己的核心組件Spring-aop中對這個第三方組件AspectJ進行了封裝(請參見spring-aop組件的源代碼)。本篇文章我們主要介紹AspectJ中使用的EL表達式,關於更多AspectJ使用和實現原理的討論,我們將在後續章節中進行。實際上AspectJ EL表達式不能算作原生的Spring EL範疇(因爲它實際上是由AspectJ組件提供的表達式功能),但是目前大家在實際使用時,都將AspectJ當作Spring的一部分,所以在這裏本文將AspectJ EL作爲一種Spring EL的擴展進行講解。
3.3.2、 Aspectj EL 示例
AspectJ在Spring的使用方式,主要是兩種。一種是在基於XML的Spring配置文件中使用,另一種是基於@AspectJ形式的註解。當然本專題主要基於Spring Boot環境講解,所以本文主要講解的還是基於@AspectJ註解的使用形式。首先先介紹AspectJ組件中幾種關鍵的註解形態(它們的定義全部在org.aspectj.lang.annotation包下):
@org.aspectj.lang.annotation.Aspect
這個註解只能在Class上進行標註,表示該類作爲一個切面定義存在於Spring容器中。@org.aspectj.lang.annotation.Before
該註解只能使用在標註了@Aspect的類的方法上,表示該註解所代表的方法將在符合條件的業務方法(被代理方法)執行前被執行。@org.aspectj.lang.annotation.After
該註解只能使用在標註了@Aspect的類的方法上,表示該註解所代表的方法將在符合條件的業務方法(被代理方法)執行後被執行,無論業務方法是正常返回還是發生異常退出。@org.aspectj.lang.annotation.AfterReturning
該註解只能使用在標註了@Aspect的類的方法上,表示該註解所代表的方法將在符合條件的業務方法(被代理方法)執行後被執行,且只當業務方法正常退出時執行。@org.aspectj.lang.annotation.AfterThrowing
該註解只能使用在標註了@Aspect的類的方法上,表示該註解所代表的方法將在符合條件的業務方法(被代理方法)執行後被執行,且只當業務方法拋出異常退出時執行。@org.aspectj.lang.annotation.Around
該註解只能使用在標註了@Aspect的類的方法上,表示符合條件的業務方法(被代理方法),將視該註解所代表的方法的內部執行過程被動執行,否則就不會執行業務方法(被代理方法)。@org.aspectj.lang.annotation.Pointcut
該註解只能使用在標註了@Aspect的類的方法上,專門用於定義一個可共享的切面執行條件(切面位置)。這樣一來保證多個切面點(Before、After、Around等)就可以共享一個切面條件。
3.3.2.1、基本使用
以下我們舉例說明一些典型的使用示例。首先,以下代碼是一個正常情況下的Spring標準代碼,MyService接口中有兩個方法,doSomething和doExceptionThing這兩個方法分別模擬了一個正常的業務過程和模擬了一個異常的業務過程。代碼如下所示:
package yinwenjie.test.proxy.interceptor;
// 這是一個bean層的spring代碼
// @author yinwenjie
@Service("MyServiceImpl")
public class MyServiceImpl implements MyService {
private static final Logger LOGGER = LoggerFactory.getLogger(MyServiceImpl.class);
@Override
public void doSomething() {
// 這是執行正常業務的代碼方法
LOGGER.info("123456");
}
@Override
public void doExceptionThing() {
// 這是一個模擬執行異常的代碼方法
throw new IllegalArgumentException("拋出了異常!!");
}
}
我們通過JUnit運行對MyService接口的調用,可得到如下結果:
......
y.test.proxy.service.MyServiceImpl : 123456
// 或者以下結果:
......
java.lang.IllegalArgumentException: 拋出了異常!!
at yinwenjie.test.proxy.service.MyServiceImpl.doExceptionThing(MyServiceImpl.java:25)
at testSpring.service.MyServiceTest.testException(MyServiceTest.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
......
那麼如果我們需要在執行業務方法之前或者之後,或者出現異常等狀況下執行一些其它過程,那麼我們可以創建一個類似如下的代理器:
/**
* 創建一個AOP切面攔截器
* @author yinwenjie
*/
@Aspect
@Component
public class MyAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(MyAspect.class);
/**
* 在被代理的業務方法執行返回後,該方法被調用。<br>
* 注意只有業務方法正常執行退出的情況下才執行
* @param joinPoint 切點的信息在這裏,可以有也可以沒有
*/
@AfterReturning("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doAfterReturning(JoinPoint joinPoint) {
LOGGER.info("攔截器作用:doAfterReturning");
}
/**
* 在被代理的業務方法執行返回後,該方法被調用。<br>
* 注意只有業務方法異常退出的情況下,才執行。
* @param joinPoint 切點的信息在這裏,可以有也可以沒有
*/
@AfterThrowing("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doAfterThrowing(JoinPoint joinPoint) {
LOGGER.info("攔截器作用:doAfterThrowing");
}
/**
* 在被代理的業務方法執行前,該方法被調用。<br>
* @param joinPoint 可以有可以沒有
*/
@Before("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doBeforeAdvice(){
LOGGER.info("攔截器作用:doBeforeAdvice");
}
/**
* 在被代理的業務方法執行返回後,該方法被調用。<br>
* 無論業務方法執行是正常退出還是拋出異常
* @param joinPoint
*/
@After("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doAfterAdvice(){
LOGGER.info("攔截器作用:doAfterAdvice");
}
}
以上示例代碼中使用的Aspect相關注解已經說明得比較清楚了,這裏的文字就不再進行贅述了。不過註解中的execution表達式要進行一些說明,execution中是一種標準的Aspect EL表達式,這是一種在AspectJ組件中使用的表達式。表達式中“* yinwenjie.test.proxy.service..*.*(..)”字符串的含義是“yinwenjie.test.proxy.service”程序包或者其子包下的任意方法(無論這個方法是否有傳參要求,無論是否有返回值,無論方法訪問修飾符如何),都使用Aspect相關注解規定的方法被代理。
以下是設定AspectJ註解後再次執行doSomething方法的日誌輸出效果:
......
14:22:08.137 INFO 451044 --- [main] y.t.p.interceptor.MyAspect : 攔截器作用:doBeforeAdvice
14:22:08.152 INFO 451044 --- [main] y.t.p.service.MyServiceImpl : 123456
14:22:08.152 INFO 451044 --- [main] y.t.p.interceptor.MyAspect : 攔截器作用:doAfterAdvice
14:22:08.153 INFO 451044 --- [main] y.t.p.interceptor.MyAspect : 攔截器作用:doAfterReturning
......
以下是設定AspectJ註解後再次執行doExceptionThing方法的日誌輸出效果:
......
14:23:21.915 INFO 450936 --- [main] y.t.p.interceptor.MyAspect : 攔截器作用:doBeforeAdvice
14:23:21.924 INFO 450936 --- [main] y.t.p.interceptor.MyAspect : 攔截器作用:doAfterAdvice
14:23:21.924 INFO 450936 --- [main] y.t.p.interceptor.MyAspect : 攔截器作用:doAfterThrowing
......
接着我們可以使用“@Pointcut”註解對相關攔截配置進行設置:
public class MyAspect {
@AfterReturning("doExecute()")
public void doAfterReturning(JoinPoint joinPoint) {
......
}
@AfterThrowing("doExecute()")
public void doAfterThrowing(JoinPoint joinPoint) {
......
}
@Before("doExecute()")
public void doBeforeAdvice(){
......
}
@After("doExecute()")
public void doAfterAdvice(){
......
}
@Pointcut("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doExecute() {
}
}
3.3.2.2、Around註解的使用
那麼@Around註解又是怎麼使用的呢?@Around註解的作用效果類似於本專題前文介紹過的Cglib動態代理中的MethodInterceptor。如果您使用了@Around註解,那麼就需要在使用了@Around註解的方法中手動調用業務方法。如下所示:
/**
* 一個使用Around的實例
* @author yinwenjie
*/
@Aspect
@Component
public class MyAround {
/**
* 日誌
*/
private static final Logger LOGGER = LoggerFactory.getLogger(MyAround.class);
/**
* 在被代理的業務方法執行返回後,該方法被調用。<br>
* 注意只有業務方法正常執行退出的情況下才執行
* @param joinPoint 切點的信息在這裏,可以有也可以沒有
*/
@Around("execution(* yinwenjie.test.proxy.service..*.*(..))")
public void doAround(ProceedingJoinPoint point) {
LOGGER.info("方法執行前執行的動作:doAround");
try {
point.proceed();
LOGGER.info("方法執行正常退出,要執行這個動作:doDone");
} catch(Throwable e) {
LOGGER.info("方法執行異常,要執行這個動作:doException");
} finally {
LOGGER.info("無論方法執行的結果怎樣,都要執行這個動作:doReturn");
}
}
}
這裏注意一下,被AspectJ AOP代理的MyService接口實現MyServiceImpl類由於代碼沒有變化,所以在之上示例中就不再贅述了。以下是使用@Around後的測試效果:
......
# doSomething方法的調用執行效果
11:19:46.589 INFO 460908 --- [main] y.t.p.i.MyAround : 方法執行前執行的動作:doAround
11:19:50.253 INFO 460908 --- [main] y.t.p.s.MyServiceImpl : 123456
11:19:50.254 INFO 460908 --- [main] y.t.p.i.MyAround : 方法執行正常退出,要執行這個動作:doDone
11:19:50.254 INFO 460908 --- [main] y.t.p.i.MyAround : 無論方法執行的結果怎樣,都要執行這個動作:doReturn
......
# doExceptionThing方法的調用執行效果
11:02:21.930 INFO 479260 --- [main] y.t.p.i.MyAround : 方法執行前執行的動作:doAround
11:02:24.562 INFO 479260 --- [main] y.t.p.i.MyAround : 方法執行異常,要執行這個動作:doException
11:02:25.066 INFO 479260 --- [main] y.t.p.i.MyAround : 無論方法執行的結果怎樣,都要執行這個動作:doReturn
......
3.3.3、Aspectj EL 中的表達式
Aspectj EL中的表達式由幾個關鍵字符構成,如下所示:
- “*”該符號是Aspectj EL 中最重要的通配符,可以匹配任意字符,但只限於一個連續的單詞。例如在不同語境下,出現一個”*”可以表示一個類名、表示一個方法名或者表示一個包目錄的某一層。
- “..”該符號匹配任意字符,並且可以是多個連續的單次。例如在不同語境下,出現”..”可以表示任意深度的包目錄、表示方法中任意多個入參類型
- “+”該符號必須伴隨類/接口名使用,表示類本身或者繼承/實現該類的某個子類。
以下我們給出一些表達式的使用示例,以幫助讀者對以上通配符進行理解:
......
// 表示匹配test包下,第一層子包下的任何類,任何方法,無論這個方法是否帶有入參,也無論這個方法的返回值是什麼
* test.*.*.*(..)
// 表示匹配test當層包下的任何類,任何方法。無論這個方法是否帶有入參,也無論這個方法的返回值是什麼
* test.*.*(..)
// 表示匹配test當層包下的任何類,任何方法。這個方法不能帶有入參,但可以是任意返回值。
* test.*.*()
// 表示匹配test當前包或任意子包下的任何方法,無論這個方法是否帶有入參,但必須沒有返回值。
// 相同於void test..*.*(..)
void test..*(..)
// 表示匹配test當前包或任意子包下的任何方法,無論這個方法是否帶有入參,但必須沒有返回值,且方法的訪問級別必須是使用public進行定義的
public void test..*(..)
// 表示匹配test當前包或任意子包下的開頭以“do”命名的方法,無論這個方法是否帶有入參,且無論有沒有返回值。
* test..do*(..)
// 表示匹配test當前包或任意子包下的任何方法,無論這個方法有沒有返回值。
// 但是該方法必須有兩個入參,且第一個入參的類型爲String
* test..*(String,*)
......
===============================
(接下文,Aspectj 中的函數、運算符、Aspectj 新特性)