JDK 动态代理的原理是什么?
简化版
JDK 动态代理是在运行期为接口生成代理类,所有方法调用都会进入 InvocationHandler.invoke(),再由它通过反射调用目标对象并添加增强逻辑。它的核心限制是只能代理接口,不能直接代理没有接口的普通类。
详细版
JDK 动态代理常用写法:
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(Object proxyObj, Method method, Object[] args) -> {
System.out.println("方法调用前");
Object result = method.invoke(target, args);
System.out.println("方法调用后");
return result;
}
);
proxy.createUser("Tom");
关键点:
Proxy.newProxyInstance()运行期创建代理对象;- 代理对象实现目标对象的接口;
- 调用代理方法时,不会直接进目标对象,而是进入
InvocationHandler.invoke(); method.invoke(target, args)通过反射调用真实对象;- 可以在反射调用前后加入日志、事务、权限、监控等增强。
JDK 动态代理的优点是不用为每个类手写代理类,适合统一增强多个接口实现。缺点是必须基于接口,反射调用也有一定开销,但大多数业务场景不是主要瓶颈。
完整版教学
一、JDK 动态代理解决了静态代理的什么问题
静态代理要手写代理类:
class UserServiceProxy implements UserService { ... }
class OrderServiceProxy implements OrderService { ... }
class ProductServiceProxy implements ProductService { ... }
如果每个代理类只是做类似的日志、事务、监控,重复度非常高。
JDK 动态代理的目标就是:不要手写代理类,让 JDK 在运行期帮我们生成。
开发者只需要写一份统一的调用处理逻辑:
InvocationHandler handler = (proxy, method, args) -> {
before();
Object result = method.invoke(target, args);
after();
return result;
};
这份逻辑可以应用到很多接口实现上。
二、JDK 动态代理的调用流程
调用流程可以拆成几步:
客户端调用代理对象方法
↓
代理对象把调用转发给 InvocationHandler.invoke()
↓
invoke() 里执行前置增强
↓
method.invoke(target, args) 调用真实对象
↓
执行后置增强并返回结果
所以代理对象本身不真正写业务逻辑,它只是把所有方法调用统一交给 InvocationHandler。
InvocationHandler 是动态代理的核心入口。
三、为什么 JDK 动态代理要求接口
JDK 动态代理生成的代理类会实现指定接口。
例如目标对象实现了:
interface UserService {
void createUser(String name);
}
生成的代理对象大致可以理解为:
class $Proxy0 implements UserService {
private InvocationHandler handler;
public void createUser(String name) {
handler.invoke(this, method, new Object[]{name});
}
}
真实生成的源码不是这样简单,但理解上可以这么看。
因为代理类是“实现接口”,所以目标类必须至少有接口。如果目标类没有接口,JDK 动态代理就无法直接代理它,这时常用 CGLIB。
四、method.invoke(target, args) 的注意点
method.invoke(target, args) 是通过反射调用真实对象方法。
这里有几个细节:
第一,调用目标必须是 target,不是 proxy。如果写成:
method.invoke(proxy, args);
可能导致递归调用代理对象,最后栈溢出。
第二,异常可能被反射包装。被调用方法抛出的异常可能包在 InvocationTargetException 里,框架通常会做拆包处理。
第三,equals、hashCode、toString 这类方法也可能进入 invoke(),需要按场景处理。
五、JDK 动态代理和 Spring AOP 的关系
Spring AOP 在很多情况下会使用 JDK 动态代理。通常规则是:如果目标对象有接口,Spring 可以使用 JDK 动态代理;如果没有接口,可能使用 CGLIB 生成子类代理。
这也是为什么面试里经常把代理模式、JDK 动态代理、CGLIB、Spring AOP 放在一起问。
不过要注意:Spring AOP 不等于 JDK 动态代理。JDK 动态代理只是 Spring AOP 可选的底层实现之一。
六、生成类、接口与调用处理器的协作
一个代理实现 3 个接口、共暴露 12 个方法时,仍可由同一个 InvocationHandler 统一拦截,而不用手写 12 个转发方法。
proxy.method -> generated method -> InvocationHandler.invoke -> Method.invoke(target)
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | InvocationHandler 收到的 proxy 参数是代理自身,错误地再次调用同一代理方法可能造成递归。 |
| 适用边界 | JDK 代理面向接口;若调用方依赖实现类类型,强制转换会失败,也暴露了抽象边界不稳。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:InvocationHandler 收到的 proxy 参数是代理自身,错误地再次调用同一代理方法可能造成递归。
八、常见误区与追问
- 误区:JDK 动态代理会直接修改目标类字节码。 它通常生成实现目标接口的新代理类,目标类本身不被改写。
- 误区:代理类存在,就代表所有调用都会经过代理。 只有客户端持有并调用代理引用时增强才生效;绕过代理直接调用目标对象,调用链自然不会出现增强。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:Object 的 equals、hashCode、toString 会经过 invoke 吗? 代理调用这些方法通常也会进入 InvocationHandler,处理器要明确它们的语义。
- 追问:代理模式和装饰器模式能只靠类图区分吗? 不能;两者都可能包装同一接口,必须结合“控制访问”还是“叠加职责”的设计意图区分。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
JDK 动态代理的核心是接口 + InvocationHandler。代理对象实现接口,方法调用统一进入 invoke(),再由 method.invoke(target, args) 调真实对象;它省掉了手写代理类,但限制是必须有接口。