真实面经题目 · 原创解析
Java 动态代理的实现原理是什么?
Java 动态代理本质上是在运行期生成一个实现目标接口的代理类,把接口方法调用统一转发给 InvocationHandler。面试回答不能只说“反射”或“运行期生成对象”,还要讲清 JDK 动态代理的接口约束、Proxy 生成代理类、方法调用链、Spring AOP 的代理选择,以及 final、private、自调用等失效边界。
真实面经题目 · 原创解析
Java 动态代理本质上是在运行期生成一个实现目标接口的代理类,把接口方法调用统一转发给 InvocationHandler。面试回答不能只说“反射”或“运行期生成对象”,还要讲清 JDK 动态代理的接口约束、Proxy 生成代理类、方法调用链、Spring AOP 的代理选择,以及 final、private、自调用等失效边界。
Java 动态代理常见指 JDK 动态代理。它要求目标对象至少实现一个接口,运行时通过 Proxy.newProxyInstance 传入类加载器、接口列表和 InvocationHandler,JDK 会生成一个实现这些接口的代理类。调用方调用接口方法时,实际进入代理类生成的方法实现,再统一转发到 InvocationHandler.invoke。invoke 能拿到代理对象、Method 和参数,因此可以在真实方法调用前后加入事务、日志、权限、监控、缓存、重试等横切逻辑,再决定是否通过反射调用目标对象。Spring AOP 就是典型应用:有接口时通常可用 JDK 动态代理,没有接口或强制类代理时使用 CGLIB 生成子类代理。二者都服务于 AOP,但 JDK 代理基于接口,CGLIB 基于继承,所以 JDK 不能直接代理没有接口的普通类,CGLIB 不能增强 final 类、final 方法和 private 方法。还要注意,代理增强只有调用经过代理对象才会生效,同类内部 this 调用通常会绕过代理,这是事务注解失效的高频原因。
JDK 动态代理不是编译期手写一个代理类,而是在运行期根据一组接口动态生成代理类。这个代理类实现目标对象的接口,因此调用方可以继续面向接口编程。真正的业务对象不会被调用方直接访问,调用方拿到的是代理对象,代理对象再把方法调用统一交给 InvocationHandler。这里要强调它代理的是接口方法调用,不是任意拦截普通对象的所有方法。
典型入口是 Proxy.newProxyInstance,三个关键参数分别是类加载器、接口数组和 InvocationHandler。类加载器决定代理类被加载到哪个命名空间,接口数组决定代理对象对外暴露哪些方法,InvocationHandler 决定方法调用时执行什么逻辑。JDK 内部会基于接口签名生成一个代理类,这个类通常继承 Proxy 并实现传入接口,再用传入的 InvocationHandler 构造出代理对象。
一次方法调用的链路是:调用方持有接口引用并调用方法,实际进入代理类生成的方法实现;代理类把本次调用封装成 Method 和参数数组,转交给 InvocationHandler.invoke;invoke 中执行前置增强,再调用目标对象对应方法,随后执行后置增强;如果目标方法抛出异常,还可以执行异常增强、事务回滚或异常转换。这个链路比一句“用了反射”更能说明原理。
InvocationHandler 是动态代理的调度中心。invoke 方法能接收 proxy、method、args 三类信息:proxy 是当前代理对象,method 表示被调用的接口方法,args 是参数。开发者可以在这里做参数校验、鉴权、事务开启、日志审计、指标采集、结果包装,也可以选择不调用真实对象而直接返回结果。要注意不要在 invoke 内随意调用 proxy 的同名方法,否则容易触发递归调用。
JDK 动态代理最大的限制是必须有接口,因为生成的代理类是实现接口,不是继承目标实现类。代理对象只能暴露接口中声明的方法,目标类独有但接口没有的方法不会成为代理对象的公开能力。实际项目里如果按具体实现类注入,而容器生成的是 JDK 代理,就可能出现类型不匹配。更稳妥的做法是面向接口注入,或者明确使用基于类的代理。
CGLIB 的思路是生成目标类的子类,通过覆写方法拦截调用,因此不要求目标类实现接口。它适合代理普通类,但依赖继承和方法覆写,所以 final 类无法继承,final 方法和 private 方法也无法被正常增强。JDK 代理对象与目标类不是父子关系,只是共同符合接口;CGLIB 代理对象则是目标类的子类型。这个差异会影响类型判断、方法可见性和代理选择。
Spring AOP 使用动态代理把横切关注点从业务代码中剥离出来,典型场景包括声明式事务、权限校验、日志审计、性能监控、缓存、重试、异常统一处理和数据源切换。调用方看似调用业务 Bean,实际可能先进入代理对象,代理对象根据切点和增强逻辑执行拦截链,再调用目标方法。事务注解能生效,正是因为外部调用经过了代理对象。
动态代理只能拦截经过代理对象的调用,因此同一个类内部 this.method 的自调用通常不会触发代理增强,这是声明式事务、缓存和权限拦截失效的常见原因。JDK 代理还受接口边界限制,CGLIB 受 final、private、构造过程和继承结构限制。工程上还要关注代理对象类型、异常包装、equals 与 hashCode 语义、切点匹配范围和调用入口是否真的经过代理。
因为 JDK 生成的代理类是实现传入接口的类,调用方通过接口类型持有代理对象。它不继承目标实现类,也不能暴露接口外的方法。没有接口时,JDK 动态代理缺少可实现的公共契约,通常需要改用 CGLIB 这类基于类继承的代理方式。
proxy 是当前代理对象,method 表示本次被调用的接口方法,args 是调用参数。实际处理中,invoke 通常围绕 method.invoke 目标对象组织前置、后置、异常和最终增强。需要注意不要在 invoke 里直接调用 proxy 的同名方法,否则可能递归。
JDK 动态代理是基于接口的组合式代理,代理对象实现接口并持有 InvocationHandler;CGLIB 是基于继承的子类代理,通过覆写可覆写方法来拦截调用。前者要求接口,后者不要求接口但受 final、private 和继承限制影响。
常见原因是调用没有经过代理对象。例如同一个类内部用 this 调用另一个带事务注解的方法,实际调用发生在目标对象内部,代理对象没有机会拦截,自然不会开启事务。也可能是方法可见性、final 限制、异常类型或 Bean 未被容器管理导致。
静态代理需要为每个接口或业务类手写代理类,方法多时重复代码很多。动态代理把通用增强逻辑集中到 InvocationHandler 或拦截器链中,在运行期为不同接口生成代理对象,适合框架统一处理事务、日志、权限等横切逻辑。