人人都会AI编程

9.1 注解式 AOP 开发:@Aspect + @Pointcut + 五大通知注解

更新时间:2026-07-11

从 Spring 2.0 引入 @AspectJ 注解支持开始,声明式切面就不再需要冗长的 XML 配置。通过一套简洁的注解,开发者可以将横切逻辑集中到独立的切面类中,并在运行时透明地织入目标 Bean。本节将聚焦于日常开发中最常用的注解式 AOP 开发方式,梳理 @Aspect@Pointcut 及五种通知注解的用法、执行顺序和实践要点。

9.1.1 启用注解式 AOP

要让 Spring 识别并处理 @Aspect 注解,需要在配置类上添加 @EnableAspectJAutoProxy

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.example")
public class AppConfig {
}

如果是 Spring Boot 应用,spring-boot-starter-aop 会自动引入相关依赖并启用 AOP 代理,无需手动添加该注解。

提示:Spring AOP 默认使用 JDK 动态代理(当目标类实现了接口时)或 CGLIB 代理(当目标类没有接口时)。@EnableAspectJAutoProxy(proxyTargetClass = true) 可强制使用 CGLIB,意味着代理对象是目标类的子类,因此 final 方法和 final 类无法被代理。Spring Boot 默认 proxyTargetClasstrue

9.1.2 创建切面类:@Aspect + @Component

一个切面类就是一个普通的 Spring Bean,需要用 @Aspect 标记,并被容器管理:

@Aspect
@Component
public class LoggingAspect {
    // 切点和通知定义
}

@Aspect 注解本身不会被 Spring 自动扫描为 Bean,因此必须配合 @Component 或通过 @Bean 方法显式注册。

9.1.3 定义切点:@Pointcut

切点(Pointcut)描述了“在哪里”应用通知。通过 @Pointcut 可以为重复使用的切点表达式赋予一个有意义的名称,提升可读性和可维护性。

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}

@Pointcut("within(com.example.web..*)")
public void webLayer() {}

@Pointcut("serviceLayer() && webLayer()")  // 组合切点
public void serviceAndWeb() {}

常用切点指示符一览:

| 指示符 | 说明 | 示例 |
|--------|------|------|
| execution | 匹配方法执行连接点 | execution(public com.example.service..*(..)) |
| within | 限定在某个包的类中 | within(com.example.service.*) |
| @annotation | 带有特定注解的方法 | @annotation(com.example.log.Loggable) |
| args | 方法参数类型匹配 | args(java.io.Serializable) |
| @args | 运行时参数带有特定注解 | 较少使用,略 |
| this/target | 代理/目标对象类型匹配 | this(com.example.Service) |
| bean | 按 Spring Bean 名称匹配 | bean(userService) |

execution 表达式详解

execution 是最常用的指示符,完整格式为:

execution(修饰符? 返回类型 包名.类名.方法名(参数列表) throws 异常类型?)

示例:

// 拦截所有 public 方法
execution(public * *(..))
// 拦截 service 包下所有方法,返回类型任意
execution(* com.example.service.*.*(..))
// 拦截 UserService 中以 find 开头,第一个参数为 Long 的方法
execution(* com.example.service.UserService.find*(Long,..))

自定义注解作为切点

通过自定义注解配合 @annotation,可以实现精准的切点控制:

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

切点定义:

@Pointcut("@annotation(loggable)")
public void loggableMethods(Loggable loggable) {}

后续通知方法可以直接拿到该注解实例,获取注解属性。

9.1.4 五大通知注解

Spring AOP 定义了五种通知类型,分别对应不同切入时机。通知方法可以接收一个 JoinPoint 类型的参数(对于环绕通知是 ProceedingJoinPoint),以获取方法签名、参数、目标对象等元信息。

1. 前置通知 @Before

在目标方法执行前运行,无法阻止目标方法的执行(除非抛出异常)。

@Before("serviceLayer()")
public void beforeAdvice(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().toShortString();
    Object[] args = joinPoint.getArgs();
    System.out.println("[Before] 方法:" + methodName + " 参数:" + Arrays.toString(args));
}

2. 后置通知 @After

在目标方法执行后运行,无论方法是正常结束还是抛出异常,都会执行。通常用于释放资源、记录结束时间等。

@After("serviceLayer()")
public void afterAdvice(JoinPoint joinPoint) {
    System.out.println("[After] 方法:" + joinPoint.getSignature().getName() + " 执行完毕");
}

3. 返回通知 @AfterReturning

在目标方法正常返回后执行,可以获取返回值。若方法抛出异常则不会触发。

@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
    System.out.println("[AfterReturning] 返回值:" + result);
}

returning 属性值必须与方法参数名一致,类型可用 Object 接收任意返回值。

4. 异常通知 @AfterThrowing

在目标方法抛出异常后执行,可以获取异常对象。若方法正常结束则不会触发。

@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
    System.out.println("[AfterThrowing] 异常:" + ex.getMessage());
}

throwing 属性同理,用于绑定捕获的异常。

5. 环绕通知 @Around

功能最强大,在目标方法执行前后、异常时均可介入,并且可以控制目标方法是否执行、修改参数和返回值。必须显式调用 ProceedingJoinPoint.proceed() 才能执行目标方法。

@Around("serviceLayer()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    String methodName = pjp.getSignature().toShortString();
    long start = System.currentTimeMillis();
    Object result;
    try {
        // 前置增强
        System.out.println("[Around] 开始执行 " + methodName);
        result = pjp.proceed();          // 执行目标方法
        // 返回增强
        System.out.println("[Around] 完成执行 " + methodName);
    } catch (Exception e) {
        // 异常增强
        System.out.println("[Around] 异常 " + e.getMessage());
        throw e;  // 不吞掉异常
    } finally {
        System.out.println("[Around] 耗时:" + (System.currentTimeMillis() - start) + "ms");
    }
    return result;  // 必须返回结果,否则调用方收到 null
}

重要:环绕通知的返回值必须返回 proceed() 的结果,或者返回自定义的替代值。如果忘记返回,调用方将得到 null,这是常见的 Bug 来源。

9.1.5 通知执行顺序

当一个切面内有多个通知,或者多个切面作用于同一个连接点时,执行顺序由 @Order 注解或实现 Ordered 接口决定。数字越小,优先级越高

默认执行顺序如下:

Around (before proceed)
  → Before
    → 目标方法
  → AfterReturning / AfterThrowing
→ After
→ Around (after proceed)

如果存在多个优先级不同的切面,高优先级切面的前置和后置“包裹”着低优先级切面。示例:

@Aspect @Component @Order(1)
public class FirstAspect { ... }

@Aspect @Component @Order(2)
public class SecondAspect { ... }

当两者均对同一方法生效时,执行顺序为:

FirstAround.before → FirstBefore → SecondAround.before → SecondBefore
  → 目标方法
SecondAfterReturning → SecondAfter → SecondAround.after
FirstAfterReturning → FirstAfter → FirstAround.after

理解这一顺序对于调试事务、日志、缓存等多切面组合场景至关重要。

9.1.6 完整示例:方法耗时统计与日志切面

以下是一个生产环境中极为常见的组合:通过自定义注解标记需要记录日志的方法,并使用环绕通知打印方法调用信息与耗时。

1. 自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Traceable {
    String value() default "";   // 用于附加描述
}

2. 切面类

@Aspect
@Component
public class TraceAspect {

    @Pointcut("@annotation(traceable)")
    public void traceablePointcut(Traceable traceable) {}

    @Around(value = "traceablePointcut(traceable)", argNames = "pjp,traceable")
    public Object trace(ProceedingJoinPoint pjp, Traceable traceable) throws Throwable {
        String description = traceable.value().isEmpty() ? "" : "【" + traceable.value() + "】";
        String methodName = pjp.getSignature().toShortString();
        Object[] args = pjp.getArgs();

        log.info("{}开始执行 {},参数:{}", description, methodName, args);
        long start = System.currentTimeMillis();
        try {
            Object result = pjp.proceed();
            long elapsed = System.currentTimeMillis() - start;
            log.info("{}执行完成 {},返回值:{},耗时:{} ms", description, methodName, result, elapsed);
            return result;
        } catch (Throwable e) {
            long elapsed = System.currentTimeMillis() - start;
            log.error("{}执行异常 {},耗时:{} ms,异常:{}", description, methodName, elapsed, e.getMessage());
            throw e;
        }
    }
}

3. 业务代码使用

@Service
public class OrderService {
    @Traceable("下单")
    public String placeOrder(String product, int qty) {
        // 业务逻辑
        return "ORDER-20250316-001";
    }
}

运行后日志输出类似:

[INFO] 【下单】开始执行 OrderService.placeOrder(..),参数:[iPhone, 2]
[INFO] 【下单】执行完成 OrderService.placeOrder(..),返回值:ORDER-20250316-001,耗时:152 ms

9.1.7 实用注意事项

1. 自调用不会触发 AOP

Spring AOP 基于代理实现,当同一个类内部的方法互相调用时(this.methodB()),this 指向的是目标对象而非代理对象,因此切面不会生效。解决方案:

  • 将需要增强的方法移动到另一个 Bean 中(推荐)。
  • 通过 AopContext.currentProxy() 获取当前代理对象,然后调用 ((MyService) AopContext.currentProxy()).methodB(),同时需在配置类上开启 @EnableAspectJAutoProxy(exposeProxy = true)

2. 切点表达式越精确越好

避免使用过于宽泛的表达式(如 execution( (..)) 配合 within(*)),以免误拦容器初始化、销毁、toString() 等方法,导致性能下降或意外行为。

3. 异常不要随意吞掉

在环绕通知中,若捕获异常后没有重新抛出,目标方法声明的受检异常会被“吞掉”,调用方感知不到。除非你明确知道自己在做什么,否则正常流程中 throw e 是必需的。

4. 代理机制的限制

  • Spring AOP 只能拦截 public 方法,protectedprivate 方法无法拦截。
  • CGLIB 代理时,finalstatic 方法无法被拦截。
  • 构造函数无法被拦截。

5. 与事务切面的协同

Spring 自身的事务管理 @Transactional 也是通过 AOP 实现的。当你同时使用自定义切面和事务时,需要注意 @Order 顺序。通常事务切面优先级较高(早于业务切面开启事务),必要时可用 @Order(Ordered.LOWEST_PRECEDENCE - 1) 调整。

9.1.8 小结

注解式 AOP 开发极大简化了横切逻辑的实现。核心步骤为:

  1. 启用 AOP 代理(@EnableAspectJAutoProxy 或 Spring Boot 自动配置)。
  2. 定义切面类(@Aspect + @Component)。
  3. 声明切点(@Pointcut + 表达式)。
  4. 编写通知方法,选择合适的通知注解(@Before@After@AfterReturning@AfterThrowing@Around)。

掌握切点表达式的书写和五种通知的执行时机,就能应付 90% 以上的 AOP 场景。下一节将深入探讨 AOP 的底层代理原理,帮助你理解这一切是如何在运行时织入的。