谈谈 Spring 的 AOP?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
概念理解:面试官不仅仅是想知道你背了几个名词,更是想看你能不能用通俗的话解释 AOP 的核心思想,以及它和 OOP 的关系。
-
核心术语:是否准确理解切面、切点、通知、连接点等术语,能不能用实际代码举例说明。
-
实现原理:是否知道 Spring AOP 底层用的是动态代理(JDK 还是 CGLIB),以及在什么场景下用哪种代理。这个才是拉开差距的关键。
-
实战能力:能不能手写一个切面,知不知道常见的使用场景和踩坑点。
核心答案
AOP(Aspect-Oriented Programming,面向切面编程)的核心思想是:把横切关注点从业务逻辑中抽离出来,统一管理。
啥叫横切关注点?日志、权限校验、事务管理、性能统计这些,几乎每个业务方法都需要,但又跟业务逻辑没半毛钱关系。如果每个方法都手写一遍,代码又臭又长。AOP 就是来解决这个问题的——"你只管写业务,这些脏活累活我来统一搞定"。
上图直观对比了有无 AOP 的代码组织方式。没有 AOP 时,每个业务方法都混入了日志、权限、事务这些非业务逻辑,代码重复且难以维护。有了 AOP 后,业务方法只关心自己的逻辑,横切关注点被抽取到切面中统一管理,代码清爽多了。
深度解析
一、核心术语
先把这些术语搞清楚,面试时才不会卡壳。
| 术语 | 英文 | 一句话解释 |
|---|---|---|
| 切面 | Aspect | 横切关注点的模块化,比如一个日志切面。就是一个包含通知和切点的类 |
| 连接点 | Join Point | 程序执行中的一个点,比如方法调用、异常抛出。Spring AOP 中特指方法的执行 |
| 切点 | Pointcut | 用来匹配连接点的表达式,决定通知要在哪些方法上生效 |
| 通知 | Advice | 在切点匹配到的方法上 "做什么事",比如打印日志、开启事务 |
| 织入 | Weaving | 把切面应用到目标对象的过程。Spring AOP 在运行时通过动态代理织入 |
| 目标对象 | Target Object | 被代理的原始对象 |
| AOP 代理 | AOP Proxy | Spring 创建的代理对象,包裹了目标对象 + 切面逻辑 |
说人话:切点管 "在哪切",通知管 "切了干啥",切面就是把这两者打包在一起,织入就是把切面 "贴" 到目标对象上。
二、五种通知类型
Spring 提供了 5 种通知,对应方法执行的不同时机——
上图展示了五种通知在目标方法执行前后的触发顺序,关键理解点:
@Before:方法执行前触发,最简单最常用。适合做参数校验、权限检查。@AfterReturning:方法正常返回后触发,可以拿到返回值。适合做结果日志、数据脱敏。@AfterThrowing:方法抛出异常后触发,可以拿到异常对象。适合做异常告警、统一错误处理。@After:方法执行后必定触发,不管有没有异常。类似finally,适合做资源清理。@Around:最强大的一种,包裹了整个方法执行过程,可以决定是否继续执行、修改参数、修改返回值。功能最全,但用不好也最容易出问题。
执行顺序:@Around 前半 → @Before → 目标方法 → @AfterReturning/@AfterThrowing → @After → @Around 后半。
三、手写一个切面
光说不练假把式,来一个完整的日志切面:
@Aspect // 声明这是一个切面
@Component // 注册为 Spring Bean
public class LogAspect {
// 定义切点:匹配 com.example.service 包下所有类的所有方法
@Pointcut("execution(* com.example.service..*.*(..))")
public void serviceLayer() {}
// 前置通知
@Before("serviceLayer()")
public void before(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("调用方法:" + methodName + ",参数:" + Arrays.toString(args));
}
// 环绕通知(最强大,能控制是否继续执行)
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
// 可以修改参数:pjp.proceed(newArgs)
Object result = pjp.proceed(); // 执行目标方法
long cost = System.currentTimeMillis() - start;
System.out.println(pjp.getSignature().getName() + " 耗时:" + cost + "ms");
return result; // 可以修改返回值
}
// 异常通知
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowing(Exception ex) {
System.out.println("方法异常:" + ex.getMessage());
// 发告警、记日志...
}
}
切点表达式常用写法:
// 匹配 service 包下所有方法
execution(* com.example.service..*.*(..))
// 匹配带有 @Cacheable 注解的方法
@annotation(org.springframework.cache.annotation.Cacheable)
// 匹配 Service 层的所有 Bean(Bean 名称为 xxService)
bean(*Service)
// 组合使用:在 service 包下且带有 @Transactional 注解的方法
execution(* com.example.service..*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)
四、Spring AOP 的实现原理 — 动态代理
这是面试的 "灵魂拷问" 部分。Spring AOP 底层靠的是动态代理,有两种实现方式:
上图对比了两种代理方式的核心差异:
-
JDK 动态代理:利用反射在运行时动态生成一个实现了目标接口的代理类。
Proxy.newProxyInstance()创建代理对象,InvocationHandler处理方法调用。限制是目标类必须实现至少一个接口,否则没法代理。Spring 默认情况下,如果目标类实现了接口,就用 JDK 动态代理。 -
CGLIB 代理:通过字节码技术在运行时生成目标类的子类,重写父类方法来实现代理。不需要接口,但不能代理
final类和final方法(因为没法重写)。Spring 默认情况下,如果目标类没有实现接口,就用 CGLIB。
Spring Boot 2.x 之后有个重要变化:默认全部使用 CGLIB 代理(spring.aop.proxy-target-class=true),不管你有没有接口。之前 Spring 默认是有接口用 JDK 代理、没接口用 CGLIB,这个行为差异面试的时候提一下很加分。
五、JDK 动态代理原理速览
// 1. 目标接口
public interface UserService {
String getUser(Long id);
}
// 2. 目标实现
public class UserServiceImpl implements UserService {
public String getUser(Long id) {
return "用户" + id;
}
}
// 3. 创建代理
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxyObj, method, args) -> {
// 前置逻辑(相当于 @Before)
System.out.println("调用前:" + method.getName());
// 执行目标方法
Object result = method.invoke(target, args);
// 后置逻辑(相当于 @AfterReturning)
System.out.println("调用后,返回:" + result);
return result;
}
);
// 4. 调用代理方法
proxy.getUser(1L); // 会先打印 "调用前",再执行方法,再打印 "调用后"
Spring AOP 在 BeanPostProcessor#postProcessAfterInitialization 阶段,检测到 Bean 被切面匹配,就会用上述方式创建代理对象,替换掉原始 Bean。
六、Spring AOP vs AspectJ
面试官偶尔会追问这个区别,简单了解就行——
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 实现方式 | 运行时动态代理 | 编译时 / 加载时织入 |
| 功能范围 | 只能代理方法级别的连接点 | 支持方法、字段、构造器、静态方法等 |
| 性能 | 每次调用多一层代理,有损耗 | 编译期织入,运行时无额外开销 |
| 使用难度 | 简单,加注解就行 | 需要额外的编译器或 agent |
| 适用场景 | 大多数业务场景 | 需要更细粒度控制的高级场景 |
日常开发 99% 用 Spring AOP 就够了。AspectJ 功能更强大但更复杂,除非你有特殊需求(比如需要拦截字段访问),否则没必要。
七、AOP 失效的常见场景
这个是生产中最容易踩的坑——同类内部方法调用,AOP 不生效。
@Service
public class OrderService {
public void createOrder() {
// 直接调内部方法,AOP 不生效!
this.validateOrder();
}
@Transactional // 这个注解是通过 AOP 实现的
public void validateOrder() {
// 事务不会生效
}
}
原因很简单:AOP 代理的是外部调用,this.validateOrder() 走的是原始对象的方法调用,不经过代理。解决方案:
- 把
validateOrder()抽到另一个 Service 中 - 通过
ApplicationContext获取代理对象再调用 - 使用
AopContext.currentProxy()获取当前代理对象(需要开启@EnableAspectJAutoProxy(exposeProxy = true))
面试高频追问
-
追问一:JDK 动态代理和 CGLIB 代理的区别?哪个性能更好?
JDK 基于接口,CGLIB 基于继承。早期 JDK 代理性能不如 CGLIB,但 JDK 8 之后大幅优化了反射性能,现在两者差距很小。Spring Boot 2.x 默认全部用 CGLIB,不用纠结这个。
-
追问二:Spring AOP 和 Spring 事务是什么关系?
Spring 事务(
@Transactional)本质上就是通过 AOP 实现的。Spring 创建一个事务切面(TransactionInterceptor),在方法执行前开启事务,正常返回时提交,异常时回滚。所以事务不生效的场景和 AOP 不生效的场景完全一致——内部调用、非 public 方法、final 方法等。 -
追问三:
@Transactional注解加在 private 方法上会生效吗?不会。因为 CGLIB 代理通过重写方法来实现拦截,private 方法无法被重写。JDK 代理基于接口,接口里也没有 private 方法。所以
@Transactional只对 public 方法生效,这也是阿里开发手册里明确要求的。
常见面试变体
- "说说 Spring AOP 的实现原理"
- "JDK 动态代理和 CGLIB 有什么区别?"
- "
@Transactional注解是怎么生效的?" - "AOP 在你项目中有哪些实际应用?"
记忆口诀
核心术语:切面打包,切点定位,通知干活,织入生效。
五种通知顺序:环绕前 → 前置 → 方法 → 返回/异常 → 后置 → 环绕后。
代理选择:有接口 JDK,没接口 CGLIB,Spring Boot 2.x 全用 CGLIB。
最大坑:内部调用不走代理,AOP 事务全失效。
总结
Spring AOP 的面试回答,核心就是三件事:概念讲清楚、代码写得出、原理说得透。概念上记住切面/切点/通知这几个术语,代码上能手写一个切面,原理上把 JDK 动态代理和 CGLIB 的区别讲明白。最后别忘了提一句 "内部调用 AOP 失效" 这个经典坑,面试官会觉得你有实战经验。
