先从一个我在群里被问过无数次的问题开始:学会反射之后,下一个绕不开的点是什么?我的答案一直是动态代理。而且不只是面试要考,你日常用的Spring、MyBatis、Feign、Retrofit这些框架,底层全都在玩同一个东西。搞清楚动态代理,你基本就能看穿半套主流框架的设计套路。这篇我打算从0开始,把动态代理讲到你能在纸上徒手写出一份可用的JDK动态代理和CGLIB代理为止。
1. 为什么需要动态代理:从一个日志需求说起
很多人学动态代理一上来就背InvocationHandler、Proxy.newProxyInstance,背完就忘,因为根本没搞清楚这东西到底解决了什么问题。我们先不碰API,先还原一个最真实的开发场景。
1.1 静态实现:给每个方法手动加日志
假设你现在负责一个订单服务,接口大概是这样的:
public interface OrderService { void createOrder(String orderId, double amount); String queryOrder(String orderId); }业务很简单,但老板提了一个需求:所有订单操作都要记录日志,包括方法名、入参、耗时。你第一反应肯定是直接改实现类:
public class OrderServiceImpl implements OrderService { @Override public void createOrder(String orderId, double amount) { long start = System.currentTimeMillis(); System.out.println("调用 createOrder,参数:" + orderId + ", " + amount); // 核心业务逻辑 System.out.println("创建订单成功,耗时:" + (System.currentTimeMillis() - start) + "ms"); } @Override public String queryOrder(String orderId) { long start = System.currentTimeMillis(); System.out.println("调用 queryOrder,参数:" + orderId); // 核心业务逻辑 System.out.println("查询订单成功,耗时:" + (System.currentTimeMillis() - start) + "ms"); return "order info"; } }看起来很简单对吧?但问题马上就来了:订单服务有几十个方法,每个方法都要写一遍这段日志逻辑。更难受的是,如果明天需求变成"所有方法都要加权限校验",你又要改几十个方法。后天需求变成"所有方法都要加事务管理",你还得再改一遍。这就是横切逻辑侵入业务代码的第一个痛点——重复。
1.2 静态代理:把公共逻辑抽出来,但依然僵硬
既然直接改业务类不行,有人就想到了代理模式。我们保留OrderServiceImpl不动,创建一个代理类,让它实现同样的接口,在代理类里统一加日志逻辑:
public class OrderServiceStaticProxy implements OrderService { private final OrderService target; public OrderServiceStaticProxy(OrderService target) { this.target = target; } @Override public void createOrder(String orderId, double amount) { long start = System.currentTimeMillis(); System.out.println("调用 createOrder,参数:" + orderId + ", " + amount); target.createOrder(orderId, amount); System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms"); } @Override public String queryOrder(String orderId) { long start = System.currentTimeMillis(); System.out.println("调用 queryOrder,参数:" + orderId); String result = target.queryOrder(orderId); System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms"); return result; } }这样写的好处是:业务类的代码干净了,公共逻辑收拢到了代理类里。但代价也很明显——你每新增一个接口,就要手写一个代理类;每新增一个方法,就要在代理类里补一个方法。接口一改,代理类也得跟着改。如果你的项目里有几十个接口,你就要维护几十个代理类,这本质上还是重复劳动。
1.3 痛点总结:JDK到底帮我们解决了什么
静态代理的核心矛盾在于:代理逻辑是通用的,但代理类却要针对每个接口单独编写。我们真正想要的是:写一次公共逻辑,然后让JVM在运行时帮我们自动生成代理类,自动把每个方法的调用转发到公共逻辑上。
这就是动态代理的意义。JDK动态代理通过反射机制,在运行时动态创建代理类,你只需要写一个InvocationHandler,告诉JVM"所有方法的调用都走这个处理器的invoke方法",剩下的代理类生成、方法匹配全部由JVM完成。你不再需要为每个接口手写代理类,这就是"动态"二字的含义。
2. JDK动态代理实战:三行代码跑通核心流程
理解了这个背景,我们再来看JDK动态代理的代码就完全不慌了。它总共只需要两个核心角色:InvocationHandler(调用处理器)和Proxy(代理类工厂)。
2.1 完整代码实现与逐行解读
我们先写调用处理器:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class LogInvocationHandler implements InvocationHandler { // 被代理的目标对象 private final Object target; public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start = System.currentTimeMillis(); System.out.println("调用方法:" + method.getName() + ",参数:" + java.util.Arrays.toString(args)); // 反射调用目标对象的真实方法 Object result = method.invoke(target, args); System.out.println("方法执行完成,耗时:" + (System.currentTimeMillis() - start) + "ms"); return result; } }三个参数要理解透彻:
- proxy:生成的代理对象本身。你可以在invoke里调用proxy的方法,但99%的情况下不要这么做,因为调用proxy的任意方法都会再次进入invoke,形成无限递归。
- method:当前被调用方法的反射对象,通过它可以拿到方法名、参数类型、注解,最重要的是通过method.invoke(target, args)调用目标对象的真实方法。
- args:调用方法时传入的参数列表。
然后通过Proxy创建代理对象:
import java.lang.reflect.Proxy; public class Main { public static void main(String[] args) { // 目标对象 OrderService target = new OrderServiceImpl(); // 创建代理对象 OrderService proxy = (OrderService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInvocationHandler(target) ); // 调用代理方法 proxy.createOrder("A001", 199.0); String result = proxy.queryOrder("A001"); System.out.println("查询结果:" + result); } }运行后输出:
调用方法:createOrder,参数:[A001, 199.0] 创建订单成功 方法执行完成,耗时:0ms 调用方法:queryOrder,参数:[A001] 查询订单成功 方法执行完成,耗时:0ms 查询结果:order info日志逻辑只写了一次,但是所有接口方法都自动有了日志,这就是动态代理最直观的威力。
2.2 Proxy.newProxyInstance三个参数到底在干什么
第一个参数是类加载器(ClassLoader)。代理类是在运行时动态生成的,JVM需要用一个类加载器把它加载进来。这里有一个非常容易踩的坑:有人会写成proxy.getClass().getClassLoader()或者随便用一个自定义类加载器,结果在某些环境下抛ClassCastException。最稳妥的写法是用被代理类的类加载器,也就是target.getClass().getClassLoader(),因为目标类和代理类实现了同一个接口,用目标类的类加载器能确保类型兼容。有些老项目里有自定义类加载器的场景(比如Tomcat的WebAppClassLoader),这时候更要保持和目标类一致的类加载器。
第二个参数是接口数组(Class[])。JDK动态代理要求被代理对象必须实现至少一个接口,代理类本质上是实现了这些接口的类,所以调用方可以安全地把代理对象强转为接口类型。
第三个参数是InvocationHandler。它负责定义"代理逻辑",JVM生成的代理类里所有方法调用,最终都会转发到handler.invoke()上。
2.3 一个关键问题:代理对象能不能强转为实现类
很多人会问:如果目标类除了接口方法,还有自己独有的方法,代理对象能调到吗?答案是不能。JVM生成的代理类只会实现你在第二个参数里传入的接口,它和目标实现类没有任何继承关系。所以下面这种写法一定会抛ClassCastException:
OrderServiceImpl implProxy = (OrderServiceImpl) Proxy.newProxyInstance(...); // 报错这条规则后面在讲CGLIB的时候会形成鲜明对比——CGLIB恰恰就是为了解决"没有接口怎么办"而诞生的,它生成的代理类是目标类的子类,所以能强转为目标类本身。
3. 从JDK代理到CGLIB:没有接口也能代理
JDK动态代理很优雅,但它有一个硬约束:目标对象必须实现接口。实际开发中,总有类不实现接口(比如很多工具类、第三方SDK里的类),还有Spring里的大多数Bean。这时候CGLIB就登场了。
3.1 CGLIB的核心思路:通过继承实现代理
CGLIB(Code Generation Library)的原理非常直观:它通过字节码技术动态生成目标类的子类,子类重写父类的非final方法,在重写方法里插入代理逻辑。
需要引入依赖。如果用的是Spring Boot项目,spring-core里已经内置了CGLIB(repackage过的),直接能用。如果是纯Java项目,在pom里加:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>然后定义一个不需要接口的业务类:
public class UserService { public void addUser(String username) { System.out.println("添加用户:" + username); } public final String getUserInfo(String userId) { return "user-" + userId; } }注意getUserInfo方法我特意加了final,后面会解释为什么。
接着写CGLIB的方法拦截器:
import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class LogMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { long start = System.currentTimeMillis(); System.out.println("CGLIB调用方法:" + method.getName() + ",参数:" + java.util.Arrays.toString(args)); // 注意这里是 proxy.invokeSuper,不是 method.invoke Object result = proxy.invokeSuper(obj, args); System.out.println("CGLIB方法执行完成,耗时:" + (System.currentTimeMillis() - start) + "ms"); return result; } }创建代理对象:
import net.sf.cglib.proxy.Enhancer; public class CglibMain { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new LogMethodInterceptor()); UserService proxy = (UserService) enhancer.create(); proxy.addUser("张三"); System.out.println(proxy.getUserInfo("1001")); } }运行输出:
CGLIB调用方法:addUser,参数:[张三] 添加用户:张三 CGLIB方法执行完成,耗时:0ms user-1001注意输出里没有getUserInfo的日志——因为它是final方法,CGLIB无法重写,所以代理逻辑直接失效。这是CGLIB一个非常重要的边界。
3.2 JDK代理与CGLIB的选择对比
我把两者的核心差异整理成一张表,面试和实际选型都很有用:
| 对比维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 代理方式 | 基于接口,运行时生成实现类 | 基于继承,生成目标类的子类 |
| 目标限制 | 必须实现至少一个接口 | 不能被final修饰,被代理的方法不能是final |
| 生成速度 | 较快(类结构简单) | 较慢(需要生成子类字节码) |
| 调用速度 | 反射调用,较慢 | 通过FastClass机制直接调用,较快 |
| JDK版本 | 是JDK原生能力,无需额外依赖 | 需要引入CGLIB或Spring内置 |
| 适用场景 | 面向接口编程的Spring默认 | 无接口的类、Spring中配置为强制CGLIB时 |
Spring里有一个非常经典的默认策略:如果Bean实现了接口,优先用JDK动态代理;如果没有实现接口,用CGLIB。但在Spring Boot 2.x以后,默认强制使用CGLIB代理,主要是为了避免JDK代理只能代理接口方法导致的AOP失效问题(比如@Transactional加在实现类上,如果只代理接口,事务就失效了)。
3.3 为什么CGLIB调用速度更快
JDK动态代理通过method.invoke(target, args)反射调用,这个调用过程涉及方法查找、访问权限检查、参数装箱拆箱等操作,性能开销较大。而CGLIB引入了FastClass机制——它为目标类和代理类额外生成了索引类,通过方法索引直接跳转调用,省去了反射查找的过程,所以调用速度明显更快。这也是很多高性能框架(如Spring、MyBatis)内部混合使用两种代理策略的原因:接口丰富的用JDK代理,类结构简单的用CGLIB。
4. 动态代理的底层机制:JVM到底生成了什么
不少人在这一节就蒙了:"动态代理到底是怎么动态的?代理类是谁生成的?长什么样?"我们直接深入字节码层面,看看JVM到底做了什么。
4.1 保存并反编译代理类
JDK动态代理在运行时会用ProxyGenerator生成一个名为$Proxy0的类。默认情况下这个类是存在JVM内存里的,普通开发看不到。我们可以在启动参数里加上:
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true或者在代码里设置:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");这样代理类会以$Proxy0.class的形式输出到当前目录的com/sun/proxy/文件夹里(取决于JDK版本)。用IDEA打开,或者用javap反编译,能看到类似这样的结构:
public final class $Proxy0 extends Proxy implements OrderService { private static Method m1; private static Method m2; private static Method m3; private static Method m4; public $Proxy0(InvocationHandler h) { super(h); } public final String queryOrder(String var1) { try { return (String) super.h.invoke(this, m3, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } public final void createOrder(String var1, double var2) { try { super.h.invoke(this, m4, new Object[]{var1, var2}); } catch (RuntimeException | Error var4) { throw var4; } catch (Throwable var5) { throw new UndeclaredThrowableException(var5); } } }这段字节码信息量很大。代理类继承了java.lang.reflect.Proxy,同时实现了OrderService接口。每个接口方法对应一个静态的Method对象(m1、m2...),这些Method对象是在代理类静态初始化时通过反射取到的。而方法调用时,代理类把this、对应的Method、参数列表打包,然后调用super.h.invoke(...),也就是我们传入的InvocationHandler.invoke方法。
4.2 为什么JDK动态代理只能代理接口
从上面的字节码就能看出来了:Java是单继承的,而代理类已经继承了Proxy类,它就不可能再去继承任何其他类。所以JDK动态代理只能通过"实现接口"来实现代理。这一点困扰过很多初学者——你没法让一个和Proxy毫无关系的类直接被动态代理,除非它有个接口。
而CGLIB走的是另一条路:不继承Proxy,而是直接继承目标类。子类对父类方法进行重写,调用invokeSuper时走的是父类真实方法。所以CGLIB没有单继承的束缚,但它受限于final:final类无法继承,final方法无法重写,这两个位置就是CGLIB的盲区,也是最常见的坑。
4.3 Spring AOP源码里的代理创建流程
理解了原理,再看Spring AOP就很容易了。Spring的AbstractAutoProxyCreator在Bean初始化后,会调用wrapIfNecessary判断这个Bean是否匹配切点表达式。如果匹配,就通过ProxyFactory创建代理对象:
- 如果目标对象实现了接口,且proxyTargetClass=false,默认用JdkDynamicAopProxy,也就是JDK动态代理。
- 否则用ObjenesisCglibAopProxy,基于CGLIB,通过Objenesis绕过构造器实例化代理类。
所以你在Spring项目里,如果一个被@Transactional标记的类没有实现接口,Spring会生成一个它的CGLIB子类来代理事务。如果这个方法恰好被final修饰,或者类是final,那CGLIB没法重写,事务注解就静默失效了——排错的时候相当折磨人。
5. 动态代理在主流框架里的真实应用
动态代理不是面试造火箭,它就在你身边的框架里天天跑。我从目前主流的几个框架里挑几个代表性场景讲一下。
5.1 Spring AOP与声明式事务
Spring AOP的整个核心就是动态代理。@Transactional之所以能让一个普通方法获得事务能力,是因为Spring容器启动时,看到这个Bean上有事务注解,就会为它生成代理对象。代理对象在调用目标方法之前,先开启事务;目标方法执行成功后提交事务;抛出RuntimeException则回滚事务。
声明式事务失效的场景很多都和代理机制有关:同类内部调用(this调用)绕过代理对象、方法不是public、方法是final、类没有实现接口但强制JDK代理等。搞懂了动态代理,这些坑就全都恍然大悟了。
5.2 MyBatis的Mapper代理
MyBatis的Mapper接口本身没有实现类,但你可以直接注入并调用它的方法。这是因为SqlSession.getMapper()内部用了MapperProxy,它实现了InvocationHandler,在invoke方法里根据方法名和参数从MapperMethod中解析SQL语句,然后执行并返回结果。每个Mapper接口的实例其实就是一个JDK动态代理对象。
可以说,我们平时用MyBatis写的那一行"userMapper.selectById(1)",真实执行链路是经过了MapperProxy处理器转发的。
5.3 日志、权限、缓存等通用横切能力
动态代理最典型的应用场景还包括:
- 统一日志:给所有Service方法加上入参、出参、耗时的记录,而不侵入业务代码。
- 权限校验:在代理的invoke方法里,读取当前登录用户,判断是否有权限调用目标方法,无权限直接抛异常或返回默认值。
- 缓存:代理方法执行前先去查询缓存,命中直接返回;未命中再调用真实方法,并把结果写入缓存。
- 事务管理:对方法进行事务开启、提交、回滚的统一管理。
- RPC框架:像Feign、Retrofit、Dubbo的接口代理,用户只需要声明接口,框架在运行时生成代理对象,把所有调用发送到远程服务。
5.4 自己动手做一个简单AOP框架
理解了动态代理的所有零件后,我们可以花十分钟搓一个极简AOP框架。思路很简单:定义一个@Log注解标记需要日志的方法,然后写一个代理工厂,通过反射找到方法上的注解,有注解才在invoke里输出日志。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Log { } public class LogProxyFactory { @SuppressWarnings("unchecked") public static <T> T createProxy(T target) { return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> { if (method.isAnnotationPresent(Log.class)) { long start = System.currentTimeMillis(); System.out.println("[AOP] 方法 " + method.getName() + " 开始执行"); Object result = method.invoke(target, args); System.out.println("[AOP] 方法 " + method.getName() + " 执行完毕,耗时 " + (System.currentTimeMillis() - start) + "ms"); return result; } return method.invoke(target, args); } ); } }用起来就像这样:
OrderService proxy = LogProxyFactory.createProxy(new OrderServiceImpl()); proxy.createOrder("A002", 299.0);这个极简AOP虽然没有Spring那么完备,但核心机制已经全部打通了:通过注解标记切点,通过动态代理织入增强逻辑。
6. 高频面试考点与实战避坑
动态代理是Java面试的高频考点。我整理了最常见的几个问题和实战中最容易踩的坑。
6.1 面试题:问烂了的那几个问题
JDK动态代理和CGLIB动态代理有什么区别?参考上面那张表格,重点答"JDK基于接口,CGLIB基于继承;CGLIB不能代理final方法和final类;Spring默认策略"。
动态代理的实现原理是什么?JDK动态代理通过ProxyGenerator在运行时生成$Proxy0类,实现目标接口,所有方法调用转发到InvocationHandler.invoke(),再由invoke通过反射调用目标真实方法。CGLIB通过字节码技术生成目标类的子类,子类重写方法,通过MethodProxy.invokeSuper调用父类方法。
JDK动态代理为什么只能代理接口?因为代理类已经继承了Proxy类,Java单继承机制不允许它再继承其它类,只能借助接口。
Spring AOP用的是JDK代理还是CGLIB?Spring Boot默认强制CGLIB,纯Spring默认是"有接口用JDK,无接口用CGLIB"。从Spring Boot 2.x开始默认使用CGLIB,因为JDK代理对实现类上的注解支持不好。
动态代理的性能问题怎么看?创建代理对象的过程比静态代理开销大(需要生成字节码、加载类),但方法调用层面,CGLIB有FastClass索引优化,JDK在较新版本也有优化。对绝大多数业务系统来说,性能差距可以忽略。需要做极致性能的场景,才考虑把代理对象缓存复用,避免重复创建。
6.2 实战避坑:invoke里调用proxy的方法会导致StackOverflowError
这是我见过最多人踩的第一个坑。在LogInvocationHandler里写:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(proxy.toString()); // 死循环 return method.invoke(target, args); }因为proxy是代理对象,调用它的toString()方法,会再次进入invoke方法,又调用proxy.toString(),无限递归最终StackOverflowError。解决办法:在invoke里永远不要调用proxy的任何方法,需要啥信息就从method和args里取。
6.3 实战避坑:method.invoke(target, args)中target传错对象
invoke方法里反射调用目标方法时,第一个参数必须是目标对象。如果传成proxy,因为proxy不是目标类型的实例,轻则ClassCastException,重则又进入递归。我见过有人把proxy传给method.invoke的,调试了半天才反应过来。
6.4 实战避坑:CGLIB代理final方法的静默失效
在Spring里给一个final方法加@Transactional,项目启动不会报任何错误,但方法执行完根本不会有事务。这就是CGLIB无法重写final方法导致的静默失效,排查起来极其痛苦。建议团队规范里明确:被Spring AOP管理的方法不要加final修饰。
另外CGLIB代理因为要生成子类,所以目标类不能被final修饰。Spring Boot 2.x默认使用CGLIB代理,如果你的某个配置类被final修饰,启动时会直接报错。
6.5 实战避坑:代理对象的类型判断
JDK代理对象isInstance(target的类型)是false,因为它和目标类没有继承关系。比如:
System.out.println(proxy instanceof OrderServiceImpl); // false System.out.println(proxy instanceof OrderService); // true这在做有些框架设计的时候很关键,别想当然用instanceof判断代理对象。
6.6 性能优化:复用代理对象与Class缓存
每次Proxy.newProxyInstance都会重新生成代理类并加载,如果业务里频繁创建代理对象,性能消耗会很可观。最典型的场景是MyBatis,如果每次查询都new一个Mapper代理,系统性能会直线下降。所以许多框架内部会缓存代理类甚至代理对象。自己在做工具类的时候也建议这样:
public class ProxyCache { private static final ConcurrentHashMap<Class<?>, Object> CACHE = new ConcurrentHashMap<>(); @SuppressWarnings("unchecked") public static <T> T getProxy(Class<T> interfaceType, InvocationHandler handler) { return (T) CACHE.computeIfAbsent(interfaceType, type -> Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler)); } }这样同一个接口只生成一次代理类,后续直接复用代理对象。
7. 学习建议:动态代理之后该往哪个方向走
动态代理学到这个程度,已经不是"学过"而是"掌握"了。按照我的经验,学完动态代理之后有几条路值得继续走,它们的知识会互相印证,越学越通透。
7.1 强化AOP思想
动态代理是AOP(面向切面编程)的底层实现手段之一,但AOP不止于动态代理。Spring AOP中还有切点(Pointcut)、通知(Advice)、切面(Aspect)的概念,这些是建立在动态代理之上的抽象。建议在理解了动态代理之后,手动去Spring官网上跑一遍@Before、@After、@Around的Demo,再尝试用动态代理复现一个简单的事务管理。这一步做完,你对Spring的认知会有一个质的飞跃。
7.2 深挖字节码增强
CGLIB只是字节码增强的代表之一,还有更底层的ASM、Java assist、以及新一代的Byte Buddy。如果你对JVM和字节码技术感兴趣,可以读一读ASM的文档,尝试用ASM生成一个最简单类的字节码。这会让你对"类是怎么被加载的""字节码长什么样"有非常直观的理解,对后面学习JVM调优、Java Agent都很有帮助。
7.3 阅读框架源码
我的建议是先从MyBatis的MapperProxy入手,因为它的类结构最简洁,逻辑最清晰。然后看Spring的JdkDynamicAopProxy、CglibAopProxy、AbstractAutoProxyCreator。你会发现,框架内部不是把动态代理当作一个"面试考点"在讲,而是把它当作基础设施在反复使用。
7.4 自己写一个小框架
这个是我最推荐的收尾动作。你可以尝试写一个类似Spring AOP的轻量级框架:支持注解定义切面、支持多个通知顺序、支持JDK代理和CGLIB代理自动切换。当你把它跑通的时候,动态代理就不只是你简历上的一行字,而是长在你脑子里的肌肉记忆了。