Lesson 53 · Spring 生态核心原理
Spring AOP 原理:JDK 动态代理 vs CGLIB
开场:面试官到底想听什么
"Spring AOP 的底层实现原理?"——这道题几乎是高级 Java 面试的必考题。
面试官真正想考的是三层理解:
- 概念层——你能准确说出 Aspect、Joinpoint、Pointcut、Advice 的关系;
- 实现层——JDK 动态代理和 CGLIB 各自的原理、区别、限制;
- 工程层——Spring Boot 2.x 为什么把默认改成了 CGLIB?代理失效怎么排查?
很多候选人背了"有接口用 JDK,没接口用 CGLIB"就上场了。面试官追问一句"Boot 2.x 之后呢?"或者"自调用为什么失效?"就卡壳。本文把 AOP 从概念到字节码全部拆透,让你回答这道题时滴水不漏。
AOP ≠ 背概念。面试官要的是"我知道代理对象是怎么生成的"+"我能排查代理失效的坑"。
AOP 核心概念:五个角色各就各位
AOP 的本质是在不修改源代码的前提下,给程序动态添加横切关注点(日志、事务、权限……)。要理解它,先搞清楚五个核心概念的协作关系:
Advice 的 5 种类型
| 类型 | 注解 | 执行时机 | 典型场景 |
|---|---|---|---|
| 前置通知 | @Before | 目标方法执行前 | 参数校验、日志 |
| 后置通知 | @After | 目标方法执行后(无论是否异常) | 资源清理 |
| 返回通知 | @AfterReturning | 目标方法正常返回后 | 结果加工 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 | 异常记录 |
| 环绕通知 | @Around | 包裹目标方法,完全控制 | 事务、缓存、性能统计 |
@Around 必须手动调用 ProceedingJoinPoint.proceed(),否则目标方法不会执行。很多人写环绕通知忘了这一行,方法"神秘消失"。
Aspect = Pointcut(在哪切)+ Advice(切了干什么)。Joinpoint 是所有可以被切的候选点,Pointcut 从中筛选,Weaving 是最终把 Advice 织入 Joinpoint 的动作。
JDK 动态代理:Proxy + InvocationHandler
JDK 动态代理是 Java 原生提供的代理机制,核心 API 只有一个:Proxy.newProxyInstance()。它的要求很明确——被代理类必须实现接口。
工作原理三步走
- 运行时生成一个实现了目标接口的代理类(类名形如
$Proxy0); - 代理类的每个方法调用都转发给
InvocationHandler.invoke(); - 在
invoke()里执行增强逻辑 + 反射调用目标对象的方法。
// 1. 定义接口 public interface UserService { void save(String name); } // 2. 目标实现类 public class UserServiceImpl implements UserService { public void save(String name) { System.out.println("保存用户: " + name); } } // 3. 自定义 InvocationHandler public class LogHandler implements InvocationHandler { private Object target; public LogHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("[日志] 方法开始: " + method.getName()); Object result = method.invoke(target, args); // 反射调用目标 System.out.println("[日志] 方法结束: " + method.getName()); return result; } } // 4. 生成代理对象 UserService real = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( real.getClass().getClassLoader(), // 类加载器 real.getClass().getInterfaces(), // 接口列表 new LogHandler(real) // InvocationHandler ); proxy.save("Alice"); // 输出日志 + 保存用户
通过 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true 可以把 $Proxy0.class 保存到磁盘反编译。你会发现:代理类继承 java.lang.reflect.Proxy,实现了目标接口,每个方法体都是 super.h.invoke(this, m, args) 的形式。
CGLIB 代理:子类字节码增强
CGLIB(Code Generation Library)通过 ASM 在运行时动态生成目标类的子类来实现代理。它不要求接口,但有限制:无法代理 final 类和 final 方法。
工作原理
- 在运行时生成目标类的子类(类名形如
UserService$$EnhancerByCGLIB$$xxx); - 子类重写目标类的所有非 final 方法;
- 每个重写方法内部通过
MethodInterceptor.intercept()进行拦截; - 通过
FastClass机制绕过反射直接调用,性能优于 JDK 代理。
// 目标类(不需要接口) public class OrderService { public void createOrder(String orderId) { System.out.println("创建订单: " + orderId); } public final void cancelOrder(String id) { System.out.println("取消订单: " + id); // final 方法无法被代理 } } // 自定义 MethodInterceptor public class LogInterceptor implements MethodInterceptor { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("[CGLIB] 前置: " + method.getName()); Object result = proxy.invokeSuper(obj, args); // 调用父类方法 System.out.println("[CGLIB] 后置: " + method.getName()); return result; } } // 生成代理对象 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OrderService.class); enhancer.setCallback(new LogInterceptor()); OrderService proxy = (OrderService) enhancer.create(); proxy.createOrder("ORD-001"); // 有日志增强 proxy.cancelOrder("ORD-001"); // 无日志增强(final 方法)
CGLIB 内部使用 FastClass 机制:为目标类和代理类各生成一个 FastClass,通过方法索引直接调用,避免了 JDK 反射的 Method.invoke() 开销。这就是为什么 CGLIB 单次调用更快,但创建代理对象更慢(需要生成字节码)。
CGLIB 的三个限制
| 限制 | 原因 | 表现 |
|---|---|---|
| 不能代理 final 类 | 无法继承 final 类 | 抛 IllegalArgumentException |
| 不能代理 final 方法 | 子类无法重写 final 方法 | 静默跳过,不报错但无增强 |
| 不能代理 private 方法 | 子类无法访问父类 private 方法 | 静默跳过 |
Spring 的选择策略:Boot 2.x 的分水岭
Spring 在创建代理时有一套决策逻辑,而且这个逻辑在 Spring Boot 2.0 发生了重大变化。
为什么 Boot 2.x 要改默认?
JDK 代理有一个非常隐蔽的坑:代理对象只实现了接口,不能强转为具体实现类。
@Service public class UserServiceImpl implements UserService { ... } // JDK 代理下(proxyTargetClass=false) @Autowired private UserServiceImpl userService; // ❌ BeanNotOfRequiredTypeException! // 正确做法:面向接口编程 @Autowired private UserService userService; // ✅ 注入接口类型
Boot 2.x 把默认改成 CGLIB 后,代理对象是目标类的子类,强转为实现类不会出错。这个改动消除了大量"按接口注入没问题,按实现类注入就报错"的诡异 bug。
// AbstractAutoProxyCreator 简化逻辑 if (proxyTargetClass == true) { if (targetClass 是 final || targetClass 是 interface) { throw 异常; // 无法代理 } return CGLIB代理; } else { if (targetClass 实现了至少一个接口) { return JDK代理; } else { return CGLIB代理; // 没接口也只能用 CGLIB } }
"Spring Boot 2.x 之前默认 JDK 代理,有接口走 JDK 没接口走 CGLIB;2.x 之后默认 proxyTargetClass=true,统一走 CGLIB。原因是 JDK 代理生成的对象不能强转为实现类,容易导致注入类型不匹配。"
代理失效场景:四大经典坑
这是面试中区分"背过概念"和"真正理解"的关键环节。以下四种场景 AOP 会失效:
坑 1:自调用(Self-invocation)——最高频
@Service public class OrderService { @Transactional // 事务注解,依赖 AOP 代理 public void createOrder() { // 业务逻辑... saveLog(); // ❌ 自调用,直接走 this,不经过代理 } @Async // 异步注解,同样依赖 AOP public void saveLog() { // 日志保存... } }
原理:Spring 容器里存的是代理对象,外部调用 orderService.createOrder() 走的是代理。但 createOrder() 内部调 saveLog(),本质是 this.saveLog(),this 是目标对象本身,不是代理对象。
// 方案 1:AopContext.currentProxy()(需开启 exposeProxy) @EnableAspectJAutoProxy(exposeProxy = true) public void createOrder() { ((OrderService) AopContext.currentProxy()).saveLog(); // ✅ } // 方案 2:自注入(把自身注入进来) @Autowired private OrderService self; public void createOrder() { self.saveLog(); // ✅ 走代理 } // 方案 3:把方法拆到不同类(推荐) @Autowired private LogService logService; public void createOrder() { logService.saveLog(); // ✅ 跨类调用,天然走代理 }
坑 2:private 方法
@Service public class PayService { @Transactional private void deductBalance() { // ❌ private 方法,AOP 无法生效 // CGLIB 子类无法重写 private 方法 // JDK 代理只能代理接口方法 } }
坑 3:final 方法 / final 类
@Service public final class ConfigService { // ❌ final 类,CGLIB 无法继承 @Cacheable("config") public String getConfig() { ... } // 缓存注解失效 } @Service public class AuthService { @Cacheable("perm") public final boolean checkPermission() { ... } // ❌ final 方法 }
坑 4:非 Spring 管理的对象
// 手动 new 的对象不经过 Spring 容器,没有代理增强 UserService service = new UserServiceImpl(); service.save("Bob"); // ❌ 任何 @Transactional 等注解都不会生效 // 正确做法:从容器获取 UserService service = applicationContext.getBean(UserService.class); service.save("Bob"); // ✅ 拿到的是代理对象
代理失效四场景:自调、私有、终态、手 new。自调用最常考,AopContext 或拆类解决。
总结:面试满分答案模板
JDK 动态代理 vs CGLIB 全面对比
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 实现方式 | 实现目标接口 | 继承目标类(子类) |
| 是否需要接口 | 必须 | 不需要 |
| 能否代理 final | 不涉及(基于接口) | 不能代理 final 类/方法 |
| 能否代理 private | 不能 | 不能 |
| 创建速度 | 快 | 慢(需生成字节码) |
| 调用速度 | 慢(反射) | 快(FastClass 直接调用) |
| 类型转换 | 只能转为接口类型 | 可转为目标类及其接口 |
| JDK 依赖 | JDK 原生 | 需引入第三方库 |
| Spring Boot 2.x 默认 | 否 | 是 |
常见面试追问
"为什么 @Transactional 不生效?"
排查清单:① 方法是不是 private?② 是不是同一个类内部自调用?③ 类是不是 final?④ 对象是不是 Spring 管理的?⑤ 异常是不是被 catch 吃掉了(事务只回滚 RuntimeException 和 Error)?
"多个 @Aspect 的执行顺序怎么控制?"
使用 @Order 注解控制切面优先级,数字越小优先级越高。同一 Aspect 内的 Advice 执行顺序:@Before → @Around(前半) → 目标方法 → @Around(后半) → @AfterReturning/@AfterThrowing → @After。
面试回答模板(30 秒版)
"Spring AOP 底层有两种代理实现:JDK 动态代理通过 Proxy.newProxyInstance 生成接口的代理实现类,所有调用转发给 InvocationHandler.invoke;CGLIB 通过 ASM 生成目标类的子类,重写方法实现拦截,内部用 FastClass 避免反射开销。Spring Boot 2.x 之前默认 JDK 代理(有接口走 JDK),2.x 之后默认 CGLIB,解决了 JDK 代理不能强转为实现类的问题。代理失效有四种场景:同类自调用绕过代理、private 方法、final 类/方法、非 Spring 管理的对象。"
面试官追问"自调用怎么解决"时,能说出 AopContext.currentProxy() 和"拆类"两种方案就加分。能讲出 CGLIB 的 FastClass 机制就是高级选手了。