Spring AOP 深度解析:从概念到源码,面试不再踩坑
Spring 面试必杀技系列 · 第四期 |
前三期:[IoC与DI] [自动配置] [Spring MVC]
技术栈:Spring Boot 2.7.x + JDK 11 + MySQL 8.x
开篇:你为什么需要 AOP?
想象一个场景:EduLearn 平台的每个 Service 方法,产品都要求「记录入参、出参和耗时」。最笨的办法是在每个方法里复制粘贴日志代码——80 个 Service 类,400+ 个方法,改一处日志格式就要全部改一遍。
这种散落在各处的相同逻辑就是「横切关注点」。AOP 解决的就是这个问题:把日志逻辑抽到一个地方,声明它「拦截哪些方法」,其余的交给框架。
一句话定义:AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,通过「切面」将横切关注点与业务逻辑分离,在不修改源码的情况下为程序动态添加功能。
一、AOP 核心概念速览
AOP 只有 5 个核心概念,理解它们的关系等于理解了 AOP。

1.1 JoinPoint(连接点)—— 所有可能的插入点
连接点是程序执行过程中「可以插入切面的位置」。在 Spring AOP 中,连接点就是方法执行(方法调用、异常抛出、字段修改等在 Spring AOP 中不被拦截)。
类比:一栋楼的每一层楼板都是可以打洞穿管的位置——但你不会每层都打。
1.2 Pointcut(切点)—— 筛选出真正需要增强的连接点
切点是一个谓词表达式,从无数连接点中筛选出真正需要增强的目标方法。
// 匹配 com.edulearn.service 包下所有方法
@Pointcut("execution(* com.edulearn.service..*.*(..))")
public void serviceLayer() {}
类比:只有卫生间和厨房的楼板需要打洞穿水管,你通过图纸(切点表达式)精确标出这些位置。
1.3 Advice(通知)—— 在切点处「做什么」
通知定义了在切点处具体执行的逻辑。Spring 提供 5 种通知类型:
| 通知类型 | 执行时机 | 能否阻止方法执行 | 能否获取返回值 | 能否修改返回值 |
|---|---|---|---|---|
| @Before | 方法执行前 | ❌ | ❌ | ❌ |
| @AfterReturning | 方法正常返回后 | ❌ | ✅ | ❌ |
| @AfterThrowing | 方法抛出异常后 | ❌ | ❌ | ❌ |
| @After | 方法执行后(finally) | ❌ | ❌ | ❌ |
| @Around | 环绕方法执行 | ✅ | ✅ | ✅ |
1.4 Aspect(切面)—— 切点 + 通知的封装
切面 = 切点(Where)+ 通知(What)。在 Spring 中,就是一个标注了 @Aspect 的类。
@Aspect
@Component
public class LogAspect {
// 切点: 定义拦截位置
@Pointcut("execution(* com.edulearn.service..*.*(..))")
public void serviceLayer() {}
// 通知: 定义拦截后做什么
@Before("serviceLayer()")
public void logBefore(JoinPoint joinPoint) { /* 记录日志 */ }
}
1.5 Weaving(织入)—— 把切面应用到目标对象
织入是将切面逻辑与业务对象整合在一起的过程。Spring AOP 在运行时通过动态代理完成织入(区别于 AspectJ 的编译期织入)。
1.6 EduLearn 概念映射

| AOP 概念 | EduLearn 实例 | 代码位置 |
|---|---|---|
| Aspect 切面 | LogAspect(日志)、AuthAspect(权限) | aspect/LogAspect.java |
| Pointcut 切点 | service 包下所有方法 | @Pointcut("execution(*..service..*.*(..))") |
| Advice 通知 | @Before 日志输入、@Around 权限校验 | logBefore() / checkAuth() |
| JoinPoint 连接点 | 每个 Service 方法都是候选点 | 如 UserService.getUserById() |
| Weaving 织入 | Spring 启动时自动生成代理 | 透明,开发者无感知 |
二、AOP 实现原理对比:JDK vs CGLIB
Spring AOP 的底层实现就是动态代理。JDK 和 CGLIB 是两种实现方式,面试常问。

