Spring AOP 核心精讲

本文将全面拆解 Spring AOP 的核心概念、底层动态代理机制、五大术语、实战代码以及真实业务中的失效坑点。内容涵盖面试高频追问与开发落地指南,做到「面试能背、开发能用、场景全覆盖」。


一、 核心思想:OOP vs AOP(通俗理解与面试开场)

1. 面试满分开场白

AOP(面向切面编程)是一种横向抽取公共重复逻辑的编程思想。它可以在不修改原有业务代码的前提下,对目标方法动态增强日志、事务、权限校验、接口监控等通用功能。其底层核心实现依赖 Spring 的动态代理机制(JDK 动态代理与 CGLIB 代理)。

2. 通俗理解:OOP 与 AOP 的本质区别

OOP(面向对象编程):纵向拆分

  • 通俗比喻:造汽车。OOP 负责把汽车拆成发动机、底盘、轮胎等模块,分别制造,解决的是业务代码的分层与模块复用问题。
  • 局限性:如果所有汽车出厂前都需要“喷漆”和“安检”,OOP 只能在每个车间(业务方法)里都写一遍喷漆和安检代码,导致代码冗余。

AOP(面向切面编程):横向抽取

  • 通俗比喻:汽车出厂流水线上的“统一安检站”。AOP 不关心汽车怎么造,它只在汽车出厂的必经之路上横插一刀,统一进行安检和喷漆。解决的是分散在所有业务方法中的通用重复代码问题。

二、 AOP 五大核心术语(通俗释义 + 切点实操案例)

统一业务场景:使用 AOP 给所有 Service 层方法统一添加日志监控与权限校验。

1. 连接点(JoinPoint)

  • 定义:程序中可以被 AOP 拦截、可以被增强的所有候选位置。在 Spring AOP 中,特指所有 public 业务方法。
  • 通俗比喻:工厂里所有可以安装安检门的地方(大门、侧门、后门)。
  • 举例UserServiceaddUser()updateUser()deleteUser() 全部都是连接点。

2. 切点(Pointcut)

  • 定义:通过切点表达式,从所有连接点中筛选出来、真正要拦截增强的方法集合
  • 通俗比喻:经过评估,决定只在大门和侧门安装安检门,这就是切点。
  • 核心区别:连接点是“所有候选”,切点是“最终选中生效的”。

切点表达式实操案例(开发最常用)

// 1. execution:按方法签名匹配(最基础)
// 拦截 com.xxx.service 包及其子包下,所有类的所有方法
@Pointcut("execution(* com.xxx.service..*.*(..))")
public void servicePointCut() {}

// 2. @annotation:按自定义注解匹配(企业开发最常用、最灵活)
// 拦截所有标注了 @RequireLogin 注解的方法
@Pointcut("@annotation(com.xxx.annotation.RequireLogin)")
public void loginPointCut() {}

// 3. within:按类匹配
// 拦截 com.xxx.controller 包下所有类的所有方法
@Pointcut("within(com.xxx.controller..*)")
public void controllerPointCut() {}

// 4. 组合切点:使用 &&、||、! 进行逻辑运算
// 拦截 service 包下,且带有 @RequireLogin 注解的方法
@Pointcut("servicePointCut() && @annotation(com.xxx.annotation.RequireLogin)")
public void combinedPointCut() {}

3. 通知(Advice)

  • 定义:拦截到切点方法后,需要执行的增强逻辑代码(AOP 真正要做的事)。
  • 通俗比喻:安检员的具体动作(查身份证、过安检机、登记信息)。
通知注解执行时机核心用途
@Before目标方法执行前权限校验、参数打印、前置参数校验
@AfterReturning目标方法正常执行成功返回后正常日志记录、返回值数据脱敏/后置处理
@AfterThrowing目标方法抛出异常时异常日志记录、告警通知、事务回滚辅助
@After目标方法最终执行完毕(无论成功/异常)资源释放、ThreadLocal 清理、统一收尾操作
@Around包裹目标方法,全权控制执行流程性能计时、事务控制、分布式锁、全流程拦截(最强通知)

4. 切面(Aspect)

  • 定义切点 + 通知 的结合体,是存放所有 AOP 增强逻辑的工具类。
  • 通俗比喻:安检站的整体方案(包含在哪里安检、安检员做什么动作)。
  • 本质:被 @Aspect + @Component 标记的普通 Java 类。

5. 目标对象(Target)与 代理对象(Proxy)

  • 目标对象:被 AOP 增强的原始业务对象(如原生的 UserServiceImpl)。
  • 代理对象:Spring 自动生成的代理实例。客户端所有调用都会先走代理对象,由代理对象触发切面逻辑,再调用目标业务方法。

三、 底层核心:两种动态代理机制与选择规则

AOP 的所有功能完全依赖动态代理实现。Spring 会根据目标类的特征自动选择代理方式。

1. 代理机制对比

