跳到正文
Joeplover
工程·2026-07-29·约 7 分钟阅读

AOP 核心概念:@Aspect、@Pointcut 与 @Around 一次讲清楚

从痛点出发理解 Spring Boot AOP:@Aspect、@Pointcut 和 @Around 分别解决什么问题,什么时候直接写切点,什么时候用自定义注解。

电脑屏幕上的代码与技术学习笔记

AOP 核心概念:@Aspect、@Pointcut 与 @Around 一次讲清楚

在 Spring Boot 项目中,日志、权限、缓存、耗时统计、限流和异常处理,经常会被很多接口重复使用。每个方法都手写一遍,业务代码会越来越臃肿;抽成公共方法,又需要在每个业务方法里主动调用。

AOP(Aspect-Oriented Programming,面向切面编程)解决的就是这类“横切关注点”:把与核心业务无关、但会横跨很多方法的功能集中处理。

这篇文章只围绕三个最核心的概念展开:@Aspect、@Pointcut 和 @Around。

一、一句话速记

概念一句话解释比喻
@Aspect把重复代码集中写在一个类里工具箱
@Pointcut定义“拦截哪些方法”的规则地址或筛选条件
@Around真正执行增强逻辑,并能控制原方法是否执行施工队

它们的关系可以这样记:

@Pointcut 定义地址,@Around 根据地址去干活。

@Around 也可以直接写拦截规则,所以不是每次都必须单独定义 @Pointcut。

二、它们分别解决什么问题?

1. 统一统计耗时

如果有 100 个方法都需要统计耗时,没有 AOP 时就要复制 100 遍计时代码。使用一个切面后,可以统一拦截目标方法,只写一次计时逻辑。

主要使用:@Around。

2. 统一判断权限

如果每个接口都自己写 if 判断,代码重复,而且漏写一个判断就可能形成安全漏洞。给需要保护的方法加一个自定义注解,再由切面统一校验即可。

主要使用:@Around + 自定义注解。

3. 统一处理缓存

查询方法通常要经历“查缓存 → 缓存没有就查数据库 → 写回缓存”。这段逻辑不应该散落在每个业务方法里。切面发现缓存命中时,可以直接返回缓存结果,跳过原方法。

主要使用:@Around。

4. 统一处理异常、限流和日志

业务方法只负责业务本身,异常转换、限流判断、访问日志等交给切面统一完成。

三、最简单的耗时统计:直接使用 @Around

只有一个通知需要使用某条拦截规则时,可以不定义 @Pointcut,直接把 execution 表达式写在 @Around 上:

@Aspect
@Component
public class TimeCostAspect {

    @Around("execution(* com.yourpackage.controller.*.*(..))")
    public Object cost(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        System.out.println(
            pjp.getSignature() + " 耗时:"
                + (System.currentTimeMillis() - start) + "ms"
        );
        return result;
    }
}

这里最关键的一行是 pjp.proceed()。它代表调用原来的目标方法。如果不调用它,原方法就不会执行。

四、多个通知共用规则:定义 @Pointcut

当多个通知都要拦截同一批方法时,可以把规则提取成一个 @Pointcut:

@Aspect
@Component
public class ServiceAspect {

    // 只拦截 service 包下的所有方法
    @Pointcut("execution(* com.yourpackage.service.*.*(..))")
    public void serviceMethod() {}

    @Around("serviceMethod()")
    public Object cost(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        System.out.println(
            "耗时:" + (System.currentTimeMillis() - start) + "ms"
        );
        return result;
    }

    @Before("serviceMethod()")
    public void logBefore() {
        System.out.println("方法开始执行");
    }

    @After("serviceMethod()")
    public void cleanUp() {
        System.out.println("方法执行完毕,清理资源");
    }
}

@Pointcut 本身不负责执行业务逻辑,它只是把一条拦截规则命名为 serviceMethod,方便其他通知引用。

五、自定义注解 + AOP:最常用的方式

有些方法需要权限校验,有些方法不需要。此时按包拦截就太粗了,更适合用自定义注解作为开关。

1. 定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedPermission {
    String value() default "ADMIN";
}

2. 编写切面

@Aspect
@Component
public class PermissionAspect {

    @Around("@annotation(needPermission)")
    public Object check(
            ProceedingJoinPoint pjp,
            NeedPermission needPermission) throws Throwable {
        String role = needPermission.value();
        User user = UserContext.getCurrentUser();

        if (!user.hasRole(role)) {
            throw new RuntimeException("无权限访问");
        }

        return pjp.proceed();
    }
}

