一、痛点切入:为什么需要AOP?

先看一段传统的业务代码:
@Servicepublic class OrderService { public void createOrder(Order order) { // 日志记录 System.out.println("[LOG] 创建订单开始,参数:" + order); // 权限校验 if (!hasPermission()) { throw new SecurityException("无权限"); } // 事务开启 beginTransaction(); try { // 核心业务逻辑 System.out.println("执行订单创建业务..."); saveOrder(order); // 事务提交 commit(); } catch (Exception e) { rollback(); throw e; } // 日志记录 System.out.println("[LOG] 创建订单结束"); } }
这种写法存在三个典型问题:
代码冗余:日志、权限、事务等代码在每一个Service方法中重复出现耦合紧密:横切关注点与核心业务逻辑混在一起,修改日志格式要改动所有方法
扩展性差:新增一个“方法耗时统计”功能,需要在成百上千个方法中逐一添加
据统计,传统OOP在处理日志、事务等横切场景时,代码重复率高达60%以上-。
AOP的设计初衷就是解决这个问题:把那些“散落在各处”的通用逻辑集中定义成“切面”,然后告诉Spring在什么时候、在什么地方自动织入这些逻辑。
二、核心概念:切面(Aspect)
定义:Aspect(切面)是将横切关注点模块化的类,它封装了需要在多个方法中重复执行的通用逻辑,如日志记录、事务管理、权限校验等-7。
生活化类比:
可以把AOP想象成一个视频剪辑软件中的“滤镜” —— 你不用手动给每一帧画面添加特效,只需要定义一个滤镜(切面),软件会自动把它应用到所有符合条件的视频片段上。同理,AOP让你定义一次“增强逻辑”,Spring会自动把它应用到所有匹配的方法上。
作用与价值:
提高代码复用性:一次定义,处处生效
降低模块间耦合:业务逻辑与通用功能分离
提升可维护性:修改切面逻辑不需要改动业务代码
常用注解:
@Aspect // 标记一个类为切面 @Component // 让Spring管理这个切面Bean
三、关联概念:通知(Advice)
定义:Advice(通知)是切面在特定连接点执行的具体动作,它告诉AOP框架“做什么”以及“什么时候做”-7。
五种通知类型:
| 注解 | 执行时机 | 典型场景 |
|---|---|---|
@Before | 目标方法执行前 | 权限校验、参数验证 |
@After | 目标方法执行后(无论异常与否) | 资源清理 |
@AfterReturning | 目标方法正常返回后 | 日志记录、结果缓存 |
@AfterThrowing | 目标方法抛出异常后 | 异常监控、回滚处理 |
@Around | 方法调用前后,可控制执行 | 性能监控、事务管理 |
环绕通知示例:
@Aspect @Component public class PerformanceAspect { @Around("@annotation(com.example.Monitor)") public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); // 执行目标方法 Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - start; System.out.println(joinPoint.getSignature() + " 执行耗时: " + duration + "ms"); return result; } }
四、概念关系:Aspect vs Advice
| 维度 | 切面(Aspect) | 通知(Advice) |
|---|---|---|
| 本质 | 模块化的横切关注点 | 切面中的具体动作 |
| 包含关系 | 通知 + 切入点 | 切面的组成部分 |
| 类比理解 | 整份“合同” | 合同中的“条款” |
| 定义方式 | @Aspect标记类 | @Before等标记方法 |
一句话总结:Aspect是“把什么逻辑、在哪里执行”的完整封装,而Advice是其中“执行什么”的那部分。
AOP还涉及其他核心术语:
连接点(JoinPoint) :程序执行过程中能够插入切面的点,在Spring中特指方法执行-
切入点(Pointcut) :通过表达式匹配连接点的规则,决定通知应用到哪些方法
织入(Weaving) :将切面应用到目标对象并创建代理对象的过程-7
五、完整代码示例:基于注解的AOP实战
场景:为Service层的所有方法添加统一的日志记录和耗时统计。
步骤一:添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
步骤二:开启AOP支持
@Configuration @EnableAspectJAutoProxy // 启用AspectJ自动代理 public class AopConfig { }
注:Spring Boot中默认已开启,无需手动配置-25。
步骤三:定义切面类
@Aspect @Component public class LoggingAspect { // 定义切入点:匹配com.example.service包下所有类的所有方法 @Pointcut("execution( com.example.service..(..))") public void serviceMethods() {} @Before("serviceMethods()") public void logBefore(JoinPoint joinPoint) { System.out.println("[BEFORE] 调用方法: " + joinPoint.getSignature().getName()); System.out.println("[BEFORE] 参数: " + Arrays.toString(joinPoint.getArgs())); } @AfterReturning(pointcut = "serviceMethods()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println("[AFTER] 方法返回: " + result); } @Around("serviceMethods()") public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - start; System.out.println("[PERF] 耗时: " + duration + "ms"); return result; } }
步骤四:业务代码(无任何侵入)
@Service public class OrderService { public String createOrder(String productId, int quantity) { // 纯业务逻辑,没有任何日志/耗时代码 System.out.println("创建订单: " + productId + ", 数量: " + quantity); return "ORDER_" + System.currentTimeMillis(); } }
执行效果:调用orderService.createOrder("book_001", 2)时,控制台输出:
[BEFORE] 调用方法: createOrder [BEFORE] 参数: [book_001, 2] 创建订单: book_001, 数量: 2 [PERF] 耗时: 3ms [AFTER] 方法返回: ORDER_1734567890123
关键对比:改造前——每增加一个功能就要改动所有方法;改造后——增强逻辑集中在切面类中,业务代码零改动,代码量减少80%以上。
六、底层原理:动态代理
Spring AOP的底层依赖于代理模式和动态代理技术-30。Spring容器启动时,会为目标对象生成一个代理对象,当客户端调用目标方法时,实际调用的是代理对象,代理对象在执行前后插入切面逻辑。
Spring AOP支持两种动态代理方式-7:
| 特性 | JDK动态代理 | CGLIB代理 |
|---|---|---|
| 实现原理 | 基于接口,生成接口的代理类 | 基于继承,生成目标类的子类 |
| 目标要求 | 必须实现至少一个接口 | 类不能是final,方法不能是final/private |
| 代理方式 | java.lang.reflect.Proxy + InvocationHandler | 字节码生成库 |
| Spring Boot 2.x+默认 | 需手动配置 | 默认使用-13 |
| 性能特点 | 创建快,执行稍慢 | 创建慢,执行快 |
核心流程图:
客户端调用 → 代理对象 → 检查是否匹配切点 → 执行前置通知 → 调用目标方法 → 执行后置通知 → 返回结果底层技术栈提示:Spring AOP的实现依赖Java的反射机制和动态代理。想深入理解AOP源码,建议先掌握这两个基础知识点。
七、高频面试题与参考答案
Q1:什么是AOP?Spring AOP是如何实现的?
标准答案:AOP(面向切面编程)是在不修改业务代码的情况下,为方法统一添加横切逻辑(如日志、事务、权限)的编程范式-41。Spring AOP基于动态代理实现:如果目标类实现了接口,使用JDK动态代理生成代理对象;如果没有实现接口,则使用CGLIB生成子类代理。Spring容器最终注入的是代理对象而非原始对象-41。
Q2:JDK动态代理和CGLIB代理有什么区别?
踩分点:JDK基于接口,CGLIB基于继承;JDK要求目标类必须实现接口,CGLIB无此要求;final类或final方法不能被CGLIB代理-41;Spring Boot 2.x开始默认使用CGLIB。
Q3:为什么@Transactional有时会失效?
标准答案:最常见的原因是:①方法不是public(Spring AOP只对public方法生效);②同一个类中的内部调用(没有经过代理对象);③方法被声明为final无法被代理;④异常被catch后没有重新抛出,事务管理器检测不到异常-41。
Q4:@Around和@Before/@After的区别是什么?
标准答案:@Before/@After只在方法执行前/后执行,无法控制方法是否执行;@Around最强大,通过ProceedingJoinPoint.proceed()完全控制目标方法的执行,可以决定是否执行、修改参数、改变返回值-41。
Q5:Spring AOP和AspectJ有什么区别?
标准答案:Spring AOP是运行时代理,基于JDK Proxy或CGLIB,功能有限(仅支持方法拦截),配置简单;AspectJ是编译时织入,支持字段访问、构造器等细粒度拦截,性能更高但需要额外编译步骤-。
八、总结
| 要点 | 核心内容 |
|---|---|
| 核心思想 | 将横切关注点从业务逻辑中分离,一次定义,处处生效 |
| 两大主角 | 切面(Aspect)= 通知(Advice)+ 切入点(Pointcut) |
| 五种通知 | Before、After、AfterReturning、AfterThrowing、Around |
| 底层原理 | JDK动态代理(接口)和CGLIB代理(继承) |
| 事务失效 | 非public、内部调用、final方法、异常被吞 |
重点提醒:面试中回答AOP相关问题,一定要点出“动态代理”这个底层机制,这是区分“会用”和“懂原理”的关键分水岭。
下期预告:AOP的进阶玩法——如何通过自定义注解实现零侵入的权限校验系统?欢迎持续关注。
