开篇引入
在Spring技术栈中,AOP(Aspect Oriented Programming,面向切面编程)与IoC并称为Spring两大核心基石,是每一位Java后端开发者必须掌握的知识点。许多初学者往往陷入“会用但不懂原理”的困境——知道@Before能拦截方法,却说不出它和@Around的本质区别;会配置切面,却答不上JDK动态代理和CGLIB的区别。AI助手科研资料,助你深入理解Spring AOP——本文将系统讲解Spring AOP的核心概念、注解使用、底层原理及高频面试考点,通过完整的代码示例带你从入门到进阶。

一、痛点切入:为什么需要AOP?
先看一个典型的业务场景——在每个Service方法中添加日志和性能监控:

@Service public class UserService { public User getUserById(Long id) { System.out.println("【日志】开始执行getUserById,参数:" + id); long start = System.currentTimeMillis(); // 业务逻辑... User user = userDao.findById(id); long end = System.currentTimeMillis(); System.out.println("【性能】getUserById耗时:" + (end - start) + "ms"); System.out.println("【日志】getUserById执行完毕,返回值:" + user); return user; } public void updateUser(User user) { System.out.println("【日志】开始执行updateUser,参数:" + user); long start = System.currentTimeMillis(); // 业务逻辑... long end = System.currentTimeMillis(); System.out.println("【性能】updateUser耗时:" + (end - start) + "ms"); System.out.println("【日志】updateUser执行完毕"); } }
这种写法的痛点显而易见:
代码冗余:日志、性能监控代码在每个方法中重复出现
耦合度高:横切关注点(日志、性能)与核心业务逻辑耦合在一起
维护困难:修改日志格式需要改动所有业务方法
扩展性差:新增切面功能(如权限校验)要在每个方法上加代码
AOP正是为了解决这个问题而生——它将日志、权限、事务等“横切关注点”从业务逻辑中抽取出来,在运行期动态织入,实现代码的解耦和复用。
二、核心概念讲解
2.1 切面(Aspect)
标准定义:Aspect(切面)是封装横切关注点的模块化单元,包含多个通知和切点。简单理解,切面就是将分散在各处的共性功能(如日志、事务)集中封装成一个类-13。
生活化类比:把切面想象成“安检员”。无论你进哪个商场,安检员都会在入口处拦截检查——安检逻辑本身独立于商场业务,但横切到了所有入口。
2.2 连接点(Join Point)
标准定义:Join Point(连接点)是程序执行过程中可以被插入切面逻辑的点。在Spring AOP中,连接点特指方法调用-13。
2.3 通知(Advice)
标准定义:Advice(通知)是在特定连接点执行的动作,定义了切面“做什么”以及“何时做”-13。Spring AOP提供了五种通知类型:
| 通知类型 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行之前 |
| 后置通知 | @After | 目标方法执行之后(无论是否异常) |
| 返回通知 | @AfterReturning | 目标方法正常返回后 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 |
| 环绕通知 | @Around | 包裹目标方法,可控制执行全过程 |
2.4 切点(Pointcut)
标准定义:Pointcut(切点)通过表达式匹配一组连接点,定义“何处”需要被切面处理-13。如果说通知定义了“何时做什么”,切点则定义了“在哪里做”-29。
生活化类比:安检员只对“所有商场入口”执行检查——“所有商场入口”就是切点表达式,定义了切面应用的位置。
三、概念关系总结
| 概念 | 作用 | 一句话理解 |
|---|---|---|
| 切面(Aspect) | 封装横切关注点的模块 | 装“增强代码”的盒子 |
| 连接点(Join Point) | 可插入切面的潜在位置 | 能“下手”的所有地方 |
| 切点(Pointcut) | 筛选需要增强的连接点 | 决定“对哪些地方下手” |
| 通知(Advice) | 切面要执行的代码 | “下手之后怎么做” |
一句话概括:切点选位置 + 通知定行为 → 构成切面——切面通过切点筛选连接点,在通知中定义增强逻辑,最终织入目标对象-。
四、代码示例:Spring Boot中的AOP实战
4.1 添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
Spring Boot已为AOP提供了自动配置支持,引入依赖后即可直接使用-22。
4.2 定义切面类
@Aspect // 标记为切面类 @Component // 纳入Spring容器管理 public class LoggingAspect { // 定义切点:匹配com.example.service包下所有类的所有方法 @Pointcut("execution( com.example.service..(..))") public void serviceMethods() {} // 前置通知 @Before("serviceMethods()") public void logBefore(JoinPoint joinPoint) { System.out.println("方法执行前:" + joinPoint.getSignature().getName() + ",参数:" + Arrays.toString(joinPoint.getArgs())); } // 后置通知 @After("serviceMethods()") public void logAfter(JoinPoint joinPoint) { System.out.println("方法执行后:" + joinPoint.getSignature().getName()); } // 返回通知(可访问返回值) @AfterReturning(pointcut = "serviceMethods()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { System.out.println("方法正常返回,结果:" + result); } // 异常通知 @AfterThrowing(pointcut = "serviceMethods()", throwing = "e") public void logAfterThrowing(JoinPoint joinPoint, Exception e) { System.out.println("方法抛异常:" + e.getMessage()); } // 环绕通知(可控制方法执行,最灵活) @Around("serviceMethods()") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); System.out.println("【Around前置】开始执行:" + joinPoint.getSignature().getName()); Object result = joinPoint.proceed(); // 执行目标方法 long end = System.currentTimeMillis(); System.out.println("【Around后置】执行完成,耗时:" + (end - start) + "ms"); return result; } }
4.3 执行效果
调用UserService.getUserById(1L)后,控制台输出:
【Around前置】开始执行:getUserById 方法执行前:getUserById,参数:[1] 执行目标方法... 方法正常返回,结果:User{id=1, name='张三'} 方法执行后:getUserById 【Around后置】执行完成,耗时:2ms
关键要点:
@Aspect标记的类被Spring容器识别为切面,不会被动态代理,而是作为横切关注点织入目标对象-23ProceedingJoinPoint的proceed()方法执行真正的业务逻辑,环绕通知可以控制是否执行、修改返回值等-22通知执行顺序:
@Around前置 →@Before→ 目标方法 →@AfterReturning/@AfterThrowing→@After→@Around后置
五、底层原理:动态代理
5.1 核心原理
Spring AOP的实现本质依赖于动态代理——通过创建目标对象的代理对象,在代理对象的方法调用前后插入切面逻辑-39。
5.2 JDK动态代理 vs CGLIB
| 对比维度 | JDK动态代理 | CGLIB代理 |
|---|---|---|
| 实现条件 | 目标类必须实现接口 | 目标类无需接口 |
| 实现原理 | 基于反射生成接口的实现类 | 通过字节码技术生成目标类的子类 |
| 代理对象 | 实现了目标接口的代理对象 | 目标类的子类对象 |
| 限制 | 仅代理接口中声明的方法 | final类/方法无法代理 |
| Spring Boot默认 | 使用CGLIB- | — |
JDK动态代理:要求目标对象至少实现一个接口,通过Proxy.newProxyInstance()创建实现接口的代理对象,调用时经由InvocationHandler拦截-20。
CGLIB代理:当目标类没有实现接口时,Spring会使用CGLIB。它通过ASM字节码生成框架创建目标类的子类,重写可重写的方法,在方法中织入切面逻辑-。
5.3 Spring Boot中的默认行为
Spring Boot默认启用CGLIB代理,因此即使类实现了接口,也会优先使用CGLIB-43。这意味着:
被代理的类不能是final类
被增强的方法不能是final或private方法-43
六、高频面试题与参考答案
Q1:什么是AOP?Spring AOP的底层原理是什么?
答案:AOP(面向切面编程)是一种编程范式,通过横向抽取机制将日志、事务等横切关注点从业务逻辑中分离,在不修改原有代码的前提下实现功能增强。
Spring AOP的底层基于动态代理:
若目标类实现接口,使用JDK动态代理(基于反射)
若目标类无接口,使用CGLIB代理(基于字节码生成子类)
Spring Boot默认使用CGLIB代理-
踩分点:AOP定义 + 动态代理 + JDK/CGLIB对比 + 默认行为。
Q2:@Before、@After、@Around的区别?
| 通知类型 | 执行时机 | 特点 |
|---|---|---|
@Before | 目标方法执行前 | 不能阻止方法执行 |
@After | 目标方法执行后(含异常) | 适合资源清理 |
@Around | 包裹目标方法 | 可控制执行流程、修改返回值,功能最强 |
Q3:JDK动态代理和CGLIB代理有什么区别?Spring Boot默认用哪个?
答案:
JDK动态代理:要求目标类实现接口,基于反射生成代理类,性能略好
CGLIB代理:无需接口,基于字节码生成子类,final类/方法无法代理
Spring Boot 默认使用CGLIB--
Q4:Spring AOP为什么只对public方法生效?内部自调用为什么无法被拦截?
答案:
JDK动态代理只代理接口中声明的public方法
CGLIB通过继承子类重写方法,private/final方法无法重写
内部自调用(
this.methodB())走的是原始对象引用,未经过代理对象,因此无法触发切面逻辑。解决方案:通过ApplicationContext.getBean()获取代理对象再调用-31
Q5:如何自定义注解实现AOP拦截?
答案:三步走——
① 定义注解:@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Loggable {}
② 切面中引用:@Pointcut("@annotation(com.example.Loggable)")
③ 在目标方法上添加@Loggable注解即可-22
七、结尾总结
本文系统讲解了Spring AOP的核心知识点:
痛点驱动:日志、权限等横切关注点分散在业务代码中,导致冗余和耦合
四大核心概念:切面(Aspect) = 切点(Pointcut)选位置 + 通知(Advice)定行为
五种通知类型:
@Before→@After→@AfterReturning/@AfterThrowing→@Around底层原理:JDK动态代理(接口反射) vs CGLIB(字节码子类),Spring Boot默认CGLIB
常见陷阱:非public方法无法拦截 + 内部自调用绕过代理
重点记忆:切点选“在哪里做”,通知定“什么时候做什么”,两者结合构成切面。面试常考动态代理机制和默认行为,务必熟记。
下一篇预告:Spring AOP进阶——通知执行链路与责任链模式深度剖析