2.1 JDK 动态代理(接口代理)
核心原理:java.lang.reflect.Proxy + InvocationHandler。
// JDK 动态代理核心流程
public class JdkProxyDemo {
public static void main(String[] args) {
// 1. 目标对象(必须实现接口)
UserService target = new UserServiceImpl();
// 2. 创建代理
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxyObj, method, args1) -> {
System.out.println(">>> 前置增强 <<<");
Object result = method.invoke(target, args1); // 反射调用
System.out.println(">>> 后置增强 <<<");
return result;
}
);
// 3. 通过代理调用
proxy.getUserById(1L); // 走 InvocationHandler.invoke()
}
}
调用链路:Client → $ProxyN.method() → InvocationHandler.invoke() → method.invoke(target, args) → 目标方法
前置条件:目标对象必须实现至少一个接口。没有接口的类无法使用 JDK 动态代理。
2.2 CGLIB 代理(子类代理)
核心原理:通过 ASM 字节码技术生成目标类的子类作为代理。
// CGLIB 代理核心流程
public class CglibProxyDemo {
public static void main(String[] args) {
// 1. 创建 Enhancer
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CourseServiceImpl.class); // 继承目标类
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> {
System.out.println(">>> 前置增强 <<<");
Object result = proxy.invokeSuper(obj, args1); // FastClass 索引调用
System.out.println(">>> 后置增强 <<<");
return result;
});
// 2. 创建代理对象(目标类的子类实例)
CourseServiceImpl proxy = (CourseServiceImpl) enhancer.create();
proxy.listCourses();
}
}
调用链路:Client → 子类代理.method() → MethodInterceptor.intercept() → FastClass.invoke(index, args) → 目标方法
前置条件:目标类不能是 final,方法不能是 final/private/static(无法被子类覆写)。
2.3 关键对比
| 对比维度 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 代理原理 | 接口代理,Proxy.newProxyInstance() | 子类代理,Enhancer.create() |
| 方法调用 | 反射 method.invoke() —— 有性能开销 | FastClass 索引调用 —— 无反射开销 |
| 创建成本 | 较小 —— 仅生成单个 Proxy 类 | 较大 —— 需要 ASM 字节码操作 |
| 前置条件 | 目标类必须实现接口 | 目标类不能是 final,方法不能 final/private/static |
| 构造器调用 | 不拦截(接口无构造器可言) | 会拦截(子类构造器会调用父类构造器) |
| Spring Boot 2.x 默认 | (已不默认) | 默认且推荐 |
2.4 CGLIB 全面碾压?不一定
JDK 动态代理在 JDK 8+ 之后,反射性能已大幅优化(MethodHandle + LambdaMetafactory)。在调用链路较深的场景中,CGLIB FastClass 的索引查找也有开销。
但 CGLIB 最大的优势不是性能,而是不需要接口——这让你可以代理任意的 @Service 或 @Component 类。
2.5 Spring AOP 选型规则
在 Spring AOP 中,DefaultAopProxyFactory 的选型逻辑如下:
// DefaultAopProxyFactory.createAopProxy() 简化逻辑
public AopProxy createAopProxy(AdvisedSupport config) {
if (config.isOptimize() || config.isProxyTargetClass()
|| hasNoUserSuppliedProxyInterfaces(config)) {
// 使用 CGLIB
return new CglibAopProxy(config);
} else {
// 使用 JDK 动态代理
return new JdkDynamicAopProxy(config);
}
}
总结:只要满足「有接口」+「没有显式配置 proxyTargetClass=true」+「没有 optimize」,Spring 就用 JDK。否则一律 CGLIB。
在 Spring Boot 2.x 中,
spring.aop.proxy-target-class默认值为true,所以默认走 CGLIB。
三、Spring AOP 注解实战:EduLearn 案例
理论到位了,来写代码。本节用 EduLearn 平台的两个真实场景展示 AOP 的完整用法。
3.1 环境准备
<!-- pom.xml 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
注意:
spring-boot-starter-web已间接依赖 aop,但显式引入更清晰。
3.2 案例一:统一日志切面
需求:记录每个 Service 方法的入参、出参和耗时,方便排查线上问题。
package com.edulearn.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Slf4j
@Aspect
@Component
public class LogAspect {
// 切点:拦截 service 包下所有 public 方法
@Pointcut("execution(public * com.edulearn.service..*.*(..))")
public void serviceLayer() {}
// 环绕通知:记录入参、出参和耗时
@Around("serviceLayer()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
// 1. 前置:记录入参
log.info("[AOP] {} 入参: {}", methodName, Arrays.toString(args));
long start = System.currentTimeMillis();
Object result;
try {
// 2. 执行目标方法
result = joinPoint.proceed();
} catch (Throwable e) {
// 3. 异常:记录异常信息
log.error("[AOP] {} 异常: {}", methodName, e.getMessage());
throw e;
}
// 4. 后置:记录出参和耗时
long elapsed = System.currentTimeMillis() - start;
log.info("[AOP] {} 出参: {} | 耗时: {}ms", methodName, result, elapsed);
return result;
}
}
核心要点:
@Around是最灵活的通知——你可以控制是否执行目标方法(joinPoint.proceed())、修改返回值、吞掉异常。joinPoint.getSignature().toShortString()输出如UserServiceImpl.getUserById(..)。- 耗时统计用
System.currentTimeMillis()即可,对绝大多数场景精度足够。
3.3 案例二:权限校验切面
需求:某些方法需要特定角色才能调用(如「删除课程」需要 ADMIN 角色)。用自定义注解 + AOP 实现声明式鉴权。
// ===== 1. 自定义注解 =====
package com.edulearn.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireRole {
/** 需要的角色 */
String value() default "USER";
}
// ===== 2. 权限校验切面 =====
package com.edulearn.aspect;
import com.edulearn.annotation.RequireRole;
import com.edulearn.common.util.UserContext; // 线程绑定的当前用户信息
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AuthAspect {
// 切点:所有标注了 @RequireRole 的方法
@Pointcut("@annotation(requireRole)")
public void authPoint(RequireRole requireRole) {}
@Around(value = "authPoint(requireRole)", argNames = "joinPoint,requireRole")
public Object checkAuth(ProceedingJoinPoint joinPoint, RequireRole requireRole) throws Throwable {
// 1. 获取当前登录用户的角色
String currentRole = UserContext.getCurrentRole();
String requiredRole = requireRole.value();
// 2. 角色匹配
if (!requiredRole.equals(currentRole)) {
String msg = String.format("权限不足!接口 %s 需要角色 [%s],当前角色 [%s]",
joinPoint.getSignature().getName(), requiredRole, currentRole);
log.warn("[Auth] {}", msg);
throw new RuntimeException(msg); // 实际项目中抛出自定义异常
}
// 3. 鉴权通过,执行目标方法
log.info("[Auth] 鉴权通过:{} 以 [{}] 角色调用", joinPoint.getSignature().getName(), currentRole);
return joinPoint.proceed();
}
}
// ===== 3. 在 Controller 中使用 =====
@RestController
@RequestMapping("/api/courses")
public class CourseController {
@Autowired
private CourseService courseService;
// 普通用户都可以查看
@GetMapping
public List<CourseVO> listCourses() { return courseService.listCourses(); }
// 只有 ADMIN 才能删除
@RequireRole("ADMIN")
@DeleteMapping("/{courseId}")
public void deleteCourse(@PathVariable Long courseId) {
courseService.deleteCourse(courseId);
}
}
核心要点:
@annotation(requireRole)匹配被@RequireRole标注的方法,并直接将注解实例绑定到方法参数。- 通过切面参数绑定获取注解值(
requireRole.value()),无需在切面中反射读取。 UserContext是一个线程绑定的工具类(通常通过ThreadLocal+Filter/Interceptor填充),确保并发安全。
3.4 结果验证
// 启动后访问:
// GET /api/courses → 无需 @RequireRole,直接返回课程列表
// DELETE /api/courses/1001 → 日志打印 "[Auth] 鉴权通过" 或 "权限不足"
四、切点表达式语法精讲
切点表达式是 AOP 中最容易写错的环节。本节覆盖 7 种指示符,附常见错误案例。
4.1 execution —— 最常用,精确匹配方法
语法骨架:
execution(修饰符? 返回值 包名.类名.方法名(参数列表) throws-异常?)
| 部分 | 说明 | 示例 |
|---|---|---|
* | 匹配任意单个部分 | * com..*.get*(..) — 任意返回值、任意包、get开头方法 |
.. | 匹配任意多层 | com.edulearn.. — 匹配 com.edulearn 及其所有子包 |
+ | 匹配子类 | UserService+ — 匹配 UserService 及其所有实现类 |
(..) | 任意参数 | 无参到多参都匹配 |
实战表达式:
// 1. 匹配 service 包下所有 public 方法,任意返回值
@Pointcut("execution(public * com.edulearn.service..*.*(..))")
// 2. 匹配 Controller 中所有返回 ResponseEntity 的方法
@Pointcut("execution(org.springframework.http.ResponseEntity com.edulearn.controller..*.*(..))")
// 3. 匹配第一个参数为 Long 类型的方法(常用于根据 ID 查询)
@Pointcut("execution(* com.edulearn.service..*.*(Long, ..))")
// 4. 匹配 save 或 update 开头的方法
@Pointcut("execution(* com.edulearn.service..*.save*(..)) || execution(* com.edulearn.service..*.update*(..))")
4.2 其他 6 种指示符速查
| 指示符 | 匹配维度 | 语法 | 典型场景 |
|---|---|---|---|
within | 类/包范围 | within(com.edulearn.service.*) | 匹配某包下所有类的所有方法(不关心方法签名) |
this | 代理对象类型 | this(com.edulearn.service.UserService) | 代理对象是某类型(注意:JDK代理时 this 是接口类型) |
target | 目标对象类型 | target(com.edulearn.service.UserServiceImpl) | 目标对象是某类型(与 this 的区别见下文) |
args | 方法参数类型 | args(java.lang.Long, ..) | 匹配第一个参数是 Long 的方法 |
@annotation | 方法上的注解 | @annotation(com.edulearn.annotation.RequireRole) | 匹配被指定注解标注的方法(可绑定参数) |
@within | 类上的注解 | @within(org.springframework.stereotype.Service) | 匹配类上标注了 @Service 的所有方法 |
bean | Spring Bean 名称 | bean(userService) | 匹配指定名称的 Bean(Spring 独有,非 AspectJ) |
4.3 this vs target —— 面试高频细节
// 假设 UserServiceImpl implements UserService
// JDK 代理:代理对象类型 = UserService(接口)
// CGLIB 代理:代理对象类型 = UserServiceImpl 的子类
// this(UserService) → JDK 代理时匹配,CGLIB 代理时也匹配(子类 instanceof 接口)
// this(UserServiceImpl) → JDK 代理时不匹配(代理不是 UserServiceImpl),CGLIB 时匹配
// target(UserServiceImpl) → 无论如何都匹配(目标对象始终是 UserServiceImpl)
4.4 常见错误 Top 3
| 错误 | 错误写法 | 正确写法 | 原因 |
|---|---|---|---|
包路径少了 .. | execution(* com.edulearn.service.*.*(..)) | execution(* com.edulearn.service..*.*(..)) | service.* 只匹配 service 目录,不包含子包 |
@annotation 缺少全限定名 | @annotation(RequireRole) | @annotation(com.edulearn.annotation.RequireRole) | 切点表达式不是 Java 代码,没有 import |
| 修饰符不匹配 | execution(* *.privateMethod(..)) | Spring AOP 无法代理 private 方法 | private 方法无法被子类覆写,也无法被接口代理 |
五、通知类型与执行顺序
5.1 五种通知的完整对比
| 通知 | 注解 | 执行时机 | 可否阻止执行 | 可否获取返回值 | 可否修改返回值 |
|---|---|---|---|---|---|
| 前置通知 | @Before | 目标方法执行前 | |||
| 返回通知 | @AfterReturning | 目标方法正常返回后 | |||
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 | |||
| 最终通知 | @After | 方法执行后(finally) | |||
| 环绕通知 | @Around | 包围目标方法 |
5.2 同一切面内的执行顺序

