Spring AOP 核心注解全解:五种通知 + 切点表达式 + 多切面顺序,一篇搞定

开篇:这段代码有坑,你能看出来吗?

先来看一段代码,不用细看,先感受一下:

@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. 五种通知类型

2.1 @Before — 前置通知

目标方法执行之前,先跑你这段代码。

@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 里拿不到返回值,因为目标方法还没跑呢。


2.2 @AfterReturning — 后置通知(正常返回)

目标方法正常跑完才触发,抛异常就不进来了。

/**
 * 后置通知:目标方法正常返回后触发
 * 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。


2.3 @AfterThrowing — 异常通知

目标方法抛了异常才触发,正常跑完不进来。

/**
 * 异常通知:目标方法抛异常后触发
 * 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。


2.4 @After — 最终通知

不管目标方法是正常跑完还是抛异常,都会执行一次。像 Java 里的 finally 块。

/**
 * 最终通知:无论成功还是失败,都执行
 * 适合做资源清理工作
 */
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("【最终】" + methodName + " 执行结束");
}

适合:关连接、释放锁、收尾清理。


2.5 @Around — 环绕通知

这是唯一一个能完全控制目标方法执行过程的通知。你决定几点几分放行,甚至决定放不放行。

/**
 * 环绕通知:完全包裹目标方法
 * 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,调用者拿到的就是你改过的那个值

2.6 五种通知的执行顺序

正常流程(方法跑完了,没有异常):

@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:我要拦截哪几个方法。

3.1 execution() — 按方法签名匹配,最常用

语法:

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.*.*(..))"

通配符速查:

通配符 作用
* 匹配任意一个词(一层包名、一个方法名)
.. 包路径里匹配任意多层;参数里匹配任意参数
+ 匹配某个类及其所有子类

3.2 @annotation() — 按注解匹配,实际项目里最好用

给目标方法打个自定义注解,切点只匹配带了这个注解的方法,精准,不容易误伤。

// 定义一个注解
@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());
}

3.3 within() — 按类匹配

针对”这个类里的所有方法”,粒度比 execution() 粗一些。

// UserService 里的所有方法
"within(com.example.service.UserService)"

// service 包下所有类的所有方法
"within(com.example.service.*)"

// service 包及所有子包
"within(com.example.service..*)"

execution() 和 within() 的区别:execution() 匹配方法签名,within() 匹配类。大多数情况用 execution() 够了。


3.4 args() — 按参数类型匹配

// 匹配第一个参数是 Long 的所有方法
"args(Long, ..)"

// 在通知里直接拿到参数值,不需要从 joinPoint.getArgs() 拿
@Before("execution(* com.example.service.*.*(..)) && args(userId, ..)")
public void before(Long userId) {
    System.out.println("userId = " + userId);
}

3.5 @within() 和 @target() — 按类上的注解匹配

这两个用得不多,但偶尔会派上用场:

// 匹配所有加了 @Service 注解的类里的方法
"@within(org.springframework.stereotype.Service)"

// @target 在运行时判断,@within 更早判断——实际用起来差别不大
"@target(org.springframework.stereotype.Service)"

3.6 组合表达式:&& / || / !

// 拦截 service 包,但排除 HealthCheckService
"execution(* com.example.service.*.*(..)) && !within(com.example.service.HealthCheckService)"

// 匹配加了 @LogOperation 或 @AuditLog 的方法
"@annotation(com.example.annotation.LogOperation) || @annotation(com.example.annotation.AuditLog)"

3.7 @Pointcut — 复用切点表达式

多个通知用同一个切点,别重复写,抽出来:

@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. 多切面执行顺序

项目大了,切面不只一个。顺序怎么定?

5.1 @Order 注解

@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)

5.2 没加 @Order 怎么办

没加 @Order 的切面,执行顺序由 JVM 类加载顺序决定——也就是随机的。项目里多个切面同时跑,一定要加 @Order。

5.3 Ordered 接口

不想用注解,也可以实现 Ordered 接口:

@Aspect
@Component
public class SecurityAspect implements Ordered {
    @Override
    public int getOrder() { return 1; }
}

效果一样,注解更简洁。


6. 三个实战切面

理论讲完了,上真家伙。

6.1 方法耗时监控切面

/**
 * 方法耗时监控切面
 * 超过 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);
            }
        }
    }
}

6.2 操作审计日志切面

先定义注解:

/**
 * 操作审计注解
 * 加在需要记录操作的方法上
 */
@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 == nullreturn "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);
    }
}

6.3 接口幂等性校验切面

/**
 * 幂等注解:加在接口方法上,防止用户手抖重复提交
 * 原理:把前端生成的唯一标识存 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. 常见踩坑

7.1 切面加了,但没生效

先把下面四个检查一遍:

  1. 切面类没加 @Component——忘了就什么都没注册
  2. 没开 AspectJ 自动代理——Spring Boot 默认开了,原生 Spring 项目要在 @Configuration 类上加 @EnableAspectJAutoProxy
  3. 方法不是 public——Spring AOP 只拦截 public 方法
  4. 同类内部调用——a() 调用同类里的 b()b() 上的切面不触发(这是代理机制的硬限制,改不了)

7.2 @Around 忘调 proceed()

// 错误:目标方法根本不会跑
@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();  // ← 记得调这个
}

7.3 @AfterReturning 里改返回值没用

// 改了也是白改,调用者拿的还是原来的
@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 方法、同类内部调用

 

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注