开篇:这段代码有坑,你能看出来吗?
先来看一段代码,不用细看,先感受一下:
@Around("execution(* com.example..*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("before");
Object result = pjp.proceed();
System.out.println("after");
return result;
}
这段代码,乍一看挺工整的。但如果你放到生产环境里跑,大概率会出三个问题:
问题一:切点表达式太宽了,com.example..*.*(..)) 意味着整个包下的所有方法全被拦截,包括那些内部调用的私有工具类。接口一多,QPS 还没上来,方法调用开销先上去了。
问题二:pjp.proceed() 抛了异常会怎样?”after” 打印不出来,因为异常把流程直接中断了。
问题三:其实这种场景根本不需要 @Around,用 @Before + @After 就够了。
三个坑,根因只有一个:对 AOP 通知类型的特点没吃透。
这篇文章,把五种通知类型怎么选、切点表达式怎么写、多切面怎么排顺序,全部讲清楚。最后三个实战切面,拿去就能用。
1. 先把概念对一遍
术语先过一遍,读代码才不会懵。
| 术语 | 意思 |
|---|---|
| 切面(Aspect) | 写横切逻辑的那个类,带 @Aspect 注解 |
| 连接点(JoinPoint) | 程序运行中能被拦截到的那个位置,Spring AOP 里就是方法调用 |
| 切点(Pointcut) | 用表达式告诉 Spring “拦截哪些连接点” |
| 通知(Advice) | 在切点处要执行的逻辑,就是 @Before、@After 这些方法 |
| 织入(Weaving) | 把切面代码塞进目标代码的过程,Spring 在运行时做这件事 |
| 引介(Introduction) | 动态给一个类加上新接口,小众但有用 |
记不住也没关系,记住这个比喻就够了:
切面 = 保安公司
切点 = 公司大门
通知 = 保安在大门口做的那些事
织入 = 把保安安排到门口
连接点 = 有人正在进门这件事
2. 五种通知类型
目标方法执行之前,先跑你这段代码。
@Aspect
@Component
public class LogAspect {
/**
* 前置通知:目标方法调用之前执行
* JoinPoint 包含了被拦截方法的完整信息,打日志就靠它
*/
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
// 方法名
String methodName = joinPoint.getSignature().getName();
// 实际传入的参数
Object[] args = joinPoint.getArgs();
System.out.println("【前置】" + methodName + " 开始执行,参数:" + Arrays.toString(args));
}
}
适合:入参打印、权限校验(校验失败直接抛异常阻止调用)、参数预处理。
注意:@Before 里拿不到返回值,因为目标方法还没跑呢。
目标方法正常跑完才触发,抛异常就不进来了。
/**
* 后置通知:目标方法正常返回后触发
* returning = "result" 把返回值绑定到参数上,名字要和下面的参数名一致
*/
@AfterReturning(
pointcut = "execution(* com.example.service.*.*(..))",
returning = "result"
)
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【后置】" + methodName + " 执行完毕,返回值:" + result);
}
适合:记录返回值、对返回值做二次处理——不过注意,这里改返回值对调用者没用,除非你用 @Around。
目标方法抛了异常才触发,正常跑完不进来。
/**
* 异常通知:目标方法抛异常后触发
* throwing = "ex" 把异常绑定到参数上
*/
@AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex"
)
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【异常】" + methodName + " 抛了:" + ex.getMessage());
}
一个常被忽略的点:@AfterThrowing 执行完之后,异常还是会继续往上抛,它拦不住。如果你想要”把异常吞掉转成别的”,只能用 @Around。
不管目标方法是正常跑完还是抛异常,都会执行一次。像 Java 里的 finally 块。
/**
* 最终通知:无论成功还是失败,都执行
* 适合做资源清理工作
*/
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【最终】" + methodName + " 执行结束");
}
适合:关连接、释放锁、收尾清理。
这是唯一一个能完全控制目标方法执行过程的通知。你决定几点几分放行,甚至决定放不放行。
/**
* 环绕通知:完全包裹目标方法
* ProceedingJoinPoint 比 JoinPoint 多了一个 proceed() 方法
* 不调用 proceed(),目标方法就永远不会执行——这是能力,也是坑
*/
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
long start = System.currentTimeMillis();
try {
System.out.println("【环绕-前】" + methodName + " 开始");
// 执行目标方法,proceed() 的返回值就是方法的返回值
Object result = pjp.proceed();
System.out.println("【环绕-后】" + methodName + " 正常结束");
return result;
} catch (Throwable ex) {
// 这里可以做异常转换,把底层异常包装成业务异常
System.out.println("【环绕-异常】" + methodName + " 出问题了:" + ex.getMessage());
throw ex; // 记得重新抛出,别吞掉
} finally {
long cost = System.currentTimeMillis() - start;
System.out.println("【环绕-耗时】" + methodName + " 跑了 " + cost + "ms");
}
}
三个容易踩的坑:
-
必须调用 pjp.proceed(),不调目标方法就不跑,很多新手卡在这里 -
返回值类型必须是 Object,不能是void -
改了返回值再 return,调用者拿到的就是你改过的那个值
正常流程(方法跑完了,没有异常):
@Around 前半段
→ @Before
→ 目标方法执行
→ @AfterReturning
→ @After
→ @Around finally 部分
异常流程(方法跑了一半,抛了异常):
@Around 前半段
→ @Before
→ 目标方法抛异常
→ @AfterThrowing
→ @After
→ @Around catch/finally 部分
对比表格:
| 通知 | 正常跑完 | 抛异常 | 能拿到返回值 | 能改返回值 | 能拦截异常 |
|---|---|---|---|---|---|
| @Before | ✅ | ✅(先执行完再抛) | ❌ | ❌ | ❌(只能在前面挡) |
| @AfterReturning | ✅ | ❌ | ✅ | ❌ | ❌ |
| @AfterThrowing | ❌ | ✅ | ❌ | ❌ | ❌ |
| @After | ✅ | ✅ | ❌ | ❌ | ❌ |
| @Around | ✅ | ✅ | ✅ | ✅ | ✅ |
怎么选:
-
只在前面做点事 → @Before -
想拿返回值 → @AfterReturning -
想兜住异常做告警 → @AfterThrowing -
想清理资源(无论成功失败)→ @After -
想完全掌控方法的进出(改参数、改返回值、决定跑不跑)→ 只有 @Around
3. 切点表达式
切点表达式告诉 Spring:我要拦截哪几个方法。
语法:
execution(修饰符? 返回值类型 类路径?方法名(参数列表))
问号是可选项,返回值和方法名是必填的。
// 匹配 service 包下所有类的所有方法(这个写最多)
"execution(* com.example.service.*.*(..))"
// 匹配 com.example 及其所有子包(双点匹配多层)
"execution(* com.example..*.*(..))"
// 只匹配 UserService 的 save 方法
"execution(* com.example.service.UserService.save(..))"
// 只匹配返回 String 的方法
"execution(String com.example.service.*.*(..))"
// 只匹配无参方法
"execution(* com.example.service.*.*())"
// 匹配第一个参数是 Long 的方法
"execution(* com.example.service.*.*(Long, ..))"
// 只匹配 public 方法
"execution(public * com.example.service.*.*(..))"
通配符速查:
| 通配符 | 作用 |
|---|---|
* |
匹配任意一个词(一层包名、一个方法名) |
.. |
包路径里匹配任意多层;参数里匹配任意参数 |
+ |
匹配某个类及其所有子类 |
给目标方法打个自定义注解,切点只匹配带了这个注解的方法,精准,不容易误伤。
// 定义一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
String value() default ""; // 操作描述
}
// 切点:匹配所有加了 @LogOperation 的方法
@Pointcut("@annotation(com.example.annotation.LogOperation)")
public void logPointcut() {}
// 在通知里直接拿到注解实例,可以读注解的属性
@Before("@annotation(logOp)")
public void before(JoinPoint joinPoint, LogOperation logOp) {
System.out.println("操作:" + logOp.value());
}
针对”这个类里的所有方法”,粒度比 execution() 粗一些。
// UserService 里的所有方法
"within(com.example.service.UserService)"
// service 包下所有类的所有方法
"within(com.example.service.*)"
// service 包及所有子包
"within(com.example.service..*)"
execution() 和 within() 的区别:execution() 匹配方法签名,within() 匹配类。大多数情况用 execution() 够了。
// 匹配第一个参数是 Long 的所有方法
"args(Long, ..)"
// 在通知里直接拿到参数值,不需要从 joinPoint.getArgs() 拿
@Before("execution(* com.example.service.*.*(..)) && args(userId, ..)")
public void before(Long userId) {
System.out.println("userId = " + userId);
}
这两个用得不多,但偶尔会派上用场:
// 匹配所有加了 @Service 注解的类里的方法
"@within(org.springframework.stereotype.Service)"
// @target 在运行时判断,@within 更早判断——实际用起来差别不大
"@target(org.springframework.stereotype.Service)"
// 拦截 service 包,但排除 HealthCheckService
"execution(* com.example.service.*.*(..)) && !within(com.example.service.HealthCheckService)"
// 匹配加了 @LogOperation 或 @AuditLog 的方法
"@annotation(com.example.annotation.LogOperation) || @annotation(com.example.annotation.AuditLog)"
多个通知用同一个切点,别重复写,抽出来:
@Aspect
@Component
public class LogAspect {
/** service 包下所有方法 */
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
/** 加了 @LogOperation 注解的方法 */
@Pointcut("@annotation(com.example.annotation.LogOperation)")
public void logMethods() {}
/** 组合切点:service 层 + 加了 @LogOperation 的方法 */
@Pointcut("serviceLayer() && logMethods()")
public void logInService() {}
// 直接引用方法名
@Before("serviceLayer()")
public void before(JoinPoint joinPoint) { /* ... */ }
@Around("logInService()")
public Object around(ProceedingJoinPoint pjp) throws Throwable { /* ... */ }
}
4. JoinPoint API:通知里能拿到什么
JoinPoint 是通知方法的入参,包含了被拦截方法的全部运行时信息。
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
// 1. 方法签名(包含方法名、参数类型、返回类型、所在类)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 2. 方法名
String methodName = signature.getName();
// 3. 方法所在类
Class<?> declaringType = signature.getDeclaringType();
// 4. 运行时传入的实际参数
Object[] args = joinPoint.getArgs();
// 5. 参数名(需要编译器保留参数名,-g 编译或用 Spring 的参数名发现)
String[] parameterNames = signature.getParameterNames();
// 6. 参数类型
Class<?>[] parameterTypes = signature.getParameterTypes();
// 7. 返回类型
Class<?> returnType = signature.getReturnType();
// 8. 目标对象(原始对象)
Object target = joinPoint.getTarget();
// 9. 代理对象
Object proxy = joinPoint.getThis();
// 10. 获取方法上的注解
Method method = signature.getMethod();
LogOperation annotation = method.getAnnotation(LogOperation.class);
}
@Around 里用 ProceedingJoinPoint:
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 执行目标方法
Object result = pjp.proceed();
// 用新参数重新执行(可以偷换入参!)
Object[] newArgs = new Object[]{ /* 修改后的参数 */ };
Object result2 = pjp.proceed(newArgs);
return result;
}
5. 多切面执行顺序
项目大了,切面不只一个。顺序怎么定?
@Aspect
@Component
@Order(1) // 数字越小,优先级越高
public class SecurityAspect {
@Before("execution(* com.example.service.*.*(..))")
public void before() { System.out.println("安全校验"); }
}
@Aspect
@Component
@Order(2)
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void before() { System.out.println("日志记录"); }
}
@Aspect
@Component
@Order(3)
public class PerformanceAspect {
@Before("execution(* com.example.service.*.*(..))")
public void before() { System.out.println("性能监控"); }
}
执行顺序(@Before 部分):
安全校验(Order=1)
日志记录(Order=2)
性能监控(Order=3)
目标方法
@After 和 @Around 后半段反过来——外层先进,后出,像剥洋葱一样:
@Around 前(Order=1)
@Before(Order=1)
@Around 前(Order=2)
@Before(Order=2)
目标方法
@After(Order=2)
@Around 后(Order=2)
@After(Order=1)
@Around 后(Order=1)
没加 @Order 的切面,执行顺序由 JVM 类加载顺序决定——也就是随机的。项目里多个切面同时跑,一定要加 @Order。
不想用注解,也可以实现 Ordered 接口:
@Aspect
@Component
public class SecurityAspect implements Ordered {
@Override
public int getOrder() { return 1; }
}
效果一样,注解更简洁。
6. 三个实战切面
理论讲完了,上真家伙。
/**
* 方法耗时监控切面
* 超过 500ms 的接口打 WARN 日志,方便在日志里捞慢接口
*/
@Aspect
@Component
@Order(10)
@Slf4j
public class PerformanceMonitorAspect {
/** 超过这个时间就打 WARN(单位毫秒)*/
private static final long WARN_THRESHOLD_MS = 500;
/**
* 切点:拦截 @RestController 和 @Service 里的 public 方法
* 用 @within 而不用 execution,是因为只关心这两类 Bean
*/
@Pointcut("@within(org.springframework.web.bind.annotation.RestController) || " +
"@within(org.springframework.stereotype.Service)")
public void monitorPointcut() {}
@Around("monitorPointcut()")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
String className = pjp.getTarget().getClass().getSimpleName();
String methodName = pjp.getSignature().getName();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - startTime;
if (cost > WARN_THRESHOLD_MS) {
log.warn("【慢方法】{}.{}() 耗时 {}ms,超过了 {}ms 阈值",
className, methodName, cost, WARN_THRESHOLD_MS);
} else {
log.debug("【耗时】{}.{}() {}ms", className, methodName, cost);
}
}
}
}
先定义注解:
/**
* 操作审计注解
* 加在需要记录操作的方法上
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
/** 操作描述,如"删除用户" */
String value() default "";
/** 操作所属模块 */
String module() default "";
}
切面实现:
/**
* 操作审计切面
* 记录:谁、什么时候、做了什么、入参是什么、返回了什么、花了多久
*/
@Aspect
@Component
@Order(5)
@Slf4j
public class AuditLogAspect {
@Autowired
private AuditLogService auditLogService; // 负责把日志异步写入数据库
/**
* 切点:只拦截加了 @AuditLog 注解的方法
* 用注解驱动比 execution() 精准,不容易误伤其他方法
*/
@Around("@annotation(auditLog)")
public Object audit(ProceedingJoinPoint pjp, AuditLog auditLog) throws Throwable {
long startTime = System.currentTimeMillis();
// 获取方法信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
// 获取当前登录用户(根据项目实际情况,从 SecurityContext 或 ThreadLocal 里取)
String operator = getCurrentUser();
String resultStatus = "SUCCESS";
String errorMsg = null;
Object result = null;
try {
result = pjp.proceed();
return result;
} catch (Throwable ex) {
resultStatus = "FAILED";
errorMsg = ex.getMessage();
throw ex; // 异常还是要继续往上抛
} finally {
long cost = System.currentTimeMillis() - startTime;
// 构造审计记录,异步入库
AuditLogRecord record = AuditLogRecord.builder()
.operator(operator)
.operateTime(LocalDateTime.now())
.module(auditLog.module())
.description(auditLog.value())
.className(pjp.getTarget().getClass().getName())
.methodName(method.getName())
.requestParams(safeSerialize(pjp.getArgs()))
.responseResult(safeSerialize(result))
.status(resultStatus)
.errorMsg(errorMsg)
.costMs(cost)
.build();
// 异步写入,不影响主流程
auditLogService.saveAsync(record);
}
}
/**
* 安全序列化
* 生产环境里注意脱敏,密码、Token 这些字段千万不能入库
*/
private String safeSerialize(Object obj) {
if (obj == null) return "null";
try {
return JSON.toJSONString(obj); // 简单处理,生产环境建议做字段过滤
} catch (Exception e) {
return obj.toString();
}
}
/** 获取当前操作用户 */
private String getCurrentUser() {
try {
return SecurityContextHolder.getContext()
.getAuthentication().getName();
} catch (Exception e) {
return "anonymous";
}
}
}
使用方式:
@Service
public class UserService {
/**
* 在方法上加 @AuditLog,业务代码完全不用改
*/
@AuditLog(module = "用户管理", value = "删除用户")
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
}
/**
* 幂等注解:加在接口方法上,防止用户手抖重复提交
* 原理:把前端生成的唯一标识存 Redis,过期时间内相同标识只放行一次
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/** key 过期时间(秒),默认 60 秒 */
int expireSeconds() default 60;
/** 提示信息 */
String message() default "请勿重复提交";
}
/**
* 幂等性校验切面
* 从请求头里拿幂等 key,前端发请求时在 Header 里带上
*/
@Aspect
@Component
@Order(1) // 幂等校验要最早执行,拦住了后面的切面都不用跑
@Slf4j
public class IdempotentAspect {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String IDEMPOTENT_KEY_HEADER = "X-Idempotent-Key";
@Around("@annotation(idempotent)")
public Object checkIdempotent(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
HttpServletRequest request = getCurrentRequest();
if (request == null) {
// 非 HTTP 调用场景,放过
return pjp.proceed();
}
String key = request.getHeader(IDEMPOTENT_KEY_HEADER);
if (StringUtils.isBlank(key)) {
throw new IllegalArgumentException(
"缺少幂等 key,请在请求头中携带 " + IDEMPOTENT_KEY_HEADER);
}
// key 加上类名+方法名做前缀,防止不同接口 key 冲突
String className = pjp.getTarget().getClass().getSimpleName();
String methodName = pjp.getSignature().getName();
String redisKey = "idempotent:" + className + ":" + methodName + ":" + key;
// setIfAbsent = SET key value NX EX expireSeconds
// true = key 不存在、设进去了 → 第一次请求,放行
// false = key 已存在 → 重复请求,拦住
Boolean isFirst = redisTemplate.opsForValue()
.setIfAbsent(redisKey, "1", idempotent.expireSeconds(), TimeUnit.SECONDS);
if (Boolean.FALSE.equals(isFirst)) {
log.warn("【幂等拦截】重复请求,key={},method={}.{}()",
key, className, methodName);
throw new BusinessException(idempotent.message());
}
try {
return pjp.proceed();
} catch (Throwable ex) {
// 业务异常之外的其他失败(系统错误),删除 key 让用户可以重试
// 参数错误之类的就别删了,重试也没用
if (!(ex instanceof BusinessException)) {
redisTemplate.delete(redisKey);
}
throw ex;
}
}
/** 拿当前 HTTP 请求 */
private HttpServletRequest getCurrentRequest() {
try {
ServletRequestAttributes attrs =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attrs != null ? attrs.getRequest() : null;
} catch (Exception e) {
return null;
}
}
}
使用方式:
@RestController
public class OrderController {
/**
* 前端在 Header 里带 X-Idempotent-Key: <UUID>
* 60 秒内相同的 key 只允许成功一次
*/
@PostMapping("/orders")
@Idempotent(expireSeconds = 60, message = "订单已提交,请勿重复下单")
public Result<Long> createOrder(@RequestBody CreateOrderRequest request) {
return Result.ok(orderService.createOrder(request));
}
}
7. 常见踩坑
先把下面四个检查一遍:
-
切面类没加 @Component——忘了就什么都没注册 -
没开 AspectJ 自动代理——Spring Boot 默认开了,原生 Spring 项目要在 @Configuration 类上加 @EnableAspectJAutoProxy -
方法不是 public——Spring AOP 只拦截 public 方法 -
同类内部调用—— a()调用同类里的b(),b()上的切面不触发(这是代理机制的硬限制,改不了)
// 错误:目标方法根本不会跑
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) {
System.out.println("before");
return null; // ← 目标方法被你干掉了
}
// 正确
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("before");
return pjp.proceed(); // ← 记得调这个
}
// 改了也是白改,调用者拿的还是原来的
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturning(Object result) {
result = "new value"; // 对 String 和基本类型完全无效
}
// 想改返回值,只有 @Around
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
return "new value"; // 改完了再 return,这才管用
}
8. 面试高频题
Q1:@Before 和 @Around 核心区别是什么?
@Before 只在方法前面跑,不能决定方法动不动、不能改返回值。@Around 完全包裹方法,可以选择不跑(不调 proceed())、改传入参数、改返回值、把异常吞掉。能力更强,但写起来也更复杂。能用 @Before 解决的问题,不要用 @Around。
Q2:多个切面怎么定顺序?
用
@Order注解,数字小的先跑。但要注意:@Before 是数字小的先执行,@After 和 @Around 的后半段是数字小的后执行,整体是个洋葱结构。
Q3:* 和 .. 的区别?
*只能匹配一层,..在包路径里匹配任意多层,在参数里匹配任意数量参数。com.example..*能匹配com.example及其所有子包,com.example.*只能匹配com.example下一层。
Q4:实际项目里,execution() 和 @annotation() 哪个好?
优先用 @annotation() + 自定义注解。execution() 写包路径容易写宽,误伤一大片;@annotation() 是精准打击,只有加了注解的方法才被拦,改动注解位置就能改拦截范围,维护起来干净得多。
Q5:@After 和 @AfterReturning 怎么选?
@After 无论成功失败都执行,适合做清理工作。@AfterReturning 只在正常返回时执行,但能拿到返回值,适合做结果处理。如果两个都要,用 @After + @AfterReturning 配合。
小结
| 场景 | 推荐 |
|---|---|
| 只在方法前做点事 | @Before |
| 想拿返回值 | @AfterReturning |
| 想做清理(无论成功失败) | @After |
| 想兜住异常做告警 | @AfterThrowing |
| 想完全掌控方法 | @Around |
| 切点写法 | @annotation() + 自定义注解,精准好维护 |
| 切面顺序 | @Order,数字越小优先级越高 |
| 切面没生效先查 | @Component、@EnableAspectJAutoProxy、public 方法、同类内部调用 |