学习材料: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 {
@Override
public int getOrder() {
return 1; // 优先级最高
}
}
@Aspect
public class SecondAspect implements Ordered {
@Override
public 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