← 返回题目列表

JDK 动态代理的原理是什么?

高频 中等 第 11 / 25 题 更新于 2026/07/28
代理模式JDK动态代理InvocationHandler反射

简化版

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 里,框架通常会做拆包处理。

第三,equalshashCodetoString 这类方法也可能进入 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) 调真实对象;它省掉了手写代理类,但限制是必须有接口。