维度JDK 动态代理CGLIB 动态代理
核心规则基于接口代理基于继承子类代理
生效条件目标类必须实现接口无需实现接口,通过生成子类重写方法增强
底层原理利用反射机制生成一个实现代理接口的匿名类利用 ASM 字节码技术生成一个被代理类的子类
失效场景目标类无接口时无法使用目标类或方法被 final 修饰时无法重写,导致失效
性能表现生成代理类速度快,运行时反射调用稍慢生成代理类速度慢,运行时直接调用方法,性能更高

2. Spring 代理选择规则(面试必背)

  1. 目标类有接口:默认优先使用 JDK 动态代理。
  2. 目标类无接口:自动使用 CGLIB 代理。
  3. Spring Boot 2.0 及以上版本:默认强制使用 CGLIB 代理(通过 spring.aop.proxy-target-class=true 配置),以避免接口新增方法导致代理失效的问题。

3. 动态代理执行流程图

目标业务对象 AOP 切面逻辑 Spring 代理对象 客户端调用 目标业务对象 AOP 切面逻辑 Spring 代理对象 客户端调用 1. 调用业务方法 (如 userService.add()) 2. 执行前置通知 (@Before) 3. 前置逻辑完成 4. 反射调用真实目标方法 5. 返回业务结果 6. 执行后置/返回通知 (@After/@AfterReturning) 7. 后置逻辑完成 8. 返回最终结果

四、 完整可落地 AOP 实战代码

1. 自定义注解(企业级开发最常用方式)

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
    String module() default "";
    String action() default "";
}

2. 核心切面类(包含 5 种通知与自定义注解切点)

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;

@Aspect
@Component
public class OperateLogAspect {

    // 切点:拦截所有标注了 @OperateLog 的方法
    @Pointcut("@annotation(com.xxx.annotation.OperateLog)")
    public void logPointCut() {}

    // 1. 前置通知
    @Before("logPointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("[Before] 方法开始执行:" + joinPoint.getSignature().getName() 
                + ",参数:" + Arrays.toString(joinPoint.getArgs()));
    }

    // 2. 正常返回通知
    @AfterReturning(value = "logPointCut()", returning = "result")
    public void afterReturn(Object result) {
        System.out.println("[AfterReturning] 方法执行成功,返回结果:" + result);
    }

    // 3. 异常通知
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void afterThrow(Exception e) {
        System.out.println("[AfterThrowing] 方法执行异常,异常信息:" + e.getMessage());
    }

    // 4. 后置通知(最终通知)
    @After("logPointCut()")
    public void after() {
        System.out.println("[After] 方法执行结束,清理 ThreadLocal 等资源");
    }

    // 5. 环绕通知(最强通知,手动控制目标方法执行)
    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[Around-Pre] 开始统计方法耗时");
        long start = System.currentTimeMillis();
        
        try {
            // 核心:手动执行目标业务方法
            Object result = pjp.proceed();
            return result;
        } finally {
            long time = System.currentTimeMillis() - start;
            System.out.println("[Around-Post] 方法执行耗时:" + time + "ms");
        }
    }
}

3. 业务 Service 测试

@Service
public class UserService {
    
    @OperateLog(module = "用户管理", action = "新增用户")
    public String addUser(String username) {
        System.out.println(">>> 执行核心业务:新增用户 " + username);
        return "新增成功:" + username;
    }
}

五、 核心业务场景:@Transactional 事务底层剖析-详细可见@EnableTanstional源码详解

@Transactional 是 Spring AOP 最经典、最核心的应用。面试中常问:“为什么加了 @Transactional 就能控制事务?”

1. 事务 AOP 底层执行流程

Spring 的事务管理完全基于 AOP 的环绕通知(@Around) 实现。底层核心类是 TransactionInterceptor

无异常

有异常

匹配

不匹配

客户端调用带 @Transactional 的方法

Spring 代理对象拦截

环绕通知前置逻辑

获取数据库连接

关闭自动提交 conn.setAutoCommit false

开启事务

执行目标业务代码 SQL

业务代码是否抛出异常?

环绕通知后置逻辑: 提交事务 commit

异常通知逻辑: 判断异常类型

是否匹配 rollbackFor?

回滚事务 rollback

提交事务 commit

释放数据库连接

2. @Transactional 常见踩坑点(面试必问)

  1. 非 public 方法失效:Spring AOP 默认只拦截 public 方法,protected/private/default 方法上加事务无效。
  2. 同类内部调用失效:类内部 A 方法调用 B 方法(B 有事务),因为绕过了代理对象,直接调用了原生对象,事务失效。
  3. 异常被 try-catch 吞掉:业务代码中 catch 了异常且没有重新抛出,AOP 切面感知不到异常,导致事务无法回滚。
  4. 异常类型不匹配:默认只回滚 RuntimeExceptionError。如果抛出的是受检异常(如 ExceptionIOException),必须显式配置 @Transactional(rollbackFor = Exception.class)
  5. 数据库引擎不支持:如 MySQL 使用了 MyISAM 引擎(不支持事务),必须改为 InnoDB。

