Spring AOP 原理

注意
本文最后更新于 2025-05-07,文中内容可能已过时。

核心本质:面向切面编程,通过代理模式,在不修改目标方法代码的前提下,对方法进行增强(如日志记录、权限校验、事务控制),实现“关注点分离”(业务逻辑与非业务逻辑分离)。

一、AOP 核心概念 🔴

5个核心概念,用“一句话+通俗解释”记,避免混淆,重点区分「连接点」和「切点」。

核心概念 官方定义 通俗解释 面试关键点
连接点(JoinPoint) 程序执行过程中可被拦截的点(如方法调用、字段赋值、异常抛出) 所有可能被增强的“位置”,Spring AOP 中仅支持「方法级连接点」 范围最广,是“候选增强点”
切点(Pointcut) 从所有连接点中筛选出的、需要被增强的点(通过表达式指定) 真正要增强的“具体方法”,比如“所有 Service 类的 add 方法” 范围比连接点小,是“选中的增强点”,常用 execution 表达式
通知(Advice) 在切点处执行的增强逻辑(如日志、权限校验) “增强的具体代码”,比如“在 add 方法执行前打印日志” 分5种类型,重点记执行顺序
切面(Aspect) 切点 + 通知的组合,封装了“什么时候增强、增强什么、怎么增强” 一个完整的增强逻辑类,比如“日志切面”(包含切点和日志通知) AOP 的核心载体,将通知和切点绑定
织入(Weaving) 将切面的增强逻辑融入到目标对象的过程 Spring 自动完成,将通知代码“织入”到目标方法的执行流程中 Spring AOP 是“运行时织入”,基于动态代理实现

连接点是“所有可能的点”,切点是“选中的点”,通知是“点上的操作”,切面是“点+操作的组合”,织入是“把操作放到点上”。

二、两种代理方式:JDK 动态代理 vs CGLIB 代理(核心难点)

Spring AOP 的底层实现依赖「动态代理」,默认逻辑:目标对象有接口 → JDK 动态代理;无接口 → CGLIB 代理(可通过配置强制使用 CGLIB)。

1. 核心区别与对比 🔴

对比维度 JDK 动态代理 CGLIB 代理
底层原理 基于 Java 反射机制,生成目标接口的代理类(实现目标接口) 基于 ASM 字节码技术,生成目标类的子类(继承目标类)
依赖条件 必须有目标接口(无接口则无法使用) 无需接口,直接代理类(可代理无接口的类)
代理对象 代理类实现目标接口,与目标对象“平级”(不是子类) 代理类继承目标类,是目标对象的“子类”
局限性 无法代理无接口的类;无法代理接口中的 static/final 方法 无法代理 final 类(不能继承);无法代理 final 方法(不能重写)
性能 反射效率较低(JDK 8 后优化明显,与 CGLIB 差距缩小) 字节码生成效率高,运行时性能优于 JDK 代理(生成子类,直接调用方法)
Spring 默认选择 目标对象有接口时,优先使用 目标对象无接口时,自动使用;也可通过配置强制使用

2. 适用场景 🔴

  • JDK 动态代理:目标对象有接口(如 Service 接口+实现类),是 Spring 默认首选;

  • CGLIB 代理:目标对象无接口(如普通 POJO 类),或需要代理类的非接口方法,可手动配置开启(spring.aop.proxy-target-class=true)。

三、代理对象创建时机 🟠

核心结论:默认是“容器启动时创建”(预加载),但懒加载场景下,第一次使用时创建,与 Bean 的懒加载(@Lazy)绑定。

  1. 默认场景(无 @Lazy):

    • Spring 容器启动时,会初始化所有单例 Bean;

    • 如果 Bean 被切面增强(需要代理),容器会在创建目标 Bean 后,立即生成代理对象,并存入容器;

    • 后续获取 Bean 时,拿到的是“代理对象”,而非目标对象。

  2. 懒加载场景(@Lazy 注解):

    • 容器启动时,不创建目标 Bean 和代理对象;

    • 第一次调用该 Bean 的方法时,才会创建目标 Bean,生成代理对象,并返回代理对象;

    • 后续调用,直接复用已创建的代理对象。

关键提醒:无论哪种时机,最终从容器中获取的都是代理对象,目标对象被代理对象包裹,增强逻辑在代理对象中执行。

四、五种通知类型及执行顺序 🟠

Spring AOP 支持5种通知类型,核心:「正常执行」和「异常执行」两种场景的顺序,尤其是 @Around 通知的特殊性(环绕通知,可控制目标方法执行)。

1. 五种通知类型(按常用程度排序)

通知类型 注解 执行时机 核心作用
环绕通知 @Around 目标方法执行前后,可控制目标方法是否执行、何时执行 最强大的通知,可实现日志、事务、权限等所有增强逻辑
前置通知 @Before 目标方法执行之前执行 做前置准备(如参数校验、日志记录开始)
后置通知 @After 目标方法执行之后执行(无论成功/失败,都会执行) 做清理工作(如关闭资源、日志记录结束)
返回通知 @AfterReturning 目标方法正常执行完成后执行(异常时不执行) 处理返回结果(如打印返回值、结果校验)
异常通知 @AfterThrowing 目标方法抛出异常后执行(正常执行时不执行) 处理异常(如异常日志、异常回滚)

2. 执行顺序 🔴

场景1:目标方法正常执行

顺序:@Around(前置) → @Before → 目标方法 → @Around(后置) → @After → @AfterReturning

场景2:目标方法抛出异常

顺序:@Around(前置) → @Before → 目标方法 → @Around(异常处理) → @After → @AfterThrowing

记忆口诀

环绕先前后,前置在目标前;后置必执行,返回异常二选一(正常返回、异常抛错,二选一执行)。

关键提醒

  • @Around 是唯一能控制目标方法执行的通知(通过 ProceedingJoinPoint.proceed() 调用目标方法);

  • @After 无论成功失败都执行,优先级高于 @AfterReturning 和 @AfterThrowing;

  • 多个同类型通知,可通过 @Order 注解指定执行顺序(值越小,执行越早)。

五、AOP 失效场景 🔴

核心原因:AOP 基于动态代理实现,若目标方法无法被代理对象拦截,AOP 增强就会失效,常见6种场景,重点前3种。

1. 内部调用(最常见)

场景:同一个 Bean 中,方法 A 调用方法 B,而方法 B 被 AOP 增强,此时增强逻辑不生效。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Service
public class UserService {
    // 方法 A(无增强)
    public void addUser() {
        // 内部调用方法 B,未通过代理对象调用
        this.updateUser(); 
    }
    
    // 方法 B(被 AOP 增强,如日志)
    @Log
    public void updateUser() {
        // 业务逻辑
    }
}

原因:this 指向的是“目标对象”,而非“代理对象”,内部调用未走代理对象的拦截逻辑,AOP 无法织入增强。

解决方案:通过 ApplicationContext 获取代理对象,或用 @Autowired 注入自身(代理对象),再调用方法。

2. Final 方法 / Static 方法(无法代理)

  • final 方法:CGLIB 无法继承 final 类、重写 final 方法,JDK 代理也无法代理 final 方法,增强失效;

  • static 方法:动态代理(JDK/CGLIB)仅能代理实例方法,static 方法属于类级别的方法,无法被代理,增强失效。

解决方案:避免对 final、static 方法做 AOP 增强,或修改方法修饰符。

3. 非 Public 方法(默认不代理)

Spring AOP 默认只对 public 方法进行代理,private、protected、default 修饰的方法,AOP 增强不生效。

原因:Spring AOP 的切点表达式(如 execution(public * com..*()))默认匹配 public 方法,非 public 方法无法被切点匹配。

解决方案:修改方法为 public,或自定义切点表达式匹配非 public 方法(不推荐,不符合规范)。

