人人都会AI编程

5.1 AOP 核心概念:切面、通知、切点、连接点、织入、引入

更新时间:2026-07-10

前面讲到 Spring 通过动态代理实现了 AOP 的底层机制,而想要真正用好 AOP,首先需要理解它的一整套概念体系。这些术语看似抽象,但一旦结合代码理解,就会发现它们只是对横切逻辑的“解剖零件”,每一个零件都有明确的分工。

5.1.1 连接点(Join point)

连接点是程序执行过程中能够插入切面的一个“时机点”。在 Spring AOP 中,连接点仅限于方法的执行——即调用某个方法的那个瞬间。

  • 比如执行 OrderService.placeOrder() 时,这个方法调用本身就是一个连接点。
  • 一个类中的每个方法,在运行时都是一个潜在的连接点。

但并不是所有连接点都会被增强,只有匹配到切点规则的连接点,才会被织入通知逻辑。

5.1.2 切点(Pointcut)

切点的作用是筛选连接点。它通过表达式定义“哪些方法需要被增强”。

常见切点表达式

Spring AOP 最常用的切点指示器是 execution(),其格式为:

execution(修饰符 返回类型 包.类.方法(参数))

例如:

  • execution( com.example.service..*(..))

匹配 com.example.service 包下所有类的所有方法(任意返回类型、任意参数)。

  • execution(public com.example...find*(..))

匹配 com.example 及其子包中所有 public、以 find 开头的方法。

  • execution(* com.example.service.OrderService.placeOrder(Long,..))

匹配 OrderService.placeOrder 方法,第一个参数为 Long 类型,后面可带任意参数。

除了 execution,还可以使用 @annotation 基于自定义注解定义切点,例如对所有标注了 @Log 的方法进行增强:

@Pointcut("@annotation(com.example.annotation.Log)")
public void logAnnotatedMethods() {}

切点的组合

可以通过 &&(且)、||(或)、!(非)将多个切点组合,例如:

@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* *.get*(..))")
public void serviceMethodsExcludingGetters() {}

切点就像一把筛子,从茫茫方法连接点中筛选出你需要织入的目标。

5.1.3 通知(Advice)

通知定义了在切点选中的连接点上“做什么”以及“何时做”。它包含了具体的横切逻辑。

Spring 提供了五种通知类型,下表给出了它们与连接点的时序关系:

| 通知类型 | 注解 | 执行时机 |
|---------|------|---------|
| 前置通知 | @Before | 目标方法执行之前 |
| 后置通知 | @After | 目标方法执行之后(无论是否抛出异常) |
| 返回后通知 | @AfterReturning | 目标方法正常返回之后 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常之后 |
| 环绕通知 | @Around | 包裹目标方法,可控制方法是否执行、修改参数、修改返回值或捕获异常 |

1. 前置通知示例

@Aspect
public class LogAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("开始执行:" + joinPoint.getSignature());
    }
}

2. 返回后通知示例(可以获取返回值)

@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
    System.out.println("方法返回:" + result);
}

3. 异常通知示例(可以获取异常信息)

@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
    System.out.println("方法抛异常:" + ex.getMessage());
}

4. 环绕通知(功能最强,需手动调用 proceed)

@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("方法执行前");
    Object result = joinPoint.proceed(); // 必须调用,否则目标方法不会执行
    System.out.println("方法执行后");
    return result;
}

环绕通知能力最全面,但也最需要谨慎使用:忘记调用 proceed() 会导致目标方法被“吞掉”,不当的异常处理可能掩盖实际错误。

5.1.4 切面(Aspect)

切面 = 切点 + 通知,是横切逻辑的完整模块。一个切面类用 @Aspect 标注,内部包含切点定义和若干通知方法。

完整的切面示例

@Aspect
@Component
public class PerformanceAspect {

    // 定义切点
    @Pointcut("execution(* com.example.service.*.*(..))")
    private void serviceLayer() {}

    @Around("serviceLayer()")
    public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long time = System.currentTimeMillis() - start;
        System.out.println(joinPoint.getSignature() + " 耗时 " + time + "ms");
        return result;
    }
}

这个切面将“性能统计”的横切逻辑封装到一个独立的类中,通过 @Pointcut 定义了筛选规则,通过 @Around 实现了统计逻辑。业务代码中完全没有与性能相关的代码,却获得了方法耗时统计的能力。

5.1.5 织入(Weaving)

织入是将切面应用到目标对象从而创建代理对象的过程。这个过程对开发者是透明的。

Spring AOP 采用运行期织入,在 IoC 容器初始化 Bean 时,检查该 Bean 是否匹配某个切面的切点。如果匹配,则通过动态代理(JDK 动态代理或 CGLIB)创建代理对象,将通知逻辑织入到方法调用的前后。

织入的时机有三种对比:

  • 编译期织入:需要特殊编译器(AspectJ),将切面嵌入 .class 文件。
  • 类加载期织入:通过类加载器的增强实现。
  • 运行期织入:Spring AOP 采用的方式,基于动态代理,无需修改字节码,使用灵活,但只支持方法级别的连接点。

对于绝大多数企业应用来说,运行期织入提供的“方法拦截”已经足够覆盖事务、日志、安全等需求。

5.1.6 引入(Introduction)

引入是一种特殊类型的通知,它可以让目标类在运行期动态实现额外的接口,并增加新的方法

假如我们有一个 OrderService 类,它本身没有实现 Auditable 接口,但我们想在不修改其源码的情况下,让它具备审计能力。可以通过 @DeclareParents 实现:

@Aspect
public class AuditableIntroduction {
    @DeclareParents(value = "com.example.service.*", 
                    defaultImpl = DefaultAuditable.class)
    public static Auditable auditable;
}

其中 Auditable 是我们定义的新接口,DefaultAuditable 是其默认实现。织入后,所有匹配的 service 包下的类都会“突然”实现了 Auditable 接口,并拥有 DefaultAuditable 中的方法实现。

实际应用场景

引入在实际业务中相对较少使用,但在框架开发或需要为大量已有类统一扩展功能时非常有用——比如给所有实体类增加“最后修改时间”的统一字段和访问方法,而无需修改每行已有代码。

5.1.7 概念串联:一张执行时序图

为了把以上概念串在一起,假设我们定义了一个切面,包含一个前置通知和一个环绕通知,作用在服务层的 placeOrder 方法上。当调用发生时,执行顺序如下:

调用方 → 代理对象(已织入切面)
         │
         ├─ 执行切点匹配
         ├─ 环绕通知 before 部分
         ├─ 前置通知
         ├─ 目标方法 placeOrder()
         ├─ 环绕通知 after 部分(正常返回)
         └─ 后置/最终通知(如 @After)

每个通知围绕着连接点(方法调用)形成了一条链,切点负责筛选,通知负责逻辑,切面负责封装,而这一切由织入过程在背后完成。

掌握这六个核心概念,你就已经能够读懂、编写绝大多数日常 AOP 切面。在后续的实践中,我们将看到如何用这些概念快速构建权限校验、日志记录、方法缓存等真实场景的切面,那时这些术语会变得更加自然。