一、概念及相關術語
概念
AOP(Aspect Oriented Programming)是一種設計思想,是軟體設計領域中的面向切面程式設計,它是物件導向程式設計的一種補充和完善,它以透過預編譯方式和執行期動態代理方式實現,在不修改原始碼的情況下,給程式動態統一新增額外功能的一種技術。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。
相關術語
①橫切關注點
分散在每個各個模組中解決同一樣的問題,如使用者驗證、日誌管理、事務處理、資料快取都屬於橫切關注點。
從每個方法中抽取出來的同一類非核心業務。在同一個專案中,我們可以使用多個橫切關注點對相關方法進行多個不同方面的增強。
這個概念不是語法層面的,而是根據附加功能的邏輯上的需要:有十個附加功能,就有十個橫切關注點。
②通知(增強)
增強,通俗說,就是你想要增強的功能,比如 安全,事務,日誌等。
每一個橫切關注點上要做的事情都需要寫一個方法來實現,這樣的方法就叫通知方法。
- 前置通知:在被代理的目標方法前執行
- 返回通知:在被代理的目標方法成功結束後執行(壽終正寢)
- 異常通知:在被代理的目標方法異常結束後執行(死於非命)
- 後置通知:在被代理的目標方法最終結束後執行(蓋棺定論)
- 環繞通知:使用try...catch...finally結構圍繞整個被代理的目標方法,包括上面四種通知對應的所有位置
③切面
封裝通知方法的類。
④目標
被代理的目標物件。
⑤代理
向目標物件應用通知之後建立的代理物件。
⑥連線點
這也是一個純邏輯概念,不是語法定義的。
把方法排成一排,每一個橫切位置看成x軸方向,把方法從上到下執行的順序看成y軸,x軸和y軸的交叉點就是連線點。通俗說,就是spring允許你使用通知的地方
⑦切入點
定位連線點的方式。
每個類的方法中都包含多個連線點,所以連線點是類中客觀存在的事物(從邏輯上來說)。
如果把連線點看作資料庫中的記錄,那麼切入點就是查詢記錄的 SQL 語句。
Spring 的 AOP 技術可以透過切入點定位到特定的連線點。通俗說,要實際去增強的方法
切點透過 org.springframework.aop.Pointcut 介面進行描述,它使用類和方法作為連線點的查詢條件。
作用
-
簡化程式碼:把方法中固定位置的重複的程式碼抽取出來,讓被抽取的方法更專注於自己的核心功能,提高內聚性。
-
程式碼增強:把特定的功能封裝到切面類中,看哪裡有需要,就往上套,被套用了切面邏輯的方法就被切面給增強了。
二、基於註解的AOP
技術說明
- 動態代理分為JDK動態代理和cglib動態代理
- 當目標類有介面的情況使用JDK動態代理和cglib動態代理,沒有介面時只能使用cglib動態代理
- JDK動態代理動態生成的代理類會在com.sun.proxy包下,類名為$proxy1,和目標類實現相同的介面
- cglib動態代理動態生成的代理類會和目標在在相同的包下,會繼承目標類
- 動態代理(InvocationHandler):JDK原生的實現方式,需要被代理的目標類必須實現介面。因為這個技術要求代理物件和目標物件實現同樣的介面(兄弟兩個拜把子模式)。
- cglib:透過繼承被代理的目標類(認乾爹模式)實現代理,所以不需要目標類實現介面。
- AspectJ:是AOP思想的一種實現。本質上是靜態代理,將代理邏輯“織入”被代理的目標類編譯得到的位元組碼檔案,所以最終效果是動態的。weaver就是織入器。Spring只是借用了AspectJ中的註解
準備工作
①新增依賴
在IOC所需依賴基礎上再加入下面依賴即可:
<!--spring context依賴-->
<!--當你引入Spring Context依賴之後,表示將Spring的基礎依賴引入了-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.1</version>
</dependency>
<!--spring aop依賴-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.1.1</version>
</dependency>
<!--spring aspects依賴-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.1.1</version>
</dependency>
<!--junit5-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.3</version>
</dependency>
<!--log4j2的依賴-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.20.0</version>
</dependency>
②準備被代理的目標資源
介面:
package com.mcode.annotationaop;
/**
* ClassName: Calculator
* Package: com.mcode.annotationaop
* Description:
*
* @Author: robin
* @Version: v1.0
*/
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
實現類:
package com.mcode.annotationaop;
import org.springframework.stereotype.Component;
/**
* ClassName: CalculatorImpl
* Package: com.mcode.annotationaop
* Description:
*
* @Author: robin
* @Version: v1.0
*/
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法內部 result = " + result);
//為了測試,模擬異常出現
//int a = 1/0;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法內部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法內部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法內部 result = " + result);
return result;
}
}
建立切面類並配置
package com.mcode.annotationaop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* ClassName: LogAspect
* Package: com.mcode.annotationaop
* Description: 切面類
*
* @Author: robin
* @Version: v1.0
*/
@Aspect //切面類
@Component //ioc容器
public class LogAspect {
//設定切入點和通知型別
//切入點表示式: execution(訪問修飾符 增強方法返回型別 增強方法所在類全路徑.方法名稱(方法引數))
//通知型別:
// 前置 @Before(value="切入點表示式配置切入點")
//@Before(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
@Before("execution(public int com.mcode.annotationaop.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->前置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
}
// 後置 @After()
@After("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->後置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
}
// 返回 @AfterReturning
@AfterReturning(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))",
returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名稱:" + methodName + ",返回結果:" + result);
}
// 異常 @AfterThrowing 獲取到目標方法異常資訊
//目標方法出現異常,這個通知執行
@AfterThrowing(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))", throwing =
"ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->異常通知,方法名稱:" + methodName + ",異常資訊:" + ex);
}
// 環繞 @Around()
@Around("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String argString = Arrays.toString(args);
Object result = null;
try {
System.out.println("環繞通知-->目標物件方法執行之前");
//呼叫目標方法
result = joinPoint.proceed();
System.out.println("環繞通知-->目標物件方法返回值之後");
} catch (Throwable e) {
e.printStackTrace();
System.out.println("環繞通知-->目標物件方法出現異常時");
} finally {
System.out.println("環繞通知-->目標物件方法執行完畢");
}
return result;
}
}
新增Spring配置類
package com.mcode.annotationaop;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* ClassName: SpringConfig
* Package: com.mcode.annotationaop
* Description:
* 基於註解的AOP的實現:
* 1、將目標物件和切面交給IOC容器管理(註解+掃描)
* 2、開啟AspectJ的自動代理,為目標物件自動生成代理
* 3、將切面類透過註解@Aspect標識
* @Author: robin
* @Version: v1.0
*/
@Configuration
@ComponentScan("com.mcode.annotationaop")
@EnableAspectJAutoProxy //開啟AspectJ的自動代理,為目標物件自動生成代理
public class SpringConfig {
}
執行測試:
package com.mcode.annotationaop;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* ClassName: TestAop
* Package: com.mcode.annotationaop
* Description:
*
* @Author: robin
* @Version: v1.0
*/
public class TestAop {
@Test
public void testAdd(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
Calculator calculator = context.getBean(Calculator.class);
calculator.add(1,2);
}
}
執行結果:
各種通知
- 前置通知:使用@Before註解標識,在被代理的目標方法前執行
- 返回通知:使用@AfterReturning註解標識,在被代理的目標方法成功結束後執行(壽終正寢)
- 異常通知:使用@AfterThrowing註解標識,在被代理的目標方法異常結束後執行(死於非命)
- 後置通知:使用@After註解標識,在被代理的目標方法最終結束後執行(蓋棺定論)
- 環繞通知:使用@Around註解標識,使用try...catch...finally結構圍繞整個被代理的目標方法,包括上面四種通知對應的所有位置
各種通知的執行順序:
- Spring版本5.3.x以前:
- 前置通知
- 目標操作
- 後置通知
- 返回通知或異常通知
- Spring版本5.3.x以後:
- 前置通知
- 目標操作
- 返回通知或異常通知
- 後置通知
切入點表示式語法
①作用
②語法細節
-
用*號代替“許可權修飾符”和“返回值”部分表示“許可權修飾符”和“返回值”不限
-
在包名的部分,一個“*”號只能代表包的層次結構中的一層,表示這一層是任意的。
- 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
-
在包名的部分,使用“*..”表示包名任意、包的層次深度任意
-
在類名的部分,類名部分整體用*號代替,表示類名任意
-
在類名的部分,可以使用*號代替類名的一部分
- 例如:*Service匹配所有名稱以Service結尾的類或介面
-
在方法名部分,可以使用*號表示方法名任意
-
在方法名部分,可以使用*號代替方法名的一部分
- 例如:*Operation匹配所有方法名以Operation結尾的方法
-
在方法引數列表部分,使用(..)表示引數列表任意
-
在方法引數列表部分,使用(int,..)表示引數列表以一個int型別的引數開頭
-
在方法引數列表部分,基本資料型別和對應的包裝型別是不一樣的
- 切入點表示式中使用 int 和實際方法中 Integer 是不匹配的
-
在方法返回值部分,如果想要明確指定一個返回值型別,那麼必須同時寫明許可權修飾符
- 例如:execution(public int ..Service.(.., int)) 正確
例如:execution( int ..Service.*(.., int)) 錯誤
- 例如:execution(public int ..Service.(.., int)) 正確
重用切入點表示式
①宣告
@Pointcut("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public void pointCut(){}
②在同一個切面中使用
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",引數:"+args);
}
③在不同切面中使用
@Before("com.mcode.annotationaop.LogAspect.pointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",引數:"+args);
}
獲取通知的相關資訊
①獲取連線點資訊
獲取連線點資訊可以在通知方法的引數位置設定JoinPoint型別的形參
@Before("execution(public int com.mcode.annotationaop.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
//獲取連線點的簽名資訊
String methodName = joinPoint.getSignature().getName();
//獲取目標方法到的實參資訊
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->前置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
}
②獲取目標方法的返回值
@AfterReturning中的屬性returning,用來將通知方法的某個形參,接收目標方法的返回值
// 返回 @AfterReturning
@AfterReturning(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))",returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名稱:" + methodName + ",返回結果:" + result);
}
③獲取目標方法的異常
@AfterThrowing中的屬性throwing,用來將通知方法的某個形參,接收目標方法的異常
@AfterThrowing(value = "execution(* com.mcode.annotationaop.CalculatorImpl.*(..))", throwing ="ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->異常通知,方法名稱:" + methodName + ",異常資訊:" + ex);
}
環繞通知
@Around("execution(* com.mcode.annotationaop.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String argString = Arrays.toString(args);
Object result = null;
try {
System.out.println("環繞通知-->目標物件方法執行之前");
//目標方法的執行,目標方法的返回值一定要返回給外界呼叫者,否則會報錯
result = joinPoint.proceed();
System.out.println("環繞通知-->目標物件方法返回值之後");
} catch (Throwable e) {
e.printStackTrace();
System.out.println("環繞通知-->目標物件方法出現異常時");
} finally {
System.out.println("環繞通知-->目標物件方法執行完畢");
}
return result;
}
切面的優先順序
相同目標方法上同時存在多個切面時,切面的優先順序控制切面的內外巢狀順序。
- 優先順序高的切面:外面
- 優先順序低的切面:裡面
使用@Order註解可以控制切面的優先順序:
- @Order(較小的數):優先順序高
- @Order(較大的數):優先順序低
@Order(1) //優先順序
public class LogAspect {
}
三、基於XML的AOP
準備工作
在Spring的配置檔案中配置:
<?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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--開啟元件掃描-->
<context:component-scan base-package="com.mcode.xmlaop"/>
<!--配置aop五種通知型別-->
<aop:config>
<!--配置切面類-->
<aop:aspect ref="logAspect">
<!--配置切入點-->
<aop:pointcut id="pointcut"
expression="execution(* com.mcode.xmlaop.CalculatorImpl.*(..))"/>
<!--配置五種通知型別-->
<!--前置通知-->
<aop:before method="beforeMethod"
pointcut="execution(public int com.mcode.xmlaop.CalculatorImpl.*(..))"/>
<!--後置通知-->
<aop:after method="afterMethod" pointcut-ref="pointcut"/>
<!--返回通知-->
<aop:after-returning method="afterReturningMethod" pointcut-ref="pointcut" returning="result"/>
<!--異常通知-->
<aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointcut" throwing="ex"/>
<!--環繞通知-->
<aop:around method="aroundMethod" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
實現
建立切面類並配置:
package com.mcode.xmlaop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* ClassName: LogAspect
* Package: com.mcode.xmlaop
* Description: 切面類
*
* @Author: robin
* @Version: v1.0
*/
@Component //ioc容器
public class LogAspect {
// 前置
public void beforeMethod(JoinPoint joinPoint) {
//獲取連線點的簽名資訊
String methodName = joinPoint.getSignature().getName();
//獲取目標方法到的實參資訊
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->前置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
}
// 後置
public void afterMethod(JoinPoint joinPoint) {
//獲取連線點的簽名資訊
String methodName = joinPoint.getSignature().getName();
//獲取目標方法到的實參資訊
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->後置通知,方法名稱:" + methodName + ",引數:" + Arrays.toString(args));
}
// 返回
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名稱:" + methodName + ",返回結果:" + result);
}
// 異常
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->異常通知,方法名稱:" + methodName + ",異常資訊:" + ex);
}
// 環繞
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String argString = Arrays.toString(args);
Object result = null;
try {
System.out.println("環繞通知-->目標物件方法執行之前");
//目標方法的執行,目標方法的返回值一定要返回給外界呼叫者,否則會報錯
result = joinPoint.proceed();
System.out.println("環繞通知-->目標物件方法返回值之後");
} catch (Throwable e) {
e.printStackTrace();
System.out.println("環繞通知-->目標物件方法出現異常時");
} finally {
System.out.println("環繞通知-->目標物件方法執行完畢");
}
return result;
}
}
執行測試:
package com.mcode.xmlaop;
import com.mcode.annotationaop.Calculator;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* ClassName: TestAop
* Package: com.mcode.annotationaop
* Description:
*
* @Author: robin
* @Version: v1.0
*/
public class TestAop {
@Test
public void testAdd(){
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("beans.xml");
Calculator calculator = context.getBean(Calculator.class);
calculator.add(1,2);
}
}
執行結果: