Lesson 53 · Spring 生态核心原理

Spring AOP 原理:JDK 动态代理 vs CGLIB

高级·⭐ 必问·#Spring·#AOP·#代理

第 1 站

开场:面试官到底想听什么

"Spring AOP 的底层实现原理?"——这道题几乎是高级 Java 面试的必考题。

面试官真正想考的是三层理解:

  1. 概念层——你能准确说出 Aspect、Joinpoint、Pointcut、Advice 的关系;
  2. 实现层——JDK 动态代理和 CGLIB 各自的原理、区别、限制;
  3. 工程层——Spring Boot 2.x 为什么把默认改成了 CGLIB?代理失效怎么排查?

很多候选人背了"有接口用 JDK,没接口用 CGLIB"就上场了。面试官追问一句"Boot 2.x 之后呢?"或者"自调用为什么失效?"就卡壳。本文把 AOP 从概念到字节码全部拆透,让你回答这道题时滴水不漏。

面试核心

AOP ≠ 背概念。面试官要的是"我知道代理对象是怎么生成的"+"我能排查代理失效的坑"。

第 2 站

AOP 核心概念:五个角色各就各位

AOP 的本质是在不修改源代码的前提下,给程序动态添加横切关注点(日志、事务、权限……)。要理解它,先搞清楚五个核心概念的协作关系:

AOP 五大核心概念关系图 Aspect 切面 = Pointcut + Advice Pointcut 切点 "在哪里织入" Advice 通知 "织入什么逻辑" Joinpoint 连接点 所有可被拦截的方法 从…中筛选 Weaving 织入 生成代理对象的过程 执行动作 Joinpoint 是候选,Pointcut 是筛选条件,Advice 是要执行的代码,Weaving 是最终动作
图 1 AOP 五大核心概念关系图

Advice 的 5 种类型

类型注解执行时机典型场景
前置通知@Before目标方法执行前参数校验、日志
后置通知@After目标方法执行后(无论是否异常)资源清理
返回通知@AfterReturning目标方法正常返回后结果加工
异常通知@AfterThrowing目标方法抛出异常后异常记录
环绕通知@Around包裹目标方法,完全控制事务、缓存、性能统计
易错点

@Around 必须手动调用 ProceedingJoinPoint.proceed(),否则目标方法不会执行。很多人写环绕通知忘了这一行,方法"神秘消失"。

一句话总结

Aspect = Pointcut(在哪切)+ Advice(切了干什么)。Joinpoint 是所有可以被切的候选点,Pointcut 从中筛选,Weaving 是最终把 Advice 织入 Joinpoint 的动作。

第 3 站

JDK 动态代理:Proxy + InvocationHandler

JDK 动态代理是 Java 原生提供的代理机制,核心 API 只有一个:Proxy.newProxyInstance()。它的要求很明确——被代理类必须实现接口

工作原理三步走

  1. 运行时生成一个实现了目标接口的代理类(类名形如 $Proxy0);
  2. 代理类的每个方法调用都转发给 InvocationHandler.invoke()
  3. invoke() 里执行增强逻辑 + 反射调用目标对象的方法。
JDK 动态代理完整示例
// 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) 的形式。

JDK 代理 = Proxy.newProxyInstance(classLoader, interfaces, handler)
第 4 站

CGLIB 代理:子类字节码增强

CGLIB(Code Generation Library)通过 ASM 在运行时动态生成目标类的子类来实现代理。它不要求接口,但有限制:无法代理 final 类和 final 方法

工作原理

  1. 在运行时生成目标类的子类(类名形如 UserService$$EnhancerByCGLIB$$xxx);
  2. 子类重写目标类的所有非 final 方法;
  3. 每个重写方法内部通过 MethodInterceptor.intercept() 进行拦截;
  4. 通过 FastClass 机制绕过反射直接调用,性能优于 JDK 代理。
CGLIB 代理完整示例
// 目标类(不需要接口)
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 方法静默跳过
第 5 站

Spring 的选择策略:Boot 2.x 的分水岭

Spring 在创建代理时有一套决策逻辑,而且这个逻辑在 Spring Boot 2.0 发生了重大变化

Spring AOP 代理选择决策(JDK 代理 vs CGLIB) Spring Framework / Boot 1.x proxyTargetClass = false(默认) 目标类实现了接口? JDK 动态代理 基于接口 CGLIB 代理 基于子类 强制 CGLIB: @EnableAspectJAutoProxy (proxyTargetClass = true) 问题: 代理类型不一致(接口→JDK,无接口→CGLIB) 注入时类型可能不匹配 JDK 代理无法强转为实现类 Spring Boot 2.x+ proxyTargetClass = true(默认) 目标类是 final? CGLIB 代理 统一使用 抛异常 无法代理 显式切回 JDK 代理: spring.aop.proxy-target-class=false 优势: 代理类型统一,行为可预测 代理对象可直接强转为实现类 避免了 JDK 代理的各种坑
图 2 Spring AOP 代理选择策略:Boot 2.x 前后对比

为什么 Boot 2.x 要改默认?

JDK 代理有一个非常隐蔽的坑:代理对象只实现了接口,不能强转为具体实现类

JDK 代理的经典坑
@Service
public class UserServiceImpl implements UserService { ... }

// JDK 代理下(proxyTargetClass=false)
@Autowired
private UserServiceImpl userService;  // ❌ BeanNotOfRequiredTypeException!

// 正确做法:面向接口编程
@Autowired
private UserService userService;  // ✅ 注入接口类型

Boot 2.x 把默认改成 CGLIB 后,代理对象是目标类的子类,强转为实现类不会出错。这个改动消除了大量"按接口注入没问题,按实现类注入就报错"的诡异 bug。

Spring 内部决策伪代码
// 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 代理生成的对象不能强转为实现类,容易导致注入类型不匹配。"

第 6 站

代理失效场景:四大经典坑

这是面试中区分"背过概念"和"真正理解"的关键环节。以下四种场景 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 方法

private 方法无法被代理
@Service
public class PayService {
    @Transactional
    private void deductBalance() {  // ❌ private 方法,AOP 无法生效
        // CGLIB 子类无法重写 private 方法
        // JDK 代理只能代理接口方法
    }
}

坑 3:final 方法 / 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 的对象没有代理
// 手动 new 的对象不经过 Spring 容器,没有代理增强
UserService service = new UserServiceImpl();
service.save("Bob");  // ❌ 任何 @Transactional 等注解都不会生效

// 正确做法:从容器获取
UserService service = applicationContext.getBean(UserService.class);
service.save("Bob");  // ✅ 拿到的是代理对象
速记口诀

代理失效四场景:自调、私有、终态、手 new。自调用最常考,AopContext 或拆类解决。

第 7 站

总结:面试满分答案模板

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.invokeCGLIB 通过 ASM 生成目标类的子类,重写方法实现拦截,内部用 FastClass 避免反射开销。Spring Boot 2.x 之前默认 JDK 代理(有接口走 JDK),2.x 之后默认 CGLIB,解决了 JDK 代理不能强转为实现类的问题。代理失效有四种场景:同类自调用绕过代理、private 方法、final 类/方法、非 Spring 管理的对象。"

最后叮嘱

面试官追问"自调用怎么解决"时,能说出 AopContext.currentProxy() 和"拆类"两种方案就加分。能讲出 CGLIB 的 FastClass 机制就是高级选手了。