學習材料:https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/advice.html
1. 什么是 Advice(通知)
定義:Advice 是 AOP 的核心概念之一,表示在特定的連接點(Join Point)上執行的代碼邏輯。
作用:通過 Advice,可以在方法調用前后、異常拋出時等位置插入自定義邏輯。
2. Advice 的類型
Spring AOP 提供了以下幾種類型的 Advice:
2.1 @Before Advice
@Before 注解用于聲明前置通知(Before Advice),即在目標方法執行之前執行自定義邏輯。
使用場景:適用于需要在方法執行前進行某些操作的場景,例如日志記錄、權限檢查等。
參數:@Before 注解可以接受一個切入點表達式,用于指定哪些方法執行前需要應用該Advice。
方法簽名:@Before 方法可以有參數,但這些參數必須是Spring AOP支持的參數類型,例如 JoinPoint、ProceedingJoinPoint(僅用于 @Around)、JoinPoint.StaticPart 等。
Pointcut表達式包含在注解里的liline版示例:
@Aspect
public class BeforeExample {@Before("execution(* com.xyz.dao.*.*(..))")public void doAccessCheck() {// ...}
}
?
Before注解里包含的是Pointcut的signature,真正的Pointcut表達式在@Pointcut注解里定義。
@Aspect
public class BeforeExample {@Before("com.xyz.CommonPointcuts.dataAccessOperation()")public void doAccessCheck() {// ...}
}@Aspect
public CommonPointcuts {@Pointcut("execute * com.xyz.dao.*.*(..)")public void dataAccessOperation() {}}
?
2.2 @AfterReturing, @AfterThrowing, @After Advice
在Spring AOP中,@AfterReturning、@AfterThrowing 和 @After 是三種不同的通知(Advice)類型,用于在連接點(Join Point)的不同階段執行自定義邏輯。以下是它們的詳細說明:
@AfterReturning
作用:在目標方法成功執行并返回結果后執行。
使用場景:適用于需要在方法成功執行后進行日志記錄、資源清理等操作的場景。
參數:可以接收返回值作為參數,通過returning屬性指定參數名。
@Pointcut("execution(* org.derek.ctroller.*.*(..))")public void log() {}@AfterReturning(pointcut = "log()", returning = "result")public void afterReturning(Object result) {log.info("LogAspect afterReturning ..., result: {}", result);}
@AfterThrowing
作用:在目標方法拋出異常后執行。
使用場景:適用于需要在方法拋出異常時進行異常處理、日志記錄等操作的場景。
參數:可以接收異常對象作為參數,通過throwing屬性指定參數名。
@Pointcut("execution(* org.derek.ctroller.*.*(..))")public void log() {}@AfterThrowing(pointcut = "log()", throwing = "exception")public void afterThrowing(Exception exception) {log.info("LogAspect afterThrowing ...", exception);}
@After
作用:無論目標方法是否成功執行,都會在方法執行后執行。
使用場景:適用于需要在方法執行后進行資源清理、日志記錄等操作的場景,不關心方法的執行結果。
參數:不接收任何特定參數。
@Slf4j
@Aspect
@Component
public class LogAspect {@Pointcut("execution(* org.derek.ctroller.*.*(..))")public void log() {}@After("log()")public void after() {log.info("LogAspect after ...");}
}
?
2.3 @Around Advice
Around Advice 是一種特殊的Advice,它會在匹配的方法執行前后運行。它有機會在方法執行前后執行自定義邏輯,并且可以決定方法是否執行、何時執行以及如何執行。
使用場景:Around Advice通常用于需要在方法執行前后共享狀態的場景,例如啟動和停止計時器。
最佳實踐:總是使用滿足需求的最弱形式的Advice。如果Before Advice已經足夠滿足需求,則不要使用Around Advice。
返回類型:Around Advice方法的返回類型應為Object。
參數:方法的第一個參數必須是ProceedingJoinPoint類型。
執行方法:在Around Advice方法體內,必須調用proceed()方法來執行目標方法。調用proceed()方法時,如果不帶參數,會將調用者原始的參數傳遞給目標方法。
@Slf4j
@Aspect
@Component
public class LogAspect {@Pointcut("execution(* org.derek.ctroller.*.*(..))")public void log() {}@Around("log()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("LogAspect around start ...");long start = System.currentMillis();Object result = joinPoint.proceed();log.info("LogAspect around end ...");long end = System.currentMillis();System.out.println("cost time: " + (end-start) + "ms");return result;}
}
3 Advice的執行順序
3.0 測試環境
使用的springboot測試依賴如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><version>3.3.6</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>3.3.6</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>3.3.6</version></dependency>
下面測試的Aspect準備攔截Controller的方法,Controller代碼如下:?
@Slf4j
@RestController
public class HelloCtroller {@GetMapping("/hello")public String hello() {log.info("execute method: hello()");return "hello";}@GetMapping("/divide")public Integer divide(@RequestParam("a") Integer a, @RequestParam("b") Integer b) {log.info("execute method: divide(), a: {}, b: {}", a, b);return a/b;}
}
?
3.1 單個Aspect的各Advice執行順序
可以看到我們LogAspect的整體代碼如下:
@Slf4j
@Aspect
@Component
public class LogAspect {@Pointcut("execution(* org.derek.ctroller.*.*(..))")public void log() {}@Around("log()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("LogAspect around start ...");Object result = joinPoint.proceed();log.info("LogAspect around end ...");return result;}@Before("log()")public void before() {log.info("LogAspect before ...");}@AfterReturning(pointcut = "log()", returning = "result")public void afterReturning(Object result) {log.info("LogAspect afterReturning ..., result: {}", result);}@AfterThrowing(pointcut = "log()", throwing = "exception")public void afterThrowing(Exception exception) {log.info("LogAspect afterThrowing ...", exception);}@After("log()")public void after() {log.info("LogAspect after ...");}
}
?
3.1.1 方法正常返回的執行順序
HelloController.hello() 方法會正常執行,并返回String結果。
我們執行 /hello地址的請求:
http://localhost:8080/hello
后臺打印的切面日志如下:
2025-04-15T13:58:26.858+08:00 ?INFO 25536 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet ? ? ? ?: Completed initialization in 1 ms
2025-04-15T13:58:26.888+08:00 ?INFO 25536 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect around start ...
2025-04-15T13:58:26.889+08:00 ?INFO 25536 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect before ...
2025-04-15T13:58:26.889+08:00 ?INFO 25536 --- [nio-8080-exec-1] org.derek.ctroller.HelloCtroller ? ? ? ? : execute method: hello()
2025-04-15T13:58:26.889+08:00 ?INFO 25536 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect afterReturning ..., result: hello
2025-04-15T13:58:26.889+08:00 ?INFO 25536 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect after ...
2025-04-15T13:58:26.889+08:00 ?INFO 25536 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect around end ...
調用方法的正常返回順序如下:
@Around start --> @Before advice --> orginal method??--> @AfterReturing --> @After --> @Around end.
3.1.2 方法碰到異常的執行順序
HelloController.divide(Integer a, Integer b)方法, 當b=0的時候,就會拋出除零異常。
http://localhost:8080/divide?a=2&b=0
執行的Aspect攔截日志如下:
2025-04-15T14:05:03.612+08:00 ?INFO 25536 --- [nio-8080-exec-5] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect around start ...
2025-04-15T14:05:03.613+08:00 ?INFO 25536 --- [nio-8080-exec-5] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect before ...
2025-04-15T14:05:03.613+08:00 ?INFO 25536 --- [nio-8080-exec-5] org.derek.ctroller.HelloCtroller ? ? ? ? : execute method: divide(), a: 2, b: 0
2025-04-15T14:05:03.613+08:00 ?INFO 25536 --- [nio-8080-exec-5] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect afterThrowing ...java.lang.ArithmeticException: / by zero
?? ?at org.derek.ctroller.HelloCtroller.divide(HelloCtroller.java:25) ~[classes/:na]
?? ?at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
?? ?at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
?? ?at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
?? ?at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
?? ?at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:355) ~[spring-aop-6.1.15.jar:6.1.15]2025-04-15T14:05:03.620+08:00 ?INFO 25536 --- [nio-8080-exec-5] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect after ...
2025-04-15T14:05:03.622+08:00 ERROR 25536 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] ? ?: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.ArithmeticException: / by zero] with root causejava.lang.ArithmeticException: / by zero
?
可以看到異常情況下,攔截執行順序如下:
@Around start --> @Before Advice --> orginal method --> @AfterThrowing --> @After (--> @Around end? 因為拋出異常這里不再執行)
3.2 多個Aspect的各Advice的執行順序
1. 默認執行順序
在 Spring AOP 中,默認情況下,Aspect 的執行順序是未定義的。如果多個 Aspect 匹配同一個連接點(Join Point),它們的執行順序可能會根據依賴注入的順序、類加載順序或其他因素動態決定。
2. 通過 @Order 注解控制順序
可以使用 @Order 注解來顯式指定 Aspect 的優先級。
數字越小,優先級越高,越先執行。
示例代碼:
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;@Aspect
@Order(1) // 優先級最高
public class FirstAspect {// 定義切入點和通知邏輯
}@Aspect
@Order(2) // 次優先級
public class SecondAspect {// 定義切入點和通知邏輯
}
3. 通過實現 Ordered 接口
如果不想使用 @Order 注解,可以實現 org.springframework.core.Ordered 接口,并重寫 getOrder() 方法。
示例代碼:
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;@Aspect
public class FirstAspect implements Ordered {@Overridepublic int getOrder() {return 1; // 優先級最高}
}@Aspect
public class SecondAspect implements Ordered {@Overridepublic int getOrder() {return 2; // 次優先級}
}
這里為了測試實際的Aspect執行順序,我們使用注解@Order的方式定義了兩個切面:
?ControllerAspect.java, 順序為1.
@Slf4j
@Aspect
@Component
@Order(1) // 優先級,越小越先執行
public class CtrollerAspect {@Pointcut("execution(* org.derek.ctroller.*.*(..))")public void controller() {System.out.println("log");}@Around("controller()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("around advice start ...");Object proceed = joinPoint.proceed();log.info("around advice end ...");return proceed;}@Before("controller()")public void before() {log.info("before advice ...");}@AfterReturning(pointcut = "controller()", returning = "result")public void afterReturning(Object result) {log.info("afterReturning advice ..., result: {}", result);}@AfterThrowing(pointcut = "controller()", throwing = "exception")public void afterThrowing(Exception exception) {log.info("afterThrowing advice ...", exception);}@After("controller()")public void after() {log.info("after advice ...");}
}
LogAspect.java, 順序為2.
@Slf4j
@Aspect
@Component
@Order(2)
public class LogAspect {@Pointcut("execution(* org.derek.ctroller.*.*(..))")public void log() {}@Around("log()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("LogAspect around start ...");Object result = joinPoint.proceed();log.info("LogAspect around end ...");return result;}@Before("log()")public void before() {log.info("LogAspect before ...");}@AfterReturning(pointcut = "log()", returning = "result")public void afterReturning(Object result) {log.info("LogAspect afterReturning ..., result: {}", result);}@AfterThrowing(pointcut = "log()", throwing = "exception")public void afterThrowing(Exception exception) {log.info("LogAspect afterThrowing ...", exception);}@After("log()")public void after() {log.info("LogAspect after ...");}
}
執行相同的hello方法,調用日志如下:
2025-04-15T13:52:45.672+08:00 ?INFO 32288 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet ? ? ? ?: Completed initialization in 0 ms
2025-04-15T13:52:45.705+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.CtrollerAspect ? ? ? ? ?: around advice start ...
2025-04-15T13:52:45.705+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.CtrollerAspect ? ? ? ? ?: before advice ...
2025-04-15T13:52:45.705+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect around start ...
2025-04-15T13:52:45.705+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect before ...
2025-04-15T13:52:45.705+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.ctroller.HelloCtroller ? ? ? ? : execute method: hello()
2025-04-15T13:52:45.705+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect afterReturning ..., result: hello
2025-04-15T13:52:45.707+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect after ...
2025-04-15T13:52:45.707+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.LogAspect ? ? ? ? ? ? ? : LogAspect around end ...
2025-04-15T13:52:45.707+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.CtrollerAspect ? ? ? ? ?: afterReturning advice ..., result: hello
2025-04-15T13:52:45.707+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.CtrollerAspect ? ? ? ? ?: after advice ...
2025-04-15T13:52:45.707+08:00 ?INFO 32288 --- [nio-8080-exec-1] org.derek.aspect.CtrollerAspect ? ? ? ? ?: around advice end ...
可以看到,多個切面執行順序如下:
1) @Order(1)的切面方法??@Around start --> @Before Advice
2) @Order(2)的切面方法 @Around start --> @Before Advice?
3) 執行原始的方法 original method
4) @Order(2)的切面方法 @AfterReturning/@AfterThrowing --> @After --> @Adround end
5)@Order(1)的切面方法 @AfterReturning/@AfterThrowing --> @After --> @Adround end