关键规则(面试必问):
- @Around 包裹一切:前置逻辑最先执行,后置逻辑最后执行
- @Before 在 proceed() 之后立即执行
- 方法正常返回:走 @AfterReturning → @After → @Around 后置
- 方法抛异常:走 @AfterThrowing → @After → @Around 后置
- @After 总是执行(类似于 finally 块),且 @AfterReturning/@AfterThrowing 与 @After 的顺序是固定的:前者先于后者
- @AfterReturning 和 @AfterThrowing 互斥
5.3 多切面执行顺序 —— @Order 洋葱模型

@Aspect @Component @Order(1) // 数字越小,优先级越高 = 越外层
public class LogAspect { }
@Aspect @Component @Order(2) // 中间层
public class AuthAspect { }
@Aspect @Component @Order(3) // 最内层
public class CacheAspect { }
执行顺序(洋葱模型,从外到内再回到外):
请求进入:
@Order(1) Around 前置 → @Order(2) Around 前置 → @Order(3) Around 前置
→ @Order(3) Before → @Order(2) Before → @Order(1) Before
→ 目标方法执行
→ @Order(1) AfterReturning → @Order(2) AfterReturning → @Order(3) AfterReturning
→ @Order(1) After → @Order(2) After → @Order(3) After
→ @Order(3) Around 后置 → @Order(2) Around 后置 → @Order(1) Around 后置
响应返回 ←

