代理机制:JDK 动态代理与 CGLIB 选用

引言

Spring AOP、@Transactional@Async 的背后都是“代理”:调用者拿到的其实是代理对象。Java 有两种造代理的方式——JDK 动态代理(基于接口)与 CGLIB(基于子类),框架按规则自动二选一。理解选用规则,才能解释“为什么强转失败”“为什么 final 方法不生效”。

核心概念

  • JDK 动态代理java.lang.reflect.Proxy + InvocationHandler,要求目标类实现至少一个接口;生成的代理只实现这些接口,因此只能拦截接口上声明的方法,调用方必须以接口类型持有代理。
  • CGLIB:运行时生成目标类的子类并覆写方法,不要求接口;但 final 类无法被继承、final 方法无法被覆写,也就无法被代理。
  • Spring 对 CGLIB 做了重打包(repackaged,位于 org.springframework.cglib,随框架分发,无需另引 cglib 依赖)。
  • Spring 的选用规则(DefaultAopProxyFactory,以官方文档为准):目标类实现了接口 → 默认用 JDK 动态代理;未实现接口 → 用 CGLIB;设置 proxyTargetClass=true 则强制 CGLIB。
  • 配置入口:@EnableAspectJAutoProxy(proxyTargetClass = true);Spring Boot 默认 spring.aop.proxy-target-class=true(即默认 CGLIB,与纯 Framework 默认不同)。
  • 影响:JDK 代理下 Bean 的真实类型是接口实现代理类,@Autowired 按接口注入没问题,但若按具体类注入/强转会失败。

代码示例

// JDK 动态代理:手动演示(只认接口)
public interface Greeter { String greet(String name); }

public class JdkProxyDemo {
    public static void main(String[] args) {
        Greeter target = name -> "Hello " + name;
        Greeter proxy = (Greeter) java.lang.reflect.Proxy.newProxyInstance(
                Greeter.class.getClassLoader(),
                new Class<?>[]{Greeter.class},
                (p, method, a) -> {
                    System.out.println("[proxy] 调用 " + method.getName());
                    return method.invoke(target, a);
                });
        System.out.println(proxy.greet("Spring"));
    }
}

// CGLIB 风格:不要求接口(Enhancer 由 Spring 重打包版提供),
// 框架内无需手写——@EnableAspectJAutoProxy(proxyTargetClass = true) 即强制

注意点

  • 忘写接口时 JDK 代理无法生成,Spring 自动落到 CGLIB,别诧异“为什么这个 Bean 是 CGLIB 代理”。
  • CGLIB 下目标类/方法不能是 final;JDK 代理下接口没声明的方法(哪怕实现类有)也不会被拦截。
  • 依赖注入一律面向接口或抽象,代理类型差异才不会咬到你。
  • 判断当前用的是哪种:打印 bean.getClass() 看类名是否含 $Proxy(JDK)或 EnhancerBySpringCGLIB(CGLIB)。
  • 事务/AOP 通知顺序与代理工厂配置相关,改动 proxyTargetClass 前先确认切面与注入方式兼容。

小结

JDK 动态代理“认接口”,CGLIB“认类”,Spring 按“有接口默认 JDK、否则 CGLIB、可强制 CGLIB”选用,Boot 默认就是 CGLIB。记住代理的边界(接口方法/final 方法)即可解释大多数“代理不生效”的怪现象。

笔记加载中…