1、名詞理解 切麵(Aspect): 含有前置通知,後置通知,返回通知,異常拋出通知,環繞通知等方法的類; 通知(Advice): 對原方法進行添加處理(如日誌等)的方法; 切入點(PointCute): 通知需要在哪些方法上執行的表達式;(可以唯一匹配或模糊匹配); 連接點(JoinPoint): ...
1、名詞理解
- 切麵(Aspect):
- 含有前置通知,後置通知,返回通知,異常拋出通知,環繞通知等方法的類;
- 通知(Advice):
- 對原方法進行添加處理(如日誌等)的方法;
- 切入點(PointCute):
- 通知需要在哪些方法上執行的表達式;(可以唯一匹配或模糊匹配);
- 連接點(JoinPoint):
- 與切入點匹配的具體執行的方法;
- 目標(Target):
- 原業務類(主要 是核心代碼);
- 代理(Proxy):
- 生成的代理類(包含原業務類的 核心代碼 和 通知裡面的代碼);
2、前置通知
2.1 jar
<properties>
<spring.version>4.3.18.RELEASE</spring.version>
</properties>
<dependencies>
<!-- spring-beans begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-beans end -->
<!-- spring-core begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-core end -->
<!-- spring-context begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-context end -->
<!-- spring-expression begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-expression end -->
<!-- spring-aspects begin -->
<!-- maven項目中,使用aop的AspectJ框架,只需要增加此依賴,自動添加依賴aspectjweaver(包含了aspectjrt)-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring-aspects end -->
</dependencies>
2.2 切入點
通知需要在哪些方法上執行的表達式;(可以唯一匹配或模糊匹配);
2.2.1 唯一匹配
execution(public int com.kgc.spring.aspectj.ArithmeticCalculator.add(int ,int ))
execution(修飾符 返回值類型 方法全類名)
2.2.2 模糊匹配
execution(* com.kgc.spring.aspectj.*.*(..)
通用切入點表達式含義:
-
第一個*:代表任意的修飾符,任意的返回值類型;
-
第二個*:代表任意的類;
-
第三個*:代表任意的方法;
-
. . :代表任意的類型和個數的形參;
2.2.3 可重用切入點表達式
其他地方直接應用此方法即可;
//重用切入點表達式
@Pointcut( "execution(* com.kgc.spring.aspectj.*.*(..))")
public void joinPointcut(){}
//同一個類中引用
@Before("joinPointcut()")
@After("joinPointcut()")
//其他類中引用(方法全類名)
@Before("com.kgc.spring.aspectj.LogAspect.joinPointcut()")
2.3 JoinPoint 和 ProceedingJoinPoint
2.3.1 JoinPoint 對象
JoinPoint對象封裝了SpringAop中切麵方法的信息,在切麵方法中添加JoinPoint參數,就可以獲取到封裝了該方法信息的JoinPoint對象。
常用api:
方法名 | 功能 |
---|---|
Signature getSignature(); | 獲取封裝了署名信息的對象,在該對象中可以獲取到目標方法名,所屬類的Class等信息 |
Object[] getArgs(); | 獲取傳入目標方法的參數對象 |
Object getTarget(); | 獲取被代理的對象 |
Object getThis(); | 獲取代理對象 |
2.3.2 ProceedingJoinPoint對象
ProceedingJoinPoint對象是JoinPoint的子介面,該對象只用在@Around的切麵方法中 添加了 兩個方法.
方法名 | 功能 |
---|---|
Object proceed() throws Throwable | 執行目標方法 |
Object proceed(Object[] var1) throws Throwable | 傳入的新的參數去執行目標方法 |
2.4 @Before
2.4.1 介面
ArithmeticCalculator
public interface ArithmeticCalculator {
//加
int add(int m,int n);
//減
int sub(int m,int n);
//乘
int nul(int m,int n);
//除
int div(int m,int n);
}
2.4.2 實現類
ArithmeticCalculatorImpl
@Service("arithmeticCalculator")
//起別名,方便單元測試,根據別名,從容器中獲取
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int m, int n) {
return m + n;
}
@Override
public int sub(int m, int n) {
return m - n;
}
@Override
public int nul(int m, int n) {
return m*n;
}
@Override
public int div(int m, int n) {
System.out.println("====== 執行 div 方法 ======");
return m/n;
}
}
2.4.3 @Before 前置通知
在目標方法執行前,自動執行此方法(通過代理實現);
@Component //聲明為一個普通的組件,放入spring的容器中,才可以生效
@Aspect //聲明當前類是 一個切麵
public class LogAspect {
//重用切入點表達式
@Pointcut( "execution(* com.kgc.spring.aspectj.*.*(..))")
public void joinPointcut(){}
//前置通知 @Before
@Before("joinPointcut()")
public void logBeforeMethod(JoinPoint joinPoint){
//獲取通知作用的目標方法名
String methodName = joinPoint.getSignature().getName();
//獲取通知作用的目標方法入參,返回的是參數值數組
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ LogAspect "+methodName+" 方法,入參:"+ Arrays.toString(methodParams) +" ------");
}
}
2.5 配置文件
spring-aop.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 組件 -->
<context:component-scan base-package="com.kgc.spring.aspectj"></context:component-scan>
<!-- 基於註解方式實現Aspect切麵 -->
<!-- 作用:當spring的容器檢測到此配置項,會自動將Aspect切麵匹配的目標對象,放入容器,預設使用的是jdk的動態代理 -->
<aop:aspectj-autoproxy ></aop:aspectj-autoproxy>
</beans>
2.6測試
public void testSpringAopAspectj(){
//從容器中獲取計算器的實例對象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
System.out.println(arithmeticCalculator.getClass());
//調用切麵作用的目標方法,執行操作,
int result = arithmeticCalculator.div(20, 10);
System.out.println("****** 通過單元測試,計算結果:"+result +" ******");
}
測試結果
class com.sun.proxy.$Proxy15
------ LogAspect div 方法,入參:[20, 10] ------
====== 執行 div 方法 ======
****** 通過單元測試,計算結果:2 ******
3、後置通知
3.1 @After
目標方法發執行之後,自動執行;
特點:
- 後置通知無法獲取目標方法的返回值;
- 它的執行跟目標方法是否拋出異常無關,不影響此方法的執行;
@After("joinPointcut()")
public void logAfterMethod(JoinPoint joinPoint){
//獲取通知作用的目標方法名
String methodName = joinPoint.getSignature().getName();
//獲取通知作用的目標方法入參,返回的是參數值數組
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ LogAspect "+methodName+" 方法執行結束 ------");
}
3.2 測試
3.2.1 無異常
@Test
public void testSpringAopAspectj(){
//從容器中獲取計算器的實例對象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
//調用切麵作用的目標方法,執行操作,
int result = arithmeticCalculator.div(20, 10);
System.out.println("****** 通過單元測試,計算結果:"+result +" ******");
}
測試結果
====== 執行 div 方法 ======
------ LogAspect div 方法執行結束 ------
****** 通過單元測試,計算結果:2 ******
3.2.2 有異常
@Test
public void testSpringAopAspectj(){
//從容器中獲取計算器的實例對象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
//調用切麵作用的目標方法,執行操作,
int result = arithmeticCalculator.div(20, 0);
System.out.println("****** 通過單元測試,計算結果:"+result +" ******");
}
測試結果
====== 執行 div 方法 ======
------ LogAspect div 方法執行結束 ------ //有異常也會執行後置通知
java.lang.ArithmeticException: / by zero
4、返回通知
4.1 @AfterReturning
- 目標方法返回結果後自動執行,可以獲取目標方法的返回值;
- 但是要求@AfterReturning必須增加屬性returning,指定一個參數名;
- 且此參數名必須跟通知方法的一個形參名一致,用於接收返回值;
@AfterReturning(value = "joinPointcut()",returning = "result")
public void afterReturningMethod(JoinPoint joinPoint,Object result){
//獲取通知作用的目標方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("------ LogAspect "+methodName+" 方法,執行結果:"+ result +" ------");
}
4.2 測試
測試結果
====== 執行 div 方法 ======
------ LogAspect div 方法,返回結果:2 ------
****** 通過單元測試,計算結果:2 ******
5、異常拋出通知
5.1 @AfterThrowing
- 異常拋出通知 @AfterThrowing ,在目標方法拋出異常後,可以獲取目標方法發生異常後拋出的異常信息;
- 但是要求 @AfterThrowing 必須增加屬性 throwing,指定一個參數名;
- 且此參數名必須跟通知方法的一個形參名一致,用於接收異常;
@AfterThrowing(value = "joinPointcut()",throwing = "ex")
public void logAfterThrowingMethod(JoinPoint joinPoint,Exception ex){
//獲取通知作用的目標方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("------ LogAspect "+methodName+" 方法,執行異常信息:"+ ex.getMessage() +" ------");
}
5.2 測試
@Test
public void testSpringAopAspectj(){
//從容器中獲取計算器的實例對象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
System.out.println(arithmeticCalculator.getClass());
//調用切麵作用的目標方法,執行操作,
int result = arithmeticCalculator.div(20, 0);
System.out.println("****** 通過單元測試,計算結果:"+result +" ******");
}
測試結果
====== 執行 div 方法 ======
------ LogAspect div 方法,執行異常信息:/ by zero ------
java.lang.ArithmeticException: / by zero
6、環繞通知
6.1 @Around
- 環繞通知 @Around,可以看作是上面四種通知的結合體,一般不建議跟單個的通知共用(防止衝突失效);
- 作用:可以讓開發人員在環繞通知的處理方法中根據不同也業務邏輯,決定是否發起對目標方法的調用;
@Around(value = "joinPointcut()")
public Object logAroundMethod(ProceedingJoinPoint joinPoint){
//獲取通知作用的目標方法名
String methodName = joinPoint.getSignature().getName();
//定義獲取目標方法的返回值變數
Object result = null;
try{
//實現前置通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,入參:"+ Arrays.toString(joinPoint.getArgs()) +" ------");
//手動調用原目標方法(業務中決定,是否對核心方法方法發起調用)
result = joinPoint.proceed();
}catch (Throwable tx){
//實現異常拋出通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,執行異常信息:"+ tx.getMessage() +" ------");
}finally {
//實現後置通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,執行結束 ------");
}
//實現返回通知功能
System.out.println("------ LogAspect "+methodName+" 方法 Around通知,執行結果:"+ result +" ------");
return result;
}
6.2 測試
6.2.1 測試結果,無異常
//調用切麵作用的目標方法,執行操作,
int result = arithmeticCalculator.div(20, 10);
class com.sun.proxy.$Proxy13
------ LogAspect div 方法 Around通知,入參:[20, 10] ------
====== 執行 div 方法 ======
------ LogAspect div 方法 Around通知,執行結束 ------
------ LogAspect div 方法 Around通知,返回結果:2 ------
****** 通過單元測試,計算結果:2 ******
6.2.2 測試結果,有異常
//調用切麵作用的目標方法,執行操作,
int result = arithmeticCalculator.div(20, 0);
class com.sun.proxy.$Proxy13
------ LogAspect div 方法 Around通知,入參:[20, 0] ------
====== 執行 div 方法 ======
------ LogAspect div 方法 Around通知,執行異常信息:/ by zero ------
------ LogAspect div 方法 Around通知,執行結束 ------
------ LogAspect div 方法 Around通知,返回結果:null ------
6.2.3 測試結果 不調用 原方法
//調用切麵作用的目標方法,執行操作,
int result = arithmeticCalculator.div(20, 0);
//(業務中決定,是否對核心方法發起調用)
//不調用核心方法
//result = joinPoint.proceed();
------ LogAspect div 方法 Around通知,入參:[20, 10] ------
------ LogAspect div 方法 Around通知,執行結束 ------
------ LogAspect div 方法 Around通知,返回結果:null ------
7、切入點優先順序
當有多個前置通知時,我們想自定義前置通知順序:使用@Order(int)
指定切麵優先順序,一般都是int型整數,值越小,優先順序越高**(預設值 2^31 - 1 最低優先順序);
7.1 多個前置通知
logBeforeMethod
@Before("joinPointcut()")
public void logBeforeMethod(JoinPoint joinPoint){
//獲取通知作用的目標方法名
String methodName = joinPoint.getSignature().getName();
//獲取通知作用的目標方法入參,返回的是參數值數組
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ LogAspectBeforeMethod "+methodName+" 方法,入參:"+ Arrays.toString(methodParams) +" ------");
}
verifyBeforeMethod
@Component
@Aspect
public class VerifyParamAspect {
@Before("com.kgc.spring.aspectj.LogAspect.joinPointcut()")
public void verifyBeforeMethod( JoinPoint joinPoint){
//獲取通知作用的目標方法名
String methodName = joinPoint.getSignature().getName();
//獲取通知作用的目標方法入參,返回的是參數值數組
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ verifyBeforeMethod "+methodName+" 方法,入參:"+ Arrays.toString(methodParams) +" ------");
}
}
7.2 測試(預設)
@Test
public void testVerifyParamAspect(){
//從容器中獲取計算器的實例對象
ArithmeticCalculator arithmeticCalculator = context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
System.out.println(arithmeticCalculator.getClass());
//調用切麵作用的目標方法,執行操作,
int result = arithmeticCalculator.div(20, 10);
System.out.println("****** 通過單元測試,計算結果:"+result +" ******");
}
測試結果
------ LogAspectBeforeMethod div 方法,入參:[20, 10] ------ //LogAspectBeforeMethod 先執行
------ verifyBeforeMethod div 方法,入參:[20, 10] ------
====== 執行 div 方法 ======
****** 通過單元測試,計算結果:2 ******
7.3 測試(自定義優先順序)
@Component
@Aspect
@Order(1) //指定切麵優先順序,一般都是int型整數,值越小,優先順序越高(預設值 2^31 - 1)
public class VerifyParamAspect {
@Before("com.kgc.spring.aspectj.LogAspect.joinPointcut()")
public void verifyBeforeMethod( JoinPoint joinPoint){
//獲取通知作用的目標方法名
String methodName = joinPoint.getSignature().getName();
//獲取通知作用的目標方法入參,返回的是參數值數組
Object[] methodParams = joinPoint.getArgs();
System.out.println("------ verifyBeforeMethod "+methodName+" 方法,入參:"+ Arrays.toString(methodParams) +" ------");
}
}
測試結果
------ verifyBeforeMethod div 方法,入參:[20, 10] ------ //優先順序高的切麵中的verifyBeforeMethod,先執行
------ LogAspectBeforeMethod div 方法,入參:[20, 10] ------
====== 執行 div 方法 ======
****** 通過單元測試,計算結果:2 ******