@annotation(needPermission) 表示:只有方法上标注了 NeedPermission 的目标方法,才会进入这个切面。同时,切面方法可以直接拿到注解对象,读取它的 value。

3. 在接口上使用

@GetMapping("/delete")
@NeedPermission("ADMIN")
public String delete(Long id) {
    return "删除成功";
}

这样,权限规则就从业务方法中抽离出来了。需要权限的接口加注解,不需要权限的接口不加注解。

六、@Around 独有的能力

@Before 和 @After 只能在目标方法前后执行逻辑,而 @Around 可以包住整个调用过程,因此拥有更多控制权。

1. 阻止原方法执行

缓存命中时可以直接返回:

if (cached != null) {
    return cached;
}
return pjp.proceed();

不调用 proceed(),原方法就不会执行。

2. 修改返回值

可以不返回原方法结果,而是返回默认值、降级数据或包装后的结果。

3. 获取或修改参数

可以通过 pjp.getArgs() 获取参数,并在调用原方法前完成参数处理。

4. 失败重试

可以在循环中多次调用 pjp.proceed(),捕获特定异常后进行重试。但重试次数、异常类型和幂等性必须谨慎设计。

5. 统一异常处理

可以捕获原方法抛出的异常,记录日志后返回降级数据,或者转换成项目统一的业务异常。

七、@Pointcut 常用表达式

目标表达式
指定包下所有类的所有方法execution(* com.example.service..(..))
指定包及其子包下所有方法execution(* com.example.service...(..))
指定某个类的某个方法execution(* com.example.UserService.getUser(..))
以 save 开头的方法execution(* save*(..))
返回值为 String 的方法execution(String com.example..(..))
参数必须是 Integer 类型execution(* com.example..(Integer))
带有指定注解的方法@annotation(com.example.MyLog)
组合条件(且)execution(* com.example.service..(..)) && args(id)
组合条件(或)execution(* save*(..))

在这些表达式中:

    • 可以表示任意类型、类名或方法名。
  • .. 在包路径中表示当前包及任意子包。
  • .. 在参数位置中表示任意数量、任意类型的参数。

八、常见踩坑点

1. 切面不生效:切面类没有交给 Spring 管理

切面类通常需要同时添加 @Aspect 和 @Component。@Aspect 表示它是切面,@Component 才表示把它交给 Spring 容器管理。

2. 切点表达式写错

包名、类名、方法名必须与实际代码完全匹配。建议先从一个范围较小的表达式开始验证,再逐步扩大范围。

3. private 方法无法被常规 Spring AOP 代理

Spring AOP 基于代理机制,通常只能拦截代理对象能够调用到的方法。private 方法无法被外部代理调用,因此不会按预期触发切面。

4. 同一个类内部调用不会经过代理

一个类中的方法直接调用同类另一个方法时,使用的是 this 引用,不会重新经过 Spring 代理。需要调整调用关系,或者通过 Spring 管理的代理对象调用。

5. @Around 忘记调用 proceed()

如果目标方法一直没有执行,第一件事就是检查环绕通知中是否调用了 pjp.proceed()。

6. 切点范围过大导致重复执行

表达式过宽可能让一个方法被多个切面重复拦截。此时应该缩小切点范围,或者使用排除条件明确排除不需要增强的方法。

九、最终决策树

只需要做一件事

例如只统计耗时:直接写 @Around("execution(...)"),不必额外定义 @Pointcut。

多个通知共享同一批目标方法

例如同时统计耗时、打印日志和清理资源:定义一个 @Pointcut,让多个通知共同引用。

需要按方法开关

例如只有部分接口需要权限校验:定义自定义注解,再使用 @Around("@annotation(xxx)")。

需要阻止原方法、修改返回值、重试或统一异常

必须使用 @Around,因为这些操作都需要控制 pjp.proceed() 的调用过程。

十、终极总结

@Aspect 是容器,@Pointcut 是地址,@Around 是干活的人。

当很多方法都在重复编写权限、日志、缓存、耗时、限流或异常代码时,就应该考虑使用 AOP 把横切关注点抽出来,集中写一次,再让符合规则的方法自动获得增强能力。

最后只记住一个选择原则:

  • 只有一个通知:直接在通知上写切点表达式。
  • 多个通知复用同一规则:定义 @Pointcut。
  • 按方法灵活开关:自定义注解 + @Around。
  • 需要控制原方法是否执行:使用 @Around。