4. 其他失效场景(了解即可)

  • 目标对象不是 Spring 容器管理的 Bean(如手动 new 的对象,未被 IOC 管理,无法生成代理);

  • 切面未被 Spring 扫描到(如未加 @Aspect、@Component 注解);

  • 切点表达式错误(未匹配到目标方法,增强逻辑无法织入)。

六、自定义切面实现方式(注解式 + 接口式)

实际开发中,注解式切面最常用(简洁、灵活),接口式切面(XML 配置)已基本淘汰,重点掌握注解式实现。

1. 注解式切面(推荐)

核心步骤:加注解(@Aspect + @Component)→ 定义切点 → 定义通知,直接上可运行代码(默写专用)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 1. 切面类(必须加 @Aspect + @Component,让 Spring 扫描到)
@Component
@Aspect
public class LogAspect {
    // 2. 定义切点(execution 表达式,匹配目标方法)
    // 表达式含义:所有 com.example.service 包下的 public 方法
    @Pointcut("execution(public * com.example.service..*(..))")
    public void logPointcut() {} // 切点标识方法(无实际逻辑)
    
    // 3. 定义通知(五种通知,结合切点)
    // 前置通知
    @Before("logPointcut()")
    public void beforeLog(JoinPoint joinPoint) {
        // 可获取方法名、参数等信息
        String methodName = joinPoint.getSignature().getName();
        System.out.println("前置通知:方法 " + methodName + " 开始执行");
    }
    
    // 环绕通知(核心,可控制目标方法执行)
    @Around("logPointcut()")
    public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知:方法执行前");
        // 调用目标方法(必须有,否则目标方法不执行)
        Object result = joinPoint.proceed(); 
        System.out.println("环绕通知:方法执行后");
        return result; // 返回目标方法的返回值
    }
    
    // 后置通知
    @After("logPointcut()")
    public void afterLog() {
        System.out.println("后置通知:方法执行结束(无论成功失败)");
    }
    
    // 返回通知
    @AfterReturning(pointcut = "logPointcut()", returning = "result")
    public void afterReturningLog(Object result) {
        System.out.println("返回通知:方法正常返回,返回值:" + result);
    }
    
    // 异常通知
    @AfterThrowing(pointcut = "logPointcut()", throwing = "e")
    public void afterThrowingLog(Exception e) {
        System.out.println("异常通知:方法抛出异常,异常信息:" + e.getMessage());
    }
}

关键提醒:@Pointcut 注解的切点表达式,是核心,常用 execution 表达式,格式:execution(访问修饰符 返回值 包名.类名.方法名(参数))。

2. 接口式切面(XML 配置,了解即可)

通过实现 Spring 提供的通知接口(如 MethodBeforeAdvice、AfterReturningAdvice),再通过 XML 配置切面、切点,现已基本不用,简化示例:

1
2
3
4
5
6
7
8
9
// 1. 实现通知接口
public class LogAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("前置通知:" + method.getName() + " 方法开始执行");
    }
}

// 2. XML 配置切面省略核心是配置切点和通知的关联

总结 🔴

  1. AOP 核心概念:连接点(所有可能点)、切点(选中的点)、通知(增强逻辑)、切面(切点+通知)、织入(整合过程);

  2. 代理方式:有接口用 JDK 动态代理,无接口用 CGLIB 代理,核心区别是“实现接口”vs“继承类”;

  3. 代理创建时机:默认容器启动时创建,@Lazy 注解下第一次使用时创建;

  4. 通知顺序:正常执行(环绕前→前置→目标→环绕后→后置→返回),异常执行(环绕前→前置→目标→环绕异常→后置→异常);

  5. 失效场景:内部调用、final/static 方法、非 public 方法、非 Spring 管理的 Bean;

  6. 自定义切面:注解式(@Aspect + @Component + 切点 + 通知)最常用,可直接默写核心代码。