JDK 动态代理类和委托类需要都实现同一个接口。也就是说只有实现了某个接口的类可以使用 Java 动态代理机制。但是,事实上使用中并不是遇到的所有类都会给你实现一个接口。因此,对于没有实现接口的类,就不能使用该机制。而 CGLIB 则可以实现对类的动态代理。

这段话点出了两种代理的根本差异:JDK 代理基于接口,CGLIB 代理基于继承。但要在 Spring 里用好它们,还需要理解代理为什么存在、Spring 如何二选一、以及各自的边界。

为什么需要代理

Spring AOP 要在不修改业务代码的前提下织入事务、日志、权限等横切逻辑。做法是:容器不直接暴露目标对象,而是暴露一个”长得一样、但方法前后能插逻辑”的代理对象。调用方无感知,增强逻辑在代理层完成——这就是代理模式在框架里的落地。

JDK 动态代理

三要素:Proxy.newProxyInstance(classLoader, interfaces, handler) + 实现 InvocationHandler:

Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(p, method, args) -> {
before(); // 前置增强
Object ret = method.invoke(target, args);
after(); // 后置增强
return ret;
});

原理:运行期由 JVM 生成实现指定接口的代理类 $Proxy0,其方法体统一转发到 InvocationHandler。限制很明显:目标类必须实现接口,且代理只覆盖接口方法。

CGLIB 动态代理

CGLIB 用 ASM 在运行期生成目标类的子类,覆写非 final 方法并织入回调(MethodInterceptor):

  • 不要求接口,直接代理类
  • 限制:final 类/方法无法代理;通过继承实现,构造器会被调用(Objenesis 可绕过)
  • Spring 内置了 repackaged 的 CGLIB,无需额外依赖

Spring 如何选择

默认规则:目标类实现了接口 → JDK 代理,否则 CGLIB。但 Spring Boot 2.x 起 spring.aop.proxy-target-class 默认为 true,即统一使用 CGLIB——原因是 JDK 代理下”按类注入”会失败(代理对象不是目标类的实例),统一 CGLIB 减少了这类困惑。

这个差异会真实地咬人:

@Autowired UserServiceImpl impl;  // 若实际是 JDK 代理 → 注入失败
@Autowired UserService iface; // 两种代理都安全

所以社区惯例是面向接口注入,让代理方式的选择对业务透明。

性能与语义边界

  • 生成成本:CGLIB 生成子类字节码比 JDK 生成接口代理慢,但代理对象创建后调用性能差异很小(JDK8 之后反射调用已大幅优化)
  • 自调用失效:同类中方法 A 调用方法 B,走的是 this 而非代理对象,B 上的事务/缓存注解不生效。这是代理模型最著名的坑,解法是拆类、注入自身或 AopContext
  • 代理只增强 public(CGLIB 含 protected/package,但语义上 AOP 只承诺 public)方法

手写最小代理体会本质

interface Service { void run(); }

Service target = () -> System.out.println("biz");
Service proxy = (Service) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{Service.class},
(p, m, a) -> { System.out.println("log"); return m.invoke(target, a); });

十行代码即 JDK 代理全貌:接口 + 转发 + 插入点。CGLIB 只是把”实现接口”换成”继承类”,思想完全一致。

理解动态代理的关键不是背 API,而是抓住一句话:代理对象是容器与调用方之间的契约替身,增强发生在替身层。所有 AOP 的坑(自调用、注入类型、final 限制)都能从这句话推出来。