【Spring AOP】@Aspect、 @Pointcut使用@annotation + 五种通知Advice注解
前言
在微服务流行的当下,在使用SpringCloud/Springboot框架开发中,AOP使用的非常广泛,尤其是@Aspect注解方式
当属最流行的,不止功能强大,性能也很优秀,还很舒心!所以本系列就结合案例
详细介绍@Aspect方式的切面
的各种用法,力求覆盖日常开发中的各种场景。本文带来的案例是:打印Log
,主要介绍@Pointcut
切点表达式的@annotation
方式,以及 五种通知Advice注解
:@Before、@After、@AfterRunning、@AfterThrowing、@Around。
AOP与Spring AOP
在正式开始之前,我们还是先了解一下AOP与Spring AOP~ 在软件开发过程中,有一些逻辑横向遍布在各个业务模块中,像权限、监控、日志、事务、异常重试等等,所以造成代码分散且冗余度高,和业务代码混夹在一起, 写起来不够优雅,改起来更是一种折磨!为了解决这些问题,AOP(Aspect Oriented Programming:面向切面编程)也就应运而生了,它是一种编程思想,就像OOP(面向对象编程)也是一种编程思想,所以AOP不是某种语言或某个框架特有的,它实现的是将横向逻辑与业务逻辑解耦,实现对业务代码无侵入,从而让我们更专注于业务逻辑本身,本质是在不改变原有业务逻辑的情况下增强横切逻辑。
在Spring中,AOP共有3种实现方式:
- Spring1.2 基于接口的配置:Spring最早的AOP实现是完全基于接口,虽然兼容,但已经不推荐了.
- Spring2.0+ schema-based 配置 :Spring2.0之后,提供了 schema-based 配置,也就是xml的方式配置.
- Spring2.0+ @Aspect配置:Spring2.0之后,也提供了
@Aspect
基于注解的实现方式,也就是本文的主角,也是目前最方便、最广泛使用的方式!**(推荐)**
@Aspect简单案例快速入门
@Aspect
注解方式,它的概念像@Aspect、@Pointcut、@Before、@After、@Around等注解都是来自于 AspectJ,但是功能的实现是纯 Spring AOP 自己实现的,主要有两大核心
:
定义[切入点]
:使用 @Pointcut 切点表达式,你可以理解成类似于正则表达式的强大东东。(本文先着重介绍@annotation
方式)定义[切入时机] 和 [增强处理逻辑]
:五种通知Advice注解 对[切入点]执行增强处理, 包括:@Before、@After、@AfterRunning、@AfterThrowing、@Around
如果没有AOP基础,对于概念可能会比较懵,所以先上一个最简单案例,基于@Aspect注解方式如何实现切面:
// @Aspect和@Component定义一个切面类
@Aspect
@Component
public class CustomLogAspect {
// 核心一:定义切点(使用@annotation方式)
@Pointcut(value = "@annotation(com.zpli.aop.CustomLog)")
public void pointCut() {
}
// 核心二:对切点增强处理(这是5种通知中的前置通知)
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知:" + joinPoint);
}
}
一共没有几行代码,就非常简单实现了在方法执行前打印日志的功能
,注解类如下(对于打上这个注解的方法 都会被切面类增强处理):
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomLog {
}
pom.xml依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
一、@Pointcut
@Pointcut切点表达式非常丰富,可以将 方法(method)、类(class)、接口(interface)、包(package) 等作为切入点,非常灵活,常用的有@annotation、execution、args、within等方式,由于篇幅原因,本文先着重介绍@annotation
方式。
@annotation
用于定义切入点,指定方法执行的位置。
@annotation方式是指:用于匹配带有特定注解的方法,切入点是指定作用于方法上的注解
。即被Spring扫描到方法上带有该注解就会执行切面通知。
@Pointcut(value = "@annotation(com.tiangang.aop.MethodLog)")
public void pointCut() {
}
案例给出的@Pointcut说明: 语法:@Pointcut(value = "@annotation(注解全类名
)")
注:只有注解类名是动态的,其它是固定写法.
execution
用于匹配特定包内的所有方法。
-
任意公共方法的执行:
execution(public * *(..))
-
任何一个以“set”开始的方法的执行:
execution(* set*(..))
-
UserService 接口的任意方法的执行:
execution(* com.zpli.service.UserService.*(..))
-
定义在service包里的任意方法的执行:
execution(* com.zpli.service.*.*(..))
-
定义在service包和所有子包里的任意类的任意方法的执行:
execution(* com.zpli.service..*.*(..))
- 第一个
*
表示匹配任意的方法返回值 - …(两个点)表示零个或多个,第一个…表示service包及其子包
- 第二个
*
表示所有类 - 第三个
*
表示所有方法 - 第二个…表示方法的任意参数个数
- 第一个
-
定义在service包和所有子包里的UserSerivce类的任意方法的执行:
execution(*com.zpli.service..UserSerivce.*(..))")
args
用于匹配具有特定参数类型的方法。
- 参数带有@Transactional标注的方法:@args(org.springframework.transaction.annotation.Transactional)
- 参数为String类型(运行时决定)的方法: args(String)
within
- service包里的任意类:
within(com.zpli.service.*)
- service包和所有子包里的任意类:
within(com.zpli.service..*)
- 带有@Transactional标注的所有类的任意方法: `@within(org.springframework.transaction.annotation.Transactional)
二、五种通知Advice
通过@Pointcut定义的切点,共有五种通知Advice方式:
注解 | 说明 |
---|---|
@Before | 前置通知,在被切的方法执行前执行 |
@After | 后置通知,在被切的方法执行后执行,比return更后 |
@AfterRunning | 返回通知,在被切的方法return后执行 |
@AfterThrowing | 异常通知,在被切的方法抛异常时执行 |
@Around | 环绕通知,这是功能最强大的Advice,可以自定义执行顺序 |
执行顺序如下:
我这里在Service里定义了一个除法方法divide(),在这个方法也打上@MethodLog注解
,让它可以被切面横切。
@Service
public class DemoService {
@MethodLog
public Integer divide(Integer a, Integer b) {
System.out.printf("方法内打印: a=%d b=%d %n", a, b);
return a / b;
}
}
用于测试的controller代码,都很简单:
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
private DemoService demoService;
@GetMapping("/divide")
public Integer divide(Integer a, Integer b) {
return demoService.divide(a, b);
}
}
1. @Around环绕通知
环绕通知方法可以包含下面四种通知方法,是最全面最灵活的通知方法,故优先介绍。
- 既可以在目标方法之前织入增强动作,也可以在执行目标方法之后织入增强动作;
- 可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标目标方法的执行;
- 可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值; 当需要改变目标方法的返回值时,只能使用Around方法;
- 虽然Around功能强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturing增强方法就可以解决的事情,就没有必要使用Around增强处理了。
注意:@Around增强方法参数是ProceedingJoinPoint
,其他通知的方法是JoinPoint
。该方法正是因为这个参数,才让@Around在其他切面注解中脱颖而出(注意是@Around才可以用该参数,其他注解是不能用的,通常都是PointCut),而它的强大之处就是它可以通过调用 proceed() 方法来执行目标方法。
@Around环绕通知的定义格式特点
- public
- 必须有一个返回值,推荐使用Object
- 方法名自定义
- 方法有参数,固定的参数
ProceedingJoinPoint
,后边也可以追加参数
@Around环绕通知的写法
第一种写法@Pointcut+@Around(切点方法名())
@Pointcut(value = "@annotation(com.zpli.aop.CustomLog)")
public void pointCut() {
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Object target = joinPoint.getTarget();
Method method = signature.getMethod();
Object[] args = joinPoint.getArgs();
printMethod(joinPoint, "[环绕通知around][proceed之前]");
// 执行方法, 可以对joinPoint.proceed()加try catch处理异常
Object result = joinPoint.proceed();
System.out.printf("[CustomLogAspect]切面 [环绕通知around][proceed之后]打印 -> [result]:%s%n", result);
return result;
}
注解语法:@Around("切点方法名
()"),前提是需要定义好**@Pointcut(注解全类名)**方法
注:只有《切点方法名》是动态的,其它是固定写法.
方法语法:public Object 方法名(ProceedingJoinPoint joinPoint) throws Throwable
第二种写法:@Around(“@annotation(注解全类名)”)
@Around("@annotation(com.zpli.aop.CustomLog)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
printMethod(joinPoint, "[环绕通知around][proceed之前]");
// 执行方法, 可以对joinPoint.proceed()加try catch处理异常
Object result = joinPoint.proceed();
System.out.printf("[CustomLogAspect]切面 [环绕通知around][proceed之后]打印 -> [result]:%s%n", result);
return result;
}
注解语法:@Around(“注解全类名”)
注:只有《注解全类名》是动态的,其它是固定写法.
方法语法:public Object 方法名(ProceedingJoinPoint joinPoint) throws Throwable
第三种写法:@Around(“@annotation(参数名)”)
@Around("@annotation(customLog)") //customLog 与下面参数名 customLog 对应
public Object around(ProceedingJoinPoint joinPoint, CustomLog customLog) throws Throwable {
printMethod(joinPoint, "[环绕通知around][proceed之前]");
// 执行方法, 可以对joinPoint.proceed()加try catch处理异常
Object result = joinPoint.proceed();
System.out.printf("[CustomLogAspect]切面 [环绕通知around][proceed之后]打印 -> [result]:%s%n", result);
return result;
}
注解语法:@Around(“参数名”)
注:只有《参数名》是动态的,其它是固定写法.
方法语法:public Object 方法名(ProceedingJoinPoint joinPoint, YourAnnotation yourAnnotation) throws Throwable
调用测试类,输出结果如下:
[MethodLogAspect]切面 [环绕通知around][proceed之前] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
[MethodLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
方法内打印: a=10 b=2
[MethodLogAspect]切面 [返回通知afterReturning] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
[MethodLogAspect]切面 [返回通知afterReturning] 打印结果 -> result:5
[MethodLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
[MethodLogAspect]切面 [环绕通知around][proceed之后]打印 -> [result]:5
ProceedingJoinPoint参数的常用方法
在 Spring AOP 的 @Around
切面中,ProceedingJoinPoint
是核心参数,提供了大量方法用于获取目标方法的上下文信息和控制流程。以下是常用方法及其作用和典型场景:
1. proceed()
方法
作用
执行被 AOP 修饰的目标方法,并返回其结果。必须调用此方法,否则切面将不会执行目标逻辑。
语法
Object result = joinPoint.proceed(); // 执行目标方法
典型场景
-
在缓存切面中,先检查缓存是否存在,若不存在再执行目标方法并缓存结果:
@Around("@annotation(customCacheable)") public Object cache(ProceedingJoinPoint joinPoint, CustomCacheable customCacheable) throws Throwable { Object cachedValue = redisService.get(cacheKey); if (cachedValue != null) { return cachedValue; } Object result = joinPoint.proceed(); // 执行目标方法并返回结果 redisService.cacheValue(cacheKey, result); return result; }
2. getSignature()
方法
作用
获取目标方法的完整签名信息(包括方法名、方法对象、参数类型、返回类型等)。
关键方法
signature.getMethod()
:返回目标方法的Method
对象。
method.getAnnotations()
:获取方法上的所有注解。
method.getParameterCount()
:获取参数数量。
method.invoke(target, args)
:直接调用目标方法(通常在切面中不需要手动调用,因为 joinPoint.proceed()
已经处理了)。
signature.getParameterTypes()
:获取方法参数的类型数组。signature.getReturnType()
:获取方法返回值类型。
语法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
典型场景
-
生成缓存键
组合方法名、参数类型和参数值:
// Arrays.toString将指定类型的数组转换为 String,数组元素会以逗号分隔,[ 和 ] 包裹整个字符串。如果传入的是null,则返回"null"。 String cacheKey = method.getName() + "_" + Arrays.toString(signature.getParameterTypes()) + "_" + Arrays.toString(args);
-
检查方法注解
判断方法是否携带特定注解:
Method method = signature.getMethod(); if (method.isAnnotationPresent(CustomCacheable.class)) { // 处理缓存逻辑 }
3. getTarget()
方法
作用
获取被 AOP 代理的真实目标对象实例(非代理对象本身)。
语法
Object target = joinPoint.getTarget();
典型场景
-
在目标对象上记录日志
log.info("目标对象类名: {}", target.getClass().getSimpleName());
-
调用目标对象的其他方法(需谨慎使用)
target.preProcess(); // 执行目标对象的前置逻辑
4. getArgs()
方法
作用
获取目标方法调用时传入的参数数组(原始参数值)。
语法
Object[] args = joinPoint.getArgs();
典型场景
-
动态生成缓存键
基于参数值生成唯一键:
String key = "cacheKey_" + Arrays.toString(args); // 参数为用户部门、用户ID时,缓存键为 cacheKey_[101,100001]
-
参数校验
在切面中统一校验参数合法性:
if (args[0] < 0) { throw new IllegalArgumentException("参数不能为负数"); }
5. getThis()
方法
作用
获取当前 AOP 代理对象实例(而非真实目标对象)。
语法
Object proxy = joinPoint.getThis();
典型场景
-
调试代理对象
log.info("代理对象类名: {}", proxy.getClass().getSimpleName());
-
特殊情况:某些框架可能需要通过代理对象获取额外信息(较少使用)。
6. getStaticPart()
方法
作用
获取目标方法的静态部分(仅适用于类方法或构造函数切面)。
语法
Class<?> staticPart = joinPoint.getStaticPart();
典型场景
-
类级别切面
检查类的静态属性或方法:
if (staticPart.isAnnotationPresent(CustomAnno.class)) { // 处理类级别注解 }
7. getClazz()
方法(Spring 5.3+)
作用
获取目标类或方法的所属类(根据切面类型自动判断)。
语法
Class<?> clazz = joinPoint.getClazz();
典型场景
-
类级别切面
检查类上的注解:
if (clazz.isAnnotationPresent(CommonAnno.class)) { // 处理控制器类逻辑 }
注意事项
- 类型转换安全
joinPoint.getSignature()
返回的是Signature
接口,需要显式转换为MethodSignature
(仅适用于方法切面)。如果是类切面或构造函数切面,需使用其他子类型(如ClassSignature
)。 - 代理对象与真实对象
joinPoint.getTarget()
是真实业务对象,而joinPoint.getThis()
是 AOP 代理对象。大部分情况下应直接操作joinPoint.getTarget()
。 - 性能考量
反射操作(如method.invoke()
)有一定的性能开销,建议仅在必要时使用。 - 参数敏感性问题
如果参数包含敏感信息(如密码),直接将其加入缓存键可能存在安全隐患,需进行脱敏处理。
通过灵活运用这些方法,可以实现对目标方法的全方位控制(如前置校验、缓存、日志、异常处理等)。
2. @Before前置通知
前置通知在被切的方法执行之前执行!
@Before("pointCut()")
public void before(JoinPoint joinPoint) throws NoSuchMethodException {
printMethod(joinPoint, "[前置通知before]");
}
注解语法:@Before("切点方法名
()")
注:只有《切点方法名》是动态的,其它是固定写法.
方法语法:public void 方法名(JoinPoint joinPoint)
这里有个非常重要参数JoinPoint:连接点 。因为Spring只支持方法
类型的连接点,所以在Spring中连接点指的就是被拦截到的方法
. 里面有三个常用的方法:
getSignature()
获取签名:
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
通过signature可以获取名称 getName() 和 参数类型 getParameterTypes()
getTarget()
获取目标类: Class<?> clazz = joinPoint.getTarget().getClass();
如果被切的类 是 被别的切面切过的类,可以使用AopUtils.getTargetClass
获取一个数组,再从数组中找你期望的类。
import org.springframework.aop.support.AopUtils;
Class<?>[] targets = AopUtils.getTargetClass(joinPoint.getTarget()).getInterfaces();
getArgs()
获取入参值
Object[] args = joinPoint.getArgs()
基于这3个方法,可以轻松打印:被切的类名、方法名、方法参数值、方法参数类型等,printMethod方法如下:
private void printMethod(JoinPoint joinPoint, String name) throws NoSuchMethodException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Class<?> clazz = joinPoint.getTarget().getClass();
Method method = clazz.getMethod(signature.getName(), signature.getParameterTypes());
System.out.printf("[CustomLogAspect]切面 %s 打印 -> [className]:%s -> [methodName]:%s -> [methodArgs]:%s%n", name, clazz.getName(), method.getName(), Arrays.toString(joinPoint.getArgs()));
}
调用测试类,输出结果如下:
[CustomLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
方法内打印: a=10 b=2
3. @After后置通知
后置通知在被切的方法执行之后执行,无论被切方法是否异常都会执行!
@After("pointCut()")
public void after(JoinPoint joinPoint) throws NoSuchMethodException {
printMethod(joinPoint, "[后置通知after]");
}
注解语法:@After("切点方法名
()")
注:只有《切点方法名》是动态的,其它是固定写法.
方法语法:public void 方法名(JoinPoint joinPoint)
调用测试类,输出结果如下:
[CustomLogAspect]切面 [前置通知after] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
方法内打印: a=10 b=2
[CustomLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
4. @AfterRunning返回通知
返回通知在被切的方法return后执行,带有返回值,如果被切方法异常则不会执行!
这里多了一个参数Object result
,注解上也多了一个参数:returning
@AfterReturning(value = "pointCut()", returning = "result") //result 与下面参数名 result 对应
public void afterReturning(JoinPoint joinPoint, Object result) throws NoSuchMethodException {
printMethod(joinPoint, "[返回通知afterReturning]");
System.out.printf("[CustomLogAspect]切面 [返回通知afterReturning] 打印结果 -> result:%s%n", result);
}
注解语法:@AfterReturning(value = "切点方法名
(), returning = "返回值参数名
")
注:只有《切点方法名》和 《返回值参数名》是动态的,其它是固定写法.
方法语法:public void 方法名(JoinPoint joinPoint, Object result)
调用测试类,输出结果如下:
[CustomLogAspect]切面 [前置通知before] 打印 -> [className]:com.zpli.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
方法内打印: a=10 b=2
[CustomLogAspect]切面 [返回通知afterReturning] 打印 -> [className]:com.zpli.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
[CustomLogAspect]切面 [返回通知afterReturning] 打印结果 -> result:5
[CustomLogAspect]切面 [后置通知after] 打印 -> [className]:com.zpli.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 2]
5. @AfterThrowing异常通知
异常通知只在被切方法异常时执行,否则不执行。
这里多了一个参数Exception e
,表示捕获所有异常,也可以设置为具体某一个异常,例如NullPointerException、RpcException等等。注解上也多了一个参数:throwing
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) throws NoSuchMethodException {
printMethod(joinPoint, "[异常通知afterThrowing]");
System.out.printf("[CustomLogAspect]切面 [异常通知afterThrowing] 打印异常 -> Exception:%s%n", e);
}
注解语法:@AfterThrowing(value = "切点方法名
(), throwing = "异常参数名
")
注:只有《切点方法名》和 《异常参数名》是动态的,其它是固定写法.
方法语法:public void 方法名(JoinPoint joinPoint, Exception e)
调用测试类,输出结果如下:
[CustomLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 0]
方法内打印: a=10 b=0
[CustomLogAspect]切面 [异常通知afterThrowing] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 0]
[CustomLogAspect]切面 [异常通知afterThrowing] 打印异常 -> Exception:java.lang.ArithmeticException: / by zero
[CustomLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService -> [methodName]:divide -> [methodArgs]:[10, 0]
总结
本文主要说明了,如何通过@Aspect
定义一个切面类,并结合打印Log案例
主要介绍了两大核心的用法:
- @Pointcut使用 @annotation 方式定义切入点
- 五种通知(Advice)注解用法:@Before、@After、@AfterRunning、@AfterThrowing、@Around