六、 AOP 完整执行顺序(高级面试考点)

注意:Spring 版本的升级导致了执行顺序的变化,这是区分初级和中高级候选人的关键点。

1. Spring 4.x 版本(旧版顺序)

环绕前置 -> 前置 -> 目标方法 -> 环绕后置 -> 后置 -> 返回/异常

2. Spring 5.x / Spring Boot 2.x 及以上版本(现行标准顺序)

Spring 官方在 5.x 版本中修复了通知执行的顺序,使其更符合直觉(环绕通知真正“环绕”了所有其他通知)。

标准执行链路

  1. @Around 前置部分(pjp.proceed() 之前的代码)
  2. @Before 前置通知
  3. 目标业务方法执行
  4. @Around 后置部分(pjp.proceed() 之后的代码)
  5. @After 最终后置通知
  6. @AfterReturning(正常) 或 @AfterThrowing(异常)

七、 AOP 失效真实业务坑点与解决方案

坑点 1:同类内部方法调用(最高频)

  • 现象:同一个类中,方法 A(无事务/无切面)调用方法 B(有事务/有切面),方法 B 的 AOP 增强失效。
  • 原因:AOP 基于代理对象拦截。内部调用时,使用的是 this 关键字(即目标对象本身),绕过了代理对象。
  • 解决方案
    1. 方案一(推荐):将方法 B 抽离到另一个 Service 类中,通过依赖注入调用。
    2. 方案二:在当前类中注入自己(@Autowired private UserService self;),通过 self.methodB() 调用。
    3. 方案三:使用 AopContext.currentProxy() 获取当前代理对象:((UserService) AopContext.currentProxy()).methodB()(需在启动类添加 @EnableAspectJAutoProxy(exposeProxy = true))。

坑点 2:方法被 final 或 static 修饰

  • 现象:切面配置正确,但目标方法未被拦截。
  • 原因:CGLIB 基于继承生成子类,无法重写 final 方法;JDK 代理基于接口,无法代理 static 方法。
  • 解决方案:移除目标方法上的 finalstatic 修饰符,改为普通的 public 实例方法。

坑点 3:切面类未被 Spring 管理

  • 现象:切面类写了,但毫无反应。
  • 原因:只加了 @Aspect,忘记加 @Component,导致切面类本身没有被注册为 Spring Bean。
  • 解决方案:确保切面类上同时存在 @Aspect@Component(或 @Configuration)。

坑点 4:多线程环境导致事务/AOP 失效

  • 现象:在 @Transactional 方法内部开启新线程执行 SQL,新线程的 SQL 不在事务控制范围内。
  • 原因:数据库连接(Connection)是绑定在当前线程的 ThreadLocal 中的。新线程无法获取原线程的事务连接。
  • 解决方案:避免在事务方法中开启新线程执行数据库操作;如果必须,需使用编程式事务或传递连接对象(极不推荐)。

八、 面试高频问答(满分模板)

问 1:切点(Pointcut)和连接点(JoinPoint)的区别是什么?

:连接点是程序中所有可以被拦截的候选方法(在 Spring 中指所有 public 方法);切点是通过表达式从连接点中筛选出来的、真正需要被增强的方法集合。连接点是“范围”,切点是“结果”。

问 2:Spring AOP 和 AspectJ 有什么区别?

:Spring AOP 是运行时织入,基于动态代理实现,不需要额外的编译过程,但只能代理 Spring 管理的 Bean,且只能拦截方法级别;AspectJ 是编译时/类加载时织入,基于字节码修改,性能更高,功能更强大(支持字段、构造器拦截),但需要专门的编译器(ajc)。日常企业开发 Spring AOP 已完全足够。

问 3:为什么 Spring Boot 2.0 之后默认使用 CGLIB 代理?

:JDK 动态代理要求目标类必须实现接口,且如果接口新增方法,代理类可能需要同步修改。CGLIB 基于继承,不强制要求接口,适用性更广。为了减少因代理方式切换带来的潜在 Bug,Spring Boot 2.0 开始默认强制使用 CGLIB。

问 4:@Transactional 为什么是 AOP 的典型应用?

:Spring 事务底层完全基于 AOP 环绕通知实现。它通过动态代理拦截业务方法,在方法执行前获取连接并开启事务,正常结束提交事务,抛出异常回滚事务。全程不修改业务代码,完美契合 AOP 横向抽离公共逻辑的核心思想。


九、 极简终极记忆口诀

连接点是候选,切点挑一批;
通知加逻辑,切面装一起;
代理来执行,原类是目标;
事务日志监控,全靠 AOP 提。
内部调用绕代理,final 方法难重写;
Boot 默认 CGLIB,执行顺序五变六。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