📅


private方法怎么也拦截不到;面试被问到AOP和AspectJ的区别,支支吾吾说不清楚。这篇文章将从零起步,帮你彻底打通

一、痛点切入:为什么需要AOP?
🔴 传统实现的尴尬

假设有一个电商系统的订单服务,核心业务方法如下:
public class OrderService { public void createOrder(Order order) { // 日志记录 System.out.println("[日志] 开始创建订单"); // 权限校验 System.out.println("[权限] 校验用户身份"); // 性能监控 long start = System.currentTimeMillis(); // 核心业务逻辑 System.out.println("订单创建成功"); // 性能监控 long end = System.currentTimeMillis(); System.out.println("[监控] 耗时:" + (end - start) + "ms"); // 事务管理(此处省略提交/回滚逻辑) } }
🔴 痛点分析
代码冗余:日志、权限、事务、监控等“横切逻辑”在几十上百个方法中反复出现
耦合过高:业务方法强制依赖非业务代码,修改日志格式需要改动所有方法
可维护性差:新增一个切面功能(如限流),需要在每个目标方法中手动添加
违背开闭原则:对扩展开放、对修改关闭的理想状态被彻底打破
AOP正是为解决这些问题而生:将日志、事务、安全等横切关注点从业务逻辑中剥离出来,统一模块化处理,在不修改原有代码的前提下完成功能增强。
二、核心概念:AOP的核心术语
📌 切面(Aspect)
定义:Aspect = 切点(Pointcut)+ 通知(Advice),是横切关注点的模块化单元。在Spring中,使用@Aspect注解标记的类即为切面。-1
类比:可以把切面理解为一个“功能插件”——比如一个日志插件,它知道自己要在哪些方法上执行(切点),也知道要在什么时机执行(通知)。-1
📌 连接点(JoinPoint)
定义:程序执行过程中可以插入增强逻辑的点。在Spring AOP中,仅支持方法执行作为连接点,不支持字段访问、构造器调用等。-
作用:理论上所有方法调用都是连接点,但我们不会对每个方法都增强,这就需要一个“筛选器”。
📌 切点(Pointcut)
定义:通过表达式精确匹配一组连接点的规则。切点决定了“哪些方法”需要被增强。-1
典型表达式:execution( com.example.service...(..)) —— 匹配service包及子包下所有类的所有方法。
📌 通知(Advice)
定义:切面在特定连接点执行的增强逻辑,包含“在什么时候执行”和“执行什么内容”。-1
Spring AOP支持5种通知类型:
| 通知类型 | 注解 | 执行时机 | 典型用途 |
|---|---|---|---|
| 前置通知 | @Before | 目标方法执行前 | 参数校验、权限检查 |
| 后置返回通知 | @AfterReturning | 目标方法正常返回后 | 结果日志记录 |
| 后置异常通知 | @AfterThrowing | 目标方法抛出异常后 | 异常日志、告警 |
| 最终通知 | @After | 方法结束后(类似finally) | 资源清理 |
| 环绕通知 | @Around | 完全控制方法执行前后 | 性能监控、事务控制 |
核心规则:@After无论是否有异常都会执行,@AfterReturning仅在正常返回后执行,@AfterThrowing仅在抛异常后执行。-
📌 目标对象(Target)
定义:被AOP增强的原始业务对象。
📌 织入(Weaving)
定义:将切面逻辑应用到目标对象,生成代理对象的过程。Spring AOP采用运行时织入(Runtime Weaving)。
三、关联概念:Spring AOP 与 AspectJ 的关系
📌 概念B:AspectJ
定义:AspectJ是一个功能完整的AOP框架,支持编译时、类加载时、运行时三种织入方式,能拦截构造函数、静态方法、字段访问等多种连接点。-
📌 二者关系
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 定位 | Spring自带的轻量级AOP实现 | 功能完整的AOP框架 |
| 织入时机 | 仅运行时(动态代理) | 编译时/类加载时/运行时 |
| 连接点范围 | 仅方法执行 | 方法、构造器、字段、静态初始化等 |
| 适用范围 | 仅Spring容器管理的Bean | 任意Java类 |
| 复杂度 | 简单,零配置成本 | 复杂,需AspectJ编译器或LTW |
一句话记忆:Spring AOP“借用”了AspectJ的注解语法,但底层仍是基于动态代理的运行时轻量级实现,并非真正的AspectJ框架。 -
四、代码示例:从零搭建一个完整的AOP切面
第1步:添加Maven依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
spring-boot-starter-aop会自动引入aspectjweaver,提供@Aspect等注解支持。-31
第2步:开启AOP自动代理
@SpringBootApplication @EnableAspectJAutoProxy // 启用AOP,Spring Boot 通常已自动配置,但显式声明更安全 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
第3步:编写业务类(目标对象)
@Service public class OrderService { public String createOrder(String orderNo) { System.out.println("【业务】正在创建订单:" + orderNo); if (orderNo == null || orderNo.isEmpty()) { throw new IllegalArgumentException("订单号不能为空"); } return "订单创建成功,订单号:" + orderNo; } }
第4步:编写切面类
@Component @Aspect // 标记为切面类 @Slf4j public class LoggingAspect { // 定义可复用的切点表达式 @Pointcut("execution( com.example.service..(..))") public void serviceMethod() {} // 前置通知:方法执行前触发,常用于参数校验、权限检查 @Before("serviceMethod()") public void logBefore(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); log.info("【前置】调用方法:{},参数:{}", methodName, Arrays.toString(args)); } // 后置返回通知:方法正常返回后触发,能拿到返回值但改不了 @AfterReturning(pointcut = "serviceMethod()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { log.info("【返回】方法:{},返回值:{}", joinPoint.getSignature().getName(), result); } // 后置异常通知:方法抛异常后触发,用于异常日志 @AfterThrowing(pointcut = "serviceMethod()", throwing = "ex") public void logException(JoinPoint joinPoint, Exception ex) { log.error("【异常】方法:{},异常信息:{}", joinPoint.getSignature().getName(), ex.getMessage()); } // 最终通知:无论成功/异常都执行,类似finally,用于资源清理 @After("serviceMethod()") public void logFinally(JoinPoint joinPoint) { log.info("【最终】方法:{} 执行完毕", joinPoint.getSignature().getName()); } // 环绕通知:最强大,可控制执行、修改参数/返回值 @Around("serviceMethod()") public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); log.info("【环绕-开始】{}", pjp.getSignature()); try { Object result = pjp.proceed(); // 执行目标方法 long cost = System.currentTimeMillis() - start; log.info("【环绕-结束】{},耗时:{}ms,返回:{}", pjp.getSignature(), cost, result); return result; } catch (Exception e) { log.error("【环绕-异常】{},异常:{}", pjp.getSignature(), e.getMessage()); throw e; } } }
第5步:运行测试
@RestController public class OrderController { @Autowired private OrderService orderService; @GetMapping("/order") public String createOrder(@RequestParam String orderNo) { return orderService.createOrder(orderNo); } }
控制台输出(正常场景):
【环绕-开始】String com.example.service.OrderService.createOrder(String) 【前置】调用方法:createOrder,参数:[ORD123] 【业务】正在创建订单:ORD123 【返回】方法:createOrder,返回值:订单创建成功,订单号:ORD123 【最终】方法:createOrder 执行完毕 【环绕-结束】String com.example.service.OrderService.createOrder(String),耗时:5ms,返回:订单创建成功,订单号:ORD123
执行顺序图示:
┌─────────────────────────────────────────────────────────┐ │ 环绕前代码 → @Before → 目标方法 → @AfterReturning → │ │ → @After → 环绕后代码 │ └─────────────────────────────────────────────────────────┘
要点说明:
JoinPoint提供了方法签名、参数等运行时信息ProceedingJoinPoint是JoinPoint的子接口,仅用于@Around通知,必须手动调用proceed()执行目标方法-1@Around的返回值类型必须为Object,用于接收并传递目标方法的返回值切面类必须由Spring容器管理(添加
@Component等注解),否则Spring无法识别和处理-12
五、底层原理:动态代理的两种实现
🔧 JDK动态代理
原理:基于Java标准库java.lang.reflect.Proxy,在运行时生成一个实现目标接口的代理类。代理类通过反射调用目标对象的方法,并在调用前后织入通知逻辑。-
适用条件:目标类必须实现至少一个接口。代理对象类型为com.sun.proxy.$ProxyXX。-
执行流程:调用代理方法 → 调用InvocationHandler.invoke() → 前置通知 → 反射调用目标方法 → 后置通知 → 返回结果。
🔧 CGLIB动态代理
原理:基于字节码增强技术,在运行时通过ASM框架动态生成目标类的子类作为代理。子类覆写目标类的非final方法,并在覆写逻辑中织入通知。--11
适用条件:目标类没有实现接口,或强制指定使用CGLIB。代理对象类型为Target$$EnhancerBySpringCGLIB$$xxx。
🆚 核心对比
| 对比维度 | JDK动态代理 | CGLIB |
|---|---|---|
| 代理方式 | 接口代理 | 子类代理 |
| 是否依赖接口 | ✅ 必须有接口 | ❌ 不需要接口 |
final方法可代理? | ❌ 不可 | ❌ 不可 |
private/static方法可代理? | ❌ 不可 | ❌ 不可 |
| 性能 | 调用成本低 | 生成类成本高,调用更快 |
| 依赖 | 标准库,无额外依赖 | 需引入cglib/asm |
| Spring默认策略 | 有接口时用JDK | 无接口时用CGLIB |
Spring代理选择策略(核心源码逻辑):
if (hasUserSuppliedProxyInterfaces()) { return JDK dynamic proxy; // 目标类有接口 → JDK代理 } else { return CGLIB proxy; // 无接口 → CGLIB代理 }
强制使用CGLIB的配置方式:
@EnableAspectJAutoProxy(proxyTargetClass = true)🧠 技术支撑点
Spring AOP的底层实现依赖于以下核心技术:
代理模式(Proxy Pattern):通过引入代理对象作为目标对象的中间层,实现访问控制与功能增强
反射机制(Reflection):JDK动态代理通过
Method.invoke()调用目标方法字节码操作(Bytecode Manipulation):CGLIB基于ASM框架动态生成字节码
BeanPostProcessor:利用IoC容器的生命周期扩展点,在Bean初始化后完成代理替换
Spring AOP的实质:在IoC容器创建Bean的时机中,根据切面规则为目标Bean生成一个代理对象,将所有横切逻辑编织成一条有序的拦截链,在代理对象执行目标方法时依次唤醒。-
📊 源码级执行流程
1. Spring容器启动,扫描@Aspect切面,生成Advisor(通知器) 2. 实例化目标Bean,完成依赖注入和初始化 3. BeanPostProcessor.postProcessAfterInitialization() 触发 ↓ 4. AbstractAutoProxyCreator.wrapIfNecessary() ↓ 5. getAdvicesAndAdvisorsForBean() → 通过切点表达式匹配 ↓ 6. 匹配成功 → createProxy() 创建代理对象 ↓ 7. 代理对象替换原始Bean存入容器 ↓ 8. 调用代理方法 → 触发MethodInterceptor拦截链 → 按序执行通知
关键点:代理不是在容器启动时创建的,而是在目标Bean初始化完成后通过postProcessAfterInitialization动态创建的。Bean在初始化阶段是真实对象,但最终被注入到容器中的是代理对象。-11
六、高频面试题与参考答案
面试题1:Spring AOP的底层原理是什么?JDK动态代理和CGLIB有什么区别?
参考答案:
Spring AOP的核心原理是动态代理。在IoC容器创建Bean的过程中,通过BeanPostProcessor扩展点,根据切点表达式的匹配结果,为目标Bean动态生成代理对象,将通知逻辑编织成拦截链,在方法调用时依次执行。
JDK动态代理和CGLIB的区别:
JDK动态代理:基于接口实现,通过
java.lang.reflect.Proxy在运行时生成实现接口的代理类,要求目标类必须有接口。代理类通过反射调用目标方法。CGLIB:基于子类继承实现,通过字节码技术生成目标类的子类作为代理,无需接口,但无法代理
final方法和final类。
Spring的默认选择策略:目标类有接口时默认使用JDK动态代理,无接口时自动切换到CGLIB。可通过@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用CGLIB。-38-
面试题2:Spring AOP 和 AspectJ 有什么区别?
参考答案:
两者都是Java实现AOP的框架,但定位和实现完全不同:
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 仅运行时(动态代理) | 编译时/类加载时/运行时 |
| 连接点范围 | 仅方法执行 | 方法、构造器、字段、静态初始化等 |
| 适用范围 | 仅Spring容器管理的Bean | 任意Java类 |
| 复杂度 | 简单,与Spring生态集成度高 | 功能强大但配置复杂 |
一句话记忆:Spring AOP是轻量级的“运行时代理”方案,借用AspectJ注解语法;AspectJ是功能完整的AOP框架,支持更细粒度的连接点拦截。-54
面试题3:为什么 @Before 中修改参数,目标方法收不到?
参考答案:
因为Spring AOP的通知方法接收到的JoinPoint中的参数是原始引用副本,@Before无法替换实际传入目标方法的参数对象。
解决方案:必须使用@Around环绕通知,通过proceed(Object[] args)显式传入修改后的参数数组。
注意:如果参数是可变对象(如Map、List、自定义DTO),在@Before中修改其字段/元素是生效的——这属于对象内部状态变更,而非“替换参数对象”。真正的参数替换只能由@Around完成。-12-12
@Around("serviceMethod()") public Object modifyParam(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); if (args.length > 0 && args[0] instanceof String) { args[0] = "[预处理]" + args[0]; } return pjp.proceed(args); // 关键:传入修改后的参数 }
面试题4:Spring AOP 有哪些通知类型?执行顺序是怎样的?
参考答案:
Spring AOP有5种通知类型:@Before(前置)、@AfterReturning(后置返回)、@AfterThrowing(后置异常)、@After(最终)、@Around(环绕)。
执行顺序(正常返回场景):@Around环绕前 → @Before → 目标方法 → @AfterReturning → @After → @Around环绕后。
异常场景:@Around环绕前 → @Before → 目标方法 → @AfterThrowing → @After → @Around捕获异常。@After无论是否有异常都会执行,类似finally块。--62
面试题5:Spring AOP 有哪些局限性?
参考答案:
主要局限性有三点:
仅对 public 方法生效:非
public方法(private、protected、包级)无法被JDK代理或CGLIB正确拦截。-同类内部方法调用失效:同一个Bean内部通过
this.methodB()调用methodB时,调用的是原始对象而非代理对象,AOP不会生效——这是最常见的“踩坑点”。-仅支持方法级连接点:Spring AOP不支持字段访问、构造器调用等细粒度拦截,如需此类功能应使用AspectJ。
仅对Spring容器管理的Bean生效:手动
new出来的对象无法被AOP增强。
七、总结
📚 核心知识回顾
| 模块 | 核心要点 |
|---|---|
| AOP思想 | 横向抽取横切关注点(日志、事务、权限等),与OOP纵向继承互补 |
| 核心术语 | Aspect(切面)= Pointcut(切点)+ Advice(通知) |
| 通知类型 | 5种:@Before、@After、@AfterReturning、@AfterThrowing、@Around |
| 底层原理 | 动态代理(JDK接口代理 + CGLIB子类代理) |
| 代理选择 | 有接口用JDK,无接口用CGLIB;final/private/static方法无法代理 |
| Spring vs AspectJ | Spring AOP轻量运行时;AspectJ功能完整,支持编译时织入 |
| 常见坑点 | private方法不拦截、内部自调用失效、@Before改参数无效 |
⚠️ 易错点提醒
@Aspect注解本身不带@Component,切面类必须显式交给Spring管理@Around必须手动调用proceed(),且通常只能调用一次@After始终执行(类似finally),@AfterReturning仅在正常返回后执行代理对象 ≠ 原始对象:通过
this调用同类方法会绕过代理
🔜 进阶预告
本文聚焦于Spring AOP的核心概念、原理与实战。下一篇我们将深入探讨:
AOP在事务管理中的底层应用
自定义注解+切面实现分布式锁、接口限流
多切面执行顺序控制与
@Order详解Spring AOP与Spring Boot自动装配的集成原理
📌 版权声明:本文为原创技术分享,欢迎转载,请注明出处。文中代码示例均可在Spring Boot 3.x环境下运行验证。如有疑问或指正,欢迎评论区交流。