记忆口诀:@Order 值越小越「外面」—— 先进入、后退出,像剥洋葱。同一切面内 @AfterReturning 先于 @After。
5.4 失效场景
如果在 @Around 中吞掉了 proceed() 的返回值或没有调用 proceed():
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) {
// ❌ 没有 return,也没有调用 proceed()
// 后续的 @After 和 @AfterReturning 都不会执行
return null; // 甚至返回 null 覆盖真实结果
}
规则:只要
proceed()没被调用,目标方法就不执行,后续所有通知(@AfterReturning / @AfterThrowing / @After / @Around 后置)都不会触发。
六、源码走读:AOP 代理创建流程
面试中,当你说「Spring AOP 基于动态代理」后,面试官很可能追问:「代理是怎么创建的?」这一节逐层走读源码。
6.1 入口:AbstractAutoProxyCreator
Spring 通过 BeanPostProcessor 的 postProcessAfterInitialization 钩子在 Bean 初始化后创建代理。
// AbstractAutoProxyCreator.postProcessAfterInitialization()
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 关键调用:判断是否需要代理,需要则创建
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
核心判断:「这个 Bean 是否需要被代理?」——由 wrapIfNecessary 回答。
6.2 wrapIfNecessary:是否需要代理
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
// 1. 已经处理过?直接返回
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// 2. 不应该被代理?跳过(Advice/Pointcut/Advisor/AopInfrastructureBean 自身)
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
return bean;
}
// 3. 获取匹配的 Advisors(切面通知列表)
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
// 4. 有匹配的 Advisor → 创建代理!
Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors,
new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
// 5. 无匹配 → 返回原始 Bean(不做代理)
return bean;
}
关键步骤:
- 检查是否已处理 / 是否为基础设施类
- 遍历所有 Advisor,看哪些切点匹配当前 Bean
- 如果有匹配 →
createProxy(),没有 → 返回原 Bean
6.3 createProxy:创建代理对象
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
// 判断是否强制使用 CGLIB
if (proxyFactory.isProxyTargetClass()) {
if (Proxy.isProxyClass(beanClass)) {
// JDK 代理的类再被代理 → 添加接口
for (Class<?> ifc : beanClass.getInterfaces()) {
proxyFactory.addInterface(ifc);
}
}
} else {
// 默认逻辑:有接口用 JDK,无接口自动升级 CGLIB
// (由 DefaultAopProxyFactory 内部判断)
}
// ... 添加 Advisor、设置 TargetSource ...
return proxyFactory.getProxy(getProxyClassLoader());
}
6.4 getProxy:选型与创建
// ProxyFactory.getProxy() → DefaultAopProxyFactory.createAopProxy()
public AopProxy createAopProxy(AdvisedSupport config) {
if (!NativeDetector.inNativeImage() &&
(config.isOptimize() || config.isProxyTargetClass()
|| hasNoUserSuppliedProxyInterfaces(config))) {
return new ObjenesisCglibAopProxy(config); // Spring 5.x+ 使用 Objenesis 实例化
} else {
return new JdkDynamicAopProxy(config);
}
}
6.5 两种 AopProxy 的调用流程
JdkDynamicAopProxy.invoke():
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 获取拦截器链(Advisor 列表 → MethodInterceptor 列表)
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) {
// 无拦截器 → 直接反射调用目标方法
return AopUtils.invokeJoinpointUsingReflection(target, method, args);
}
// 2. 构造 ReflectiveMethodInvocation,依次执行拦截链
MethodInvocation invocation = new ReflectiveMethodInvocation(
proxy, target, method, args, targetClass, chain);
return invocation.proceed();
}
CglibAopProxy(DynamicAdvisedInterceptor):
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) {
// 1. 获取拦截器链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) {
// 无拦截器 → 直接通过 methodProxy 调用(FastClass,无反射)
return methodProxy.invoke(target, args);
}
// 2. 构造 CglibMethodInvocation
CglibMethodInvocation invocation = new CglibMethodInvocation(
proxy, target, method, args, targetClass, chain, methodProxy);
return invocation.proceed(); // proceed() 依次执行拦截器链
}
6.6 拦截器链执行流程
ReflectiveMethodInvocation.proceed() 的核心逻辑:
public Object proceed() throws Throwable {
// currentInterceptorIndex 从 -1 开始,每次 +1
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
// 所有拦截器执行完毕 → 调用目标方法
return invokeJoinpoint();
}
// 取出下一个拦截器,执行其 invoke(this)
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
5 种通知对应 MethodInterceptor 的实现类:
| 通知 | MethodInterceptor 实现 |
|---|---|
| @Before | MethodBeforeAdviceInterceptor → 先调 advice.before(),再 proceed() |
| @AfterReturning | AfterReturningAdviceInterceptor → 先 proceed() 得到结果,再调 advice.afterReturning() |
| @AfterThrowing | AspectJAfterThrowingAdvice → try-proceed,catch 后调 invokeAdviceMethod() |
| @After | AspectJAfterAdvice → try-finally,finally 中调 advice(一定执行) |
| @Around | AspectJAroundAdvice → 调 advice 的 around 方法,由开发者决定 proceed() |
七、面试真题与追问链
本节模拟真实面试场景,每个考点逐层深入追问。
7.1 考点一:Spring AOP 的底层原理是什么?
第 1 层(初级回答):
Spring AOP 基于动态代理实现。如果目标对象实现了接口,用 JDK 动态代理;如果没有接口,用 CGLIB 代理。
第 2 层(追问:两种代理的区别?):
JDK 动态代理基于接口,通过
Proxy.newProxyInstance()创建代理,调用时通过InvocationHandler.invoke()→ 反射method.invoke()调用目标方法。
CGLIB 基于子类继承,通过Enhancer.create()创建代理,调用时通过MethodInterceptor.intercept()→ FastClass 索引调用(无反射开销)。
第 3 层(追问:Spring 如何选择用哪个?):
DefaultAopProxyFactory.createAopProxy()中判断:如果config.isProxyTargetClass()为 true,或目标类没有接口,就用 CGLIB。Spring Boot 2.x 默认proxy-target-class=true,所以默认走 CGLIB。
第 4 层(追问:CGLIB 有什么限制?):
final 类无法被代理(不能继承),final/private/static 方法无法被增强(不能覆写)。另外 CGLIB 无法代理构造器调用(虽然会拦截子类构造器中的方法调用,但那是另一个话题)。
第 5 层(追问:你提到 CGLIB 用 FastClass 无反射,那它真的完全无反射吗?):
FastClass 通过方法签名到索引号的映射来避免反射。但 FastClass 本身是通过反射生成的(遍历目标类的方法)。创建时有一次反射成本,后续调用无反射。这是「空间换时间」策略。
7.2 考点二:Spring AOP 自调用失效,为什么?
场景描述:
@Service
public class OrderService {
@Transactional // AOP 增强
public void createOrder() {
this.payOrder(); // 直接调用 this,不走代理!
}
@Transactional // AOP 增强
public void payOrder() { /* ... */ }
}
第 1 层:
this.payOrder()是直接调用当前对象的方法,没有经过代理对象,所以 AOP 拦截不到。
第 2 层(追问:为什么 this 不走代理?):
Spring AOP 的代理是「外套」——代理对象内部持有一个 target(原始对象)。外部调用
proxy.createOrder()时经过代理外套,但createOrder内部的this指向的是 target(原始对象),它的方法调用直接发生在原始对象上,完全绕过了代理。
第 3 层(追问:怎么解决?):
方案一:自己注入自己(推荐,简单有效)
@Service
public class OrderService {
@Autowired
private OrderService self; // Spring 注入的是代理对象
public void createOrder() {
self.payOrder(); // 走代理!
}
}
方案二:AopContext.currentProxy()(需要配置 exposeProxy=true)
@EnableAspectJAutoProxy(exposeProxy = true) // 配置类
@Service
public class OrderService {
public void createOrder() {
((OrderService) AopContext.currentProxy()).payOrder();
}
}
方案三:重构,将 payOrder 提取到独立的 Service。
第 4 层(追问:@Transactional 自调用失效是最常见的坑,原理一样吗?):
完全一样。
@Transactional本身就是通过 AOP 实现的(TransactionInterceptor是一个 Advisor),所以自调用同样会让事务不生效。
7.3 考点三:@Transactional 为什么依赖 AOP?
第 1 层:
Spring 的事务管理通过 AOP 实现。
@Transactional注解的方法被TransactionInterceptor拦截,在方法前开启事务,方法正常返回后提交,方法抛异常后回滚。
第 2 层(追问:TransactionInterceptor 在拦截器链中的位置?):
TransactionInterceptor是一个MethodInterceptor,它被封装在BeanFactoryTransactionAttributeSourceAdvisor中。和其他 Advisor 一样,在AbstractAutoProxyCreator.wrapIfNecessary()阶段被匹配和应用。
第 3 层(追问:如果同时有 @Transactional 和自定义 @Around,执行顺序?):
由
@Order或 Advisor 的加载顺序决定。通常事务 Advisor 的优先级较高(在自定义切面之前),可以通过@EnableTransactionManagement(order = N)调整。
第 4 层(追问:事务传播机制和 AOP 有什么关系?):
事务传播机制(REQUIRED / REQUIRES_NEW / NESTED 等)通过
TransactionInterceptor在invoke()中判断当前是否存在事务,决定是加入还是新建。每次方法调用都经过拦截器链,事务状态通过TransactionSynchronizationManager在 ThreadLocal 中维护。
7.4 追问链速查表
| 考点 | L1 基础 | L2 进阶 | L3 深入 | L4 发散 |
|---|---|---|---|---|
| AOP 原理 | 动态代理 | JDK vs CGLIB 区别 | DefaultAopProxyFactory 选型逻辑 | CGLIB 限制(final/构造器) |
| 自调用失效 | this 不走代理 | 代理=外套+target | 自己注入自己 / AopContext | @Transactional 同原理 |
| @Transactional | AOP 实现事务 | TransactionInterceptor 机制 | 多 Advisor 排序 | 事务传播与 ThreadLocal |
| 循环依赖 | 三级缓存 | AOP 提前曝光 | getEarlyBeanReference | @Async 与循环依赖的冲突 |
必背概念速查表
| 概念 | 一句话定义 | 面试金句 |
|---|---|---|
| AOP | 面向切面编程,将横切关注点模块化 | 「在不修改源码的情况下动态添加功能」 |
| Aspect 切面 | 切点 + 通知的组合 | 「Aspect = Where + What」 |
| Pointcut 切点 | 定义通知在何处执行 | 「从无数 JoinPoint 中筛选目标」 |
| Advice 通知 | 在切点处执行的逻辑 | 「5 种:Before / After / Around / AfterReturning / AfterThrowing」 |
| JoinPoint 连接点 | 可插入切面的候选位置 | 「Spring AOP 只支持方法执行」 |
| Weaving 织入 | 将切面应用到目标对象 | 「Spring = 运行时动态代理织入」 |
| JDK 动态代理 | 基于接口的代理 | 「反射 method.invoke(),必须实现接口」 |
| CGLIB 代理 | 基于子类的代理 | 「FastClass 索引调用,不能是 final 类」 |
| 自调用失效 | this 调用不走代理 | 「代理=外套,this 指向原始 target」 |
| @Order | 多切面排序 | 「值越小越外层,洋葱模型」 |
| proceed() | 执行目标方法 | 「不调 proceed() = 后续通知全不触发」 |
| exposeProxy | 暴露当前代理对象 | 「AopContext.currentProxy() 解决自调用」 |
下期速递:第五期「Spring 事务深度解析」—— 事务传播机制 7 种行为逐一带案例、事务失效的 5 大场景、@Transactional 源码走读,以及「为什么加了 @Transactional 却没回滚?」的灵魂追问。
:Spring AOP 深度解析&spm=1001.2101.3001.5002&articleId=161634437&d=1&t=3&u=f0e8476496544f919b3007c49ce57249)
617

被折叠的 条评论
为什么被折叠?



