掌握AOP,不只是会用@Aspect,更要懂原理、会面试
在Spring框架的两大核心思想中,IoC(控制反转)让对象管理变得简单,而AOP(面向切面编程)则让代码复用达到了新的高度。不少开发者在使用Spring AOP时,常常陷入“只会加注解、不懂底层原理”的困境——能写出@Around拦截日志,却说不出JDK动态代理和CGLIB的区别;知道@Transactional能管理事务,却答不上来为什么同一个类中的方法调用事务会失效。今天,AI助手达达就带大家从头梳理Spring AOP的核心知识点,从概念到代码再到底层原理,帮你建立完整的知识链路。

一、痛点切入:传统OOP的困境
业务代码里的“重复劳动”

假设你正在开发一个电商系统,有登录、下单、支付、查询等多个业务方法。每个方法你都想加上日志打印、权限校验、性能监控和事务控制。如果用传统的OOP方式,代码会变成这样:
@Service public class OrderService { public void createOrder(Order order) { // 日志记录 System.out.println("开始创建订单,参数:" + order); // 权限校验 if (!hasPermission()) { throw new RuntimeException("无权限"); } // 性能监控 long start = System.currentTimeMillis(); try { // 核心业务逻辑 doCreateOrder(order); // 事务提交 } catch (Exception e) { // 事务回滚 } finally { // 性能记录 System.out.println("耗时:" + (System.currentTimeMillis() - start)); } } public void cancelOrder(Long id) { // 同样的日志、权限、监控代码……(重复!) } }
痛点分析
上面这种写法存在三大致命问题:
代码重复率极高——每个方法都要写一遍横切逻辑,据统计传统OOP在日志、事务等场景的代码重复率可高达60%以上-11。
耦合严重——业务代码与横切逻辑紧密交织,修改日志格式或权限规则,需要改动所有业务方法。
扩展性差——要新增一种横切逻辑(如限流),又得在每个方法里加一遍。
正是在这种背景下,AOP(面向切面编程) 应运而生,它允许开发者在不修改源代码的前提下,给程序动态添加扩展功能,作为面向对象编程的补充,提高模块的内聚性,降低模块间的耦合-7。
二、核心概念讲解:AOP中的六个关键词
AOP全称 Aspect Oriented Programming,即面向切面编程,是Spring框架的两大核心技术之一(另一个是IoC)-1。要理解AOP,必须先掌握以下六个核心概念:
① 连接点(Join Point)
连接点是程序执行过程中可以插入切面逻辑的“候选位置”。在Spring AOP中,简单来说就是所有可以被增强的方法——默认情况下,IoC容器中Bean的public方法都可作为连接点-7。
② 切点(Pointcut)
切点是一个匹配规则,用来从众多连接点中筛选出“真正需要增强的目标方法”。你可以把它理解为一份“增强名单”,只有匹配切点表达式的方法才会被织入切面逻辑-7。
③ 通知(Advice)
通知定义了增强逻辑在目标方法的什么时机执行。Spring AOP提供了五种通知类型-12:
| 通知类型 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行前 |
| 后置通知 | @After | 目标方法执行后(无论是否异常) |
| 返回通知 | @AfterReturning | 目标方法正常返回后 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常时 |
| 环绕通知 | @Around | 包裹整个方法调用,前后均可控制 |
④ 切面(Aspect)
切面是封装横切关注点的模块,它把切点(在哪里增强)和通知(何时增强、怎样增强)组合在一起,形成一个完整的增强单元-12。例如一个日志切面,就是“在service包下的所有方法执行前后打印日志”。
⑤ 目标对象(Target Object)
被代理的原始业务对象,也就是包含核心业务逻辑的那个对象。
⑥ 织入(Weaving)
把切面逻辑应用到目标对象并创建代理对象的过程。Spring AOP采用的是运行时织入,即在程序运行期间动态生成代理对象。
三、Spring AOP与AspectJ的关系
很多初学者会混淆Spring AOP和AspectJ。需要厘清的是:AspectJ是功能更强大的独立AOP框架,而Spring AOP借鉴了AspectJ的注解风格。
| 对比维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行时动态代理 | 编译时或类加载时 |
| 依赖关系 | 依赖Spring容器 | 独立使用,不依赖Spring |
| 连接点范围 | 仅方法级别 | 支持字段、构造器、静态代码块等 |
| 性能 | 略低(运行时生成代理) | 更高(编译时优化) |
| 配置方式 | 注解 + XML | 需单独编译器ajc |
一句话总结:Spring AOP是轻量级的运行时AOP方案,适用于对Spring Bean方法进行拦截;AspectJ是重量级的完整AOP框架,功能更强大但配置更复杂-12。Spring AOP集成了AspectJ的注解语法,所以你在代码中看到的@Aspect、@Before这些注解,实际上来自AspectJ。
核心记忆:Spring AOP = 运行时增强 + 基于代理;AspectJ = 编译时增强 + 基于字节码操作
四、代码示例:用注解实现AOP
下面通过一个完整的Spring Boot示例,演示如何使用AOP实现方法耗时监控和日志记录。
4.1 添加依赖
<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
4.2 编写切面类
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect // 标记为切面类 @Component // 交给Spring管理 public class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); // ① 定义切点:匹配service包下所有类的所有方法 @Pointcut("execution( com.example.service..(..))") public void servicePointcut() {} // ② 前置通知:方法调用前记录 @Before("servicePointcut()") public void beforeMethod(JoinPoint joinPoint) { log.info("调用方法:{}.{}", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName()); } // ③ 环绕通知:监控方法执行耗时(最强大) @Around("servicePointcut()") public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); // 调用目标方法 —— 这一行必须写! Object result = joinPoint.proceed(); long costTime = System.currentTimeMillis() - startTime; log.info("方法 {} 执行耗时:{}ms", joinPoint.getSignature().getName(), costTime); return result; } // ④ 异常通知:记录异常信息 @AfterThrowing(value = "servicePointcut()", throwing = "e") public void afterThrowing(JoinPoint joinPoint, Exception e) { log.error("方法 {} 抛出异常:{}", joinPoint.getSignature().getName(), e.getMessage()); } }
4.3 目标业务类
@Service public class UserService { public String getUserInfo(Long id) { if (id == null || id <= 0) { throw new IllegalArgumentException("用户ID不能为空或负数"); } return "用户ID:" + id + ",姓名:张三"; } }
4.4 执行流程解析
当调用userService.getUserInfo(1L)时,执行顺序如下:
┌─────────────────────────────────────────────┐ │ ① 客户端调用 userService.getUserInfo(1L) │ └─────────────────┬───────────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ ② @Before前置通知:记录方法调用信息 │ └─────────────────┬───────────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ ③ @Around环绕通知前半段:startTime记录 │ └─────────────────┬───────────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ ④ joinPoint.proceed() → 执行目标方法 │ └─────────────────┬───────────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ ⑤ @Around环绕通知后半段:计算耗时并记录 │ └─────────────────┬───────────────────────────┘ ▼ ┌─────────────────────────────────────────────┐ │ ⑥ 返回结果给客户端 │ └─────────────────────────────────────────────┘
关键注意点:
@Around环绕通知中必须调用joinPoint.proceed(),否则目标方法不会执行@Around方法的返回值必须为Object类型,以接收原方法的返回值-1切面类必须被Spring管理,所以需要添加
@Component
五、底层原理:动态代理机制
5.1 设计思想基础:代理模式
Spring AOP的底层实现本质上依赖于代理模式这一经典设计模式——通过引入代理对象作为目标对象的中间层,实现对目标对象访问的控制与增强-29。代理模式的核心价值在于解耦核心业务逻辑与横切关注点。
5.2 Spring AOP的两种代理方案
Spring AOP根据目标类的特性,会智能选择使用JDK动态代理还是CGLIB代理-:
| 对比维度 | JDK动态代理 | CGLIB代理 |
|---|---|---|
| 使用条件 | 目标类实现了至少一个接口 | 目标类未实现接口,或强制指定 |
| 实现原理 | 基于接口,通过反射生成代理类 | 通过继承目标类生成子类代理 |
| 性能对比 | 较高 | 略低(需生成字节码) |
| 依赖 | JDK原生支持,无需额外包 | 需引入cglib包 |
选择决策树:Spring通过DefaultAopProxyFactory自动判断——若目标类无接口或配置proxyTargetClass=true,则使用CGLIB;否则使用JDK动态代理-。
5.3 JDK动态代理原理
JDK动态代理要求目标对象必须实现至少一个接口,核心涉及java.lang.reflect.Proxy类和InvocationHandler接口。当调用代理对象的方法时,调用会被转发到InvocationHandler.invoke()方法,在这个方法中可以插入前置、后置等增强逻辑-12。
5.4 CGLIB动态代理原理
CGLIB(Code Generation Library)通过字节码技术生成目标类的子类作为代理,并覆盖父类的方法来织入增强逻辑。final类或final方法无法被CGLIB代理。
5.5 两个重要陷阱
同一个类内部方法调用不会触发AOP:Spring AOP只能拦截通过代理对象进行的方法调用,同一个类中
this.methodB()这样的内部自调用不会走代理,因此不会被增强-。Spring AOP默认只对public方法生效:private、protected等方法无法被JDK或CGLIB正确拦截。
六、高频面试题与参考答案
Q1:请解释Spring AOP的底层实现原理是什么?
参考答案(踩分点:代理机制 → 两种方式 → 选择策略):
Spring AOP基于动态代理模式实现。当目标类实现了接口时,默认使用JDK动态代理,通过java.lang.reflect.Proxy生成接口的代理实例,调用通过InvocationHandler转发;当目标类没有实现接口时,使用CGLIB动态代理,通过字节码技术生成目标类的子类作为代理。Spring通过DefaultAopProxyFactory自动选择代理方式。
Q2:JDK动态代理和CGLIB有什么区别?性能上哪个更好?
参考答案(踩分点:条件 → 原理 → 性能):
| 区别 | JDK动态代理 | CGLIB |
|---|---|---|
| 使用条件 | 目标类必须有接口 | 目标类无接口或强制指定 |
| 实现原理 | 基于接口 + 反射 | 基于继承 + 字节码增强 |
| 性能 | 较高 | 略低 |
| 局限性 | 只能代理接口方法 | final类/方法无法代理 |
性能上,JDK动态代理通常优于CGLIB,因为CGLIB需要额外的字节码生成开销。
Q3:Spring AOP中通知有哪几种类型?
参考答案(踩分点:五种类型 + 各自时机):
@Before:前置通知,目标方法执行前执行@After:后置通知,目标方法执行后执行(无论是否异常)@AfterReturning:返回通知,目标方法正常返回后执行@AfterThrowing:异常通知,目标方法抛出异常后执行@Around:环绕通知,包裹整个方法调用,可控制执行流程
Q4:Spring AOP和AspectJ有什么区别?
参考答案(踩分点:织入时机 → 依赖 → 功能范围):
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行时动态代理 | 编译时/类加载时 |
| 依赖 | 依赖Spring容器 | 独立,不依赖Spring |
| 连接点 | 仅方法级别 | 字段、构造器、静态代码块等 |
| 性能 | 略低 | 更高 |
Spring AOP集成了AspectJ的注解语法,是轻量级解决方案;AspectJ是功能更强大的完整AOP框架。
Q5:为什么同一个类中方法调用会导致AOP失效?如何解决?
参考答案(踩分点:代理原理 → 原因 → 解决方案):
原因:Spring AOP基于代理实现,当通过代理对象调用方法时才会触发增强;同一个类中this.methodB()的内部调用直接调用原始对象的方法,不走代理,因此不会被增强。
解决方案:
将目标方法提取到另一个Bean中,通过依赖注入调用
使用
AopContext.currentProxy()获取当前代理对象,通过代理调用-40重新设计切面逻辑,避免依赖内部调用
七、总结
本文从传统OOP的痛点出发,系统梳理了Spring AOP的六个核心概念、五种通知类型、与AspectJ的对比关系,并通过完整的代码示例展示了AOP在Spring Boot中的实际应用。在底层原理部分,详细解析了JDK动态代理和CGLIB两种代理方式的区别与选择策略。
核心要点回顾:
AOP的核心思想——将横切关注点从业务逻辑中抽离,实现代码复用和低耦合
连接点、切点、通知、切面——理解这四个概念就掌握了AOP的骨架
Spring AOP基于动态代理实现,会根据目标类是否实现接口自动选择代理方式
@Around环绕通知最强大,但记得调用proceed()面试必考点:代理机制、通知类型、Spring AOP与AspectJ的区别
下一期预告:AI助手达达将继续带大家深入剖析Spring事务管理,从@Transactional到底层事务传播机制,让你在面试中从容应对事务相关的高频问题。
文章信息:AI助手达达 · 2026年4月10日首发
