## 11.1、環境搭建 > 創建名為spring_aop_annotation的新module,過程參考[9.1節](https://www.cnblogs.com/Javaer1995/p/17610379.html "9.1節") ### 11.1.1、配置打包方式和依賴 ![image](h ...
11.1、環境搭建
創建名為spring_aop_annotation的新module,過程參考9.1節
11.1.1、配置打包方式和依賴
註意:AOP需要在IOC的基礎上實現,因此需要導入IOC的依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.rain</groupId>
<artifactId>spring_aop_annotation</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!-- Spring-IOC的依賴 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<!-- spring-AOP的依賴 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
<!-- junit測試 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
11.1.2、創建Calculator介面及實現類
package org.rain.spring.aop.annotation;
/**
* @author liaojy
* @date 2023/8/12 - 17:43
*/
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 org.rain.spring.aop.annotation;
import org.springframework.stereotype.Component;
/**
* @author liaojy
* @date 2023/8/12 - 17:45
*/
// @Component註解保證這個目標類能夠放入IOC容器
@Component
public class CalculatorImpl implements Calculator {
public int add(int i, int j) {
int result = i + j;
System.out.println("方法內部 result = " + result);
return result;
}
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法內部 result = " + result);
return result;
}
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法內部 result = " + result);
return result;
}
public int div(int i, int j) {
int result = i / j;
System.out.println("方法內部 result = " + result);
return result;
}
}
11.1.3、創建切麵類LoggerAspect
package org.rain.spring.aop.annotation;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* @author liaojy
* @date 2023/8/12 - 17:56
*/
// @Aspect表示這個類是一個切麵類
@Aspect
// @Component註解保證這個切麵類能夠放入IOC容器
@Component
public class LoggerAspect {
}
11.1.4、創建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">
<!--
對指定的package進行掃描,將使用組件註解的類的對象(本示例是目標對象和切麵對象),交給spring的ioc容器來管理
-->
<context:component-scan base-package="org.rain.spring.aop.annotation"></context:component-scan>
<!--
開啟基於註解的AOP功能,該功能會為目標對象自動生成代理
-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
11.2、前置通知的使用
11.2.1、基本示例
11.2.1.1、配置前置方法
/*
* @Before註解:用於將方法標識為前置通知(方法)
* @Before註解的value屬性值為切入點表達式,其作用是將該前置通知(方法)安插到對應目標方法的連接點上
* */
@Before("execution(public int org.rain.spring.aop.annotation.CalculatorImpl.add(int , int))")
public void beforeMethod(){
System.out.println("LoggerAspect,前置通知");
}
11.2.1.2、測試使用效果
由控制台日誌可知,切麵類的前置通知(方法),通過切入點表達式,作用到了目標方法的連接點上
@Test
public void testAOPByAnnotation(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-aop-annotation.xml");
// 註意:這裡不能直接獲取目標對象來使用;因為使用了AOP之後,IOC容器中就只有對應目標對象的代理對象;
// 如果強行獲取目標對象,則報錯:NoSuchBeanDefinitionException
//Calculator calculator = ioc.getBean(CalculatorImpl.class);
// 雖然不知道代理對象的類名,但可以通過代理對象和目標對象共同實現的介面類型來從ioc容器中獲取代理對象
Calculator calculator = ioc.getBean(Calculator.class);
// 只能通過代理對象來訪問目標對象中的方法
calculator.add(1,2);
}
11.2.2、高級示例
11.2.2.1、改進前置方法
該示例中(前置)通知方法引入了連接點參數,通過連接點參數,可以動態獲取(切入點表達式)對應的目標方法的名稱和參數列表
/*
* @Before註解:用於將方法標識為前置通知(方法)
* @Before註解的value屬性值為切入點表達式,其作用是將該前置通知(方法)安插到對應目標方法的連接點上
* */
@Before("execution(public int org.rain.spring.aop.annotation.CalculatorImpl.add(int , int))")
// joinPoint參數:可以獲取(通過切入點表達式定位出的)連接點的相關信息
public void beforeMethod(JoinPoint joinPoint){
// 獲取連接點所對應目標方法的名稱
String methodName = joinPoint.getSignature().getName();
// 獲取連接點所對應目標方法的參數列表
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect-->前置通知,方法名:"+methodName+",參數:"+ Arrays.toString(args));
}
11.2.2.2、測試使用效果
11.3、切入點表達式的進階用法
11.3.1、高頻用法示例
// @Before("execution(public int org.rain.spring.aop.annotation.CalculatorImpl.add(int , int))")
/**
* 第一個*表示任意訪問修飾符和返回值類型,
* 第二個*表示該類的任意方法名稱,
* (..)表示方法的任意參數列表
* 在類的位置也可以使用*,表示當前包下所有的類,
* 在包的位置也可以使用*,表示當前包下所有的子包,
*/
@Before("execution(* org.rain.spring.aop.annotation.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect-->前置通知,方法名:"+methodName+",參數:"+ Arrays.toString(args));
}
11.3.2、詳細語法圖解
11.3.3、復用切入點表達式
11.3.3.1、聲明公共的切入點表達式
@Pointcut("execution(* org.rain.spring.aop.annotation.CalculatorImpl.*(..))")
public void pointCutOne(){}
11.3.3.2、在同一個切麵類中復用
// @Before註解的value屬性值,可以設置為使用了@Pointcut註解標識的方法名,從而復用該@Pointcut註解定義的切入點表達式
@Before("pointCutOne()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect-->前置通知,方法名:"+methodName+",參數:"+ Arrays.toString(args));
}
11.3.3.3、在不同一個切麵類中復用
// 復用其他切麵類中@Pointcut註解定義的切入點表達式,
// @Before註解的value屬性值,需要設置為使用了@Pointcut註解標識的(全限定類名+)方法名
@Before("org.rain.spring.aop.annotation.LoggerAspect.pointCutOne()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect-->前置通知,方法名:"+methodName+",參數:"+ Arrays.toString(args));
}
11.4、其他通知的使用
11.4.1、後置通知
11.4.1.1、配置後置方法
// @After註解:用於將方法標識為後置通知(方法)
@After("pointCutOne()")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect-->後置通知,方法名:"+methodName+",參數:"+ Arrays.toString(args));
}
11.4.1.2、測試使用效果
由控制台日誌可知,後置通知在目標對象方法的finally子句中執行(一般用於釋放資源)
@Test
public void testAOPByAnnotation(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-aop-annotation.xml");
// 雖然不知道代理對象的類名,但可以通過代理對象和目標對象共同實現的介面類型來從ioc容器中獲取代理對象
Calculator calculator = ioc.getBean(Calculator.class);
// 只能通過代理對象來訪問目標對象中的方法
calculator.div(1,0);
}
11.4.2、返回通知
11.4.2.1、配置返回通知
/**
* @AfterReturning註解:用於將方法標識為返回通知(方法)
* returning屬性:指定(返回)通知方法中的某個參數(名),用於接收目標對象方法的返回值
*/
@AfterReturning(value = "pointCutOne()",returning = "result")
public void afterReturningMethod(JoinPoint joinPoint,Object result){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect-->返回通知,方法名:"+methodName+",結果:"+ result);
}
11.4.2.2、測試使用效果
由控制台日誌可知,返回通知在目標對象方法的返回值之後執行
@Test
public void testAOPByAnnotation(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-aop-annotation.xml");
// 雖然不知道代理對象的類名,但可以通過代理對象和目標對象共同實現的介面類型來從ioc容器中獲取代理對象
Calculator calculator = ioc.getBean(Calculator.class);
// 只能通過代理對象來訪問目標對象中的方法
calculator.div(1,1);
}
11.4.3、異常通知
11.4.3.1、配置異常通知
/**
* @AfterThrowing註解:用於將方法標識為異常通知(方法)
* throwing屬性:指定(異常)通知方法中的某個參數(名),用於接收目標對象方法出現的異常
*/
@AfterThrowing(value = "pointCutOne()",throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint,Exception ex){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect-->異常通知,方法名:"+methodName+",異常:"+ ex);
}
11.4.3.2、測試使用效果
由控制台日誌可知,異常通知在目標對象方法的catch子句中執行
@Test
public void testAOPByAnnotation(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-aop-annotation.xml");
// 雖然不知道代理對象的類名,但可以通過代理對象和目標對象共同實現的介面類型來從ioc容器中獲取代理對象
Calculator calculator = ioc.getBean(Calculator.class);
// 只能通過代理對象來訪問目標對象中的方法
calculator.div(1,0);
}
11.4.4、通知的執行順序
11.4.4.1、Spring版本5.3.x以前
-
前置通知
-
目標操作
-
後置通知
-
返回通知或異常通知
11.4.4.2、Spring版本5.3.x以後
本示例
-
前置通知
-
目標操作
-
返回通知或異常通知
-
後置通知
11.5、環繞通知
11.5.1、配置環繞通知
環繞通知和動態代理的形式,非常相似
/**
* @Around註解:用於將方法標識為環繞通知(方法)
* 環繞通知(方法)使用的參數是ProceedingJoinPoint類型
* 環繞通知(方法)的返回值,必須和目標對象方法的返回值一致
*/
@Around("pointCutOne()")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint){
String methodName = proceedingJoinPoint.getSignature().getName();
Object[] args = proceedingJoinPoint.getArgs();
Object result = null;
try {
System.out.println("LoggerAspect-->環繞前置通知,方法名:"+methodName+",參數:"+ Arrays.toString(args));
// 表示目標對象方法的執行
result = proceedingJoinPoint.proceed();
System.out.println("LoggerAspect-->環繞返回通知,方法名:"+methodName+",結果:"+ result);
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("LoggerAspect-->環繞異常通知,方法名:"+methodName+",異常:"+ throwable);
}finally {
System.out.println("LoggerAspect-->環繞後置通知,方法名:"+methodName+",參數:"+ Arrays.toString(args));
}
return result;
}
11.5.2、測試使用效果
註意:因為環繞通知包括了其他四種通知,所以一般要麼配置其他四種通知,要麼只配置環繞通知;本示例為了展示效果才同時配置
@Test
public void testAOPByAnnotation(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-aop-annotation.xml");
// 雖然不知道代理對象的類名,但可以通過代理對象和目標對象共同實現的介面類型來從ioc容器中獲取代理對象
Calculator calculator = ioc.getBean(Calculator.class);
// 只能通過代理對象來訪問目標對象中的方法
calculator.div(1,1);
}
11.6、切麵的優先順序
11.6.1、創建其他切麵類ValidateAspect
package org.rain.spring.aop.annotation;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* @author liaojy
* @date 2023/8/15 - 7:49
*/
@Aspect
@Component
public class ValidateAspect {
}
11.6.2、配置前置通知方法
@Before("org.rain.spring.aop.annotation.LoggerAspect.pointCutOne()")
public void beforeMethod(){
System.out.println("ValidateAspect-->前置通知");
}
11.6.3、測試使用效果
由控制台日誌可知,ValidateAspect切麵的前置通知方法生效了,但執行順序在LoggerAspect切麵的前置通知方法的後面
@Test
public void testAOPByAnnotation(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("spring-aop-annotation.xml");
// 雖然不知道代理對象的類名,但可以通過代理對象和目標對象共同實現的介面類型來從ioc容器中獲取代理對象
Calculator calculator = ioc.getBean(Calculator.class);
// 只能通過代理對象來訪問目標對象中的方法
calculator.div(1,1);
}
11.6.4、調整切麵的優先順序
package org.rain.spring.aop.annotation;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author liaojy
* @date 2023/8/15 - 7:49
*/
@Aspect
@Component
// @Order註解:用於設置切麵的優先順序,value屬性值越小,優先順序越高,預設值為Integer的最大值
@Order(2023)
public class ValidateAspect {
@Before("org.rain.spring.aop.annotation.LoggerAspect.pointCutOne()")
public void beforeMethod(){
System.out.println("ValidateAspect-->前置通知");
}
}
11.6.5、測試調整後的效果
由控制台日誌可知,ValidateAspect切麵的前置通知方法的執行順序,在LoggerAspect切麵的前置通知方法的前面
這是因為ValidateAspect切麵的@Order註解的value屬性值已設為2023,要小於LoggerAspect切麵所使用的預設值(Integer的最大值2147483647)
11.7、擴展知識
-
AspectJ本質上是靜態代理,將代理邏輯“織入”被代理的目標類編譯得到的位元組碼文件,但最終效果是動態的。
-
weaver就是織入器,Spring只是借用了AspectJ中的註解。