ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java动态代理两种方式及其优缺点详解

Java动态代理两种方式及其优缺点详解 摘要动态代理是 Java 生态中最核心的底层技术之一Spring AOP、MyBatis Mapper、RPC 框架、服务治理等都建立在它之上。Java 中实现动态代理主要有两种方式基于接口的 JDK 动态代理以及基于继承的 CGLIB 动态代理。本文从代理模式出发系统深入地剖析 JDK 动态代理与 CGLIB 的工作原理、底层源码机制、类结构差异、性能表现、适用场景及其各自的优缺点并给出完整的代码示例、运行分析、反编译演示、框架应用案例和面试高频考点力求帮助读者从原理到实战全面掌握动态代理技术。1. 为什么需要动态代理在理解 JDK 动态代理和 CGLIB 之前必须首先回答一个根本性问题为什么需要动态代理这个问题看似简单却直接决定了后续技术选型的逻辑和深度。如果不能理解动态代理要解决的痛点后续的源码分析和性能对比也就失去了意义。在传统软件开发中我们经常面临这样的需求在不修改原始类代码的前提下为某个类的某些方法增加额外逻辑比如日志记录、性能统计、事务管理、权限校验、缓存处理、异常统一处理等。这类逻辑横切在多个业务方法之间如果直接写在业务代码里会导致大量重复代码同时使业务逻辑与横切关注点严重耦合。一旦日志格式、事务策略或权限规则发生变化开发人员就不得不在大量业务类中逐一修改极易遗漏和出错。1.1 静态代理的基本思路最直观的解决方案是静态代理。所谓静态代理就是开发人员手工编写一个代理类让代理类实现与目标类相同的接口或者继承目标类然后在代理类中调用目标对象的方法并在调用前后加入额外逻辑。下面是一个典型示例javapublic interface UserService { void save(String name); User findById(Long id); } public class UserServiceImpl implements UserService { Override public void save(String name) { System.out.println(保存用户 name); } Override public User findById(Long id) { System.out.println(查询用户 id); return new User(id, 张三); } } public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target target; } Override public void save(String name) { System.out.println(开始记录日志); try { target.save(name); System.out.println(保存成功); } catch (Exception e) { System.out.println(保存失败 e.getMessage()); throw e; } finally { System.out.println(结束记录日志); } } Override public User findById(Long id) { System.out.println(开始记录日志); try { User user target.findById(id); System.out.println(查询成功); return user; } catch (Exception e) { System.out.println(查询失败 e.getMessage()); throw e; } finally { System.out.println(结束记录日志); } } }1.2 静态代理的局限与动态代理的价值静态代理的优点是直观、容易理解、便于调试。但它的局限性也极其明显代码重复严重当一个接口有几十个方法时代理类中的日志、事务等逻辑会在每个方法中重复出现代理类代码会非常冗长。维护成本高如果系统中有几百个类都需要加日志就要手工编写几百个代理类工作量大且极易出错。可扩展性差当接口新增方法时目标类和代理类都要同步修改。任何遗漏都可能导致代理失效或运行时错误。代理逻辑分散相同的横切逻辑分散在不同代理类中难以统一管理和升级。动态代理正是为了解决这些问题而诞生。它允许开发人员在运行时动态生成代理类无需手工编写代理类源码。代理类由虚拟机在运行期自动创建并加载同一套代理逻辑可以应用到任意数量的类上从而极大减少重复代码使横切关注点的管理变得集中、灵活、可复用。Java 生态中动态代理的实现方式主要有两种JDK 动态代理从 JDK 1.3 开始引入位于java.lang.reflect包中要求目标类必须实现接口代理类与目标类实现相同接口以接口为契约进行方法拦截。CGLIB 动态代理CGLIBCode Generation Library是强大的字节码生成库通过继承目标类并在子类中拦截方法调用来实现代理不要求目标类实现接口。两者各有优劣。真正理解它们的原理、差异和适用场景是深入掌握 Spring、MyBatis、Dubbo 等框架的基础。下面将按“理论准备 → JDK 原理 → CGLIB 原理 → 对比分析 → 框架应用 → 面试解析”的顺序逐步展开。2. 代理模式的理论基础动态代理是代理模式的一种高级实现形态。要真正理解动态代理必须先把代理模式本身弄清楚因为动态代理只是把代理模式中“手工编写代理类”这一环节自动化了。2.1 代理模式的定义代理模式Proxy Pattern是 GoF 设计模式中的一种结构型模式。其定义是为其他对象提供一种代理以控制对这个对象的访问。通俗地说代理对象就像目标对象的替身客户端通过代理对象间接访问目标对象代理对象可以在访问前后做额外处理也可以决定是否真正调用目标对象。代理模式的核心角色有三个抽象主题Subject定义目标对象和代理对象共同遵循的接口客户端面向这个抽象主题编程。真实主题RealSubject实现抽象主题接口承载真正的业务逻辑也就是被代理的目标对象。代理主题Proxy同样实现抽象主题接口内部持有真实主题引用在调用真实主题方法前后增加额外逻辑。代理模式的类图关系可以表达为客户端依赖 Subject 接口RealSubject 与 Proxy 都实现 Subject 接口Proxy 内部聚合 RealSubject 并在调用前后插入控制逻辑。2.2 代理模式的常见类型根据代理用途不同代理模式通常分为以下几类远程代理为位于不同地址空间的对象提供本地代表封装网络通信细节让调用者感觉像在调用本地对象。RMI、各类 RPC 框架是典型代表。虚拟代理延迟加载开销较大的对象在真正需要使用时才创建或初始化。例如大型图片、复杂报表的懒加载。保护代理控制对目标对象的访问权限根据调用者身份决定是否放行。例如权限校验组件。缓存代理在代理中缓存目标方法返回结果避免重复执行昂贵计算或重复查询数据库。智能引用代理在访问对象时附加额外动作如引用计数、日志记录、资源释放等。这些不同类型本质上都是通过在代理对象中包装目标对象并在调用链路上加入自定义逻辑实现的。动态代理的意义在于它将“代理类的生成”从编译期推迟到运行期使开发人员不必为每一种代理场景手写代理类。2.3 静态代理与动态代理的本质区别静态代理的处理逻辑在编译前就已确定开发人员明确知道要代理哪个类、要添加什么逻辑。动态代理则不同它在运行期才能确定目标对象和代理逻辑带来了巨大灵活性但代价是引入反射或字节码操作带来一定性能开销和实现复杂度。从实现层面看两者的根本区别在于静态代理类由程序员编写动态代理类由框架或 JDK 在运行期生成。正是这种生成时机与生成方式的差异决定了两种代理方式的适用场景和优缺点。后面的分析将围绕“生成什么、如何生成、调用如何分发、有什么限制”四个问题逐层展开。3. JDK 动态代理深入剖析3.1 JDK 动态代理的定位与核心思想JDK 动态代理是 Java 标准库提供的动态代理实现从 JDK 1.3 版本引入位于java.lang.reflect包中核心类包括Proxy和InvocationHandler。它的最大特点是基于接口进行代理被代理的目标类必须实现至少一个接口。JDK 动态代理在运行期动态生成一个代理类的字节码这个代理类实现目标对象所实现的所有接口。代理类中的每个接口方法在被调用时都会把方法调用分发给统一的InvocationHandler处理器由该处理器统一执行附加逻辑再通过反射调用目标对象的对应方法。其设计思想是把所有接口方法的调用汇聚到InvocationHandler.invoke一个入口中从而实现横切逻辑的集中管理。开发人员只需实现一次InvocationHandler即可代理任意实现了接口的目标对象。3.2 核心 API 介绍JDK 动态代理主要涉及三个要素java.lang.reflect.Proxy代理类工厂类提供newProxyInstance静态方法创建代理实例。java.lang.reflect.InvocationHandler调用处理器接口只有一个invoke方法代理对象的所有方法调用都会被转发到这里。目标接口被代理对象实现的接口代理类会实现这些接口。Proxy.newProxyInstance方法签名如下javapublic static Object newProxyInstance(ClassLoader loader, Class?[] interfaces, InvocationHandler h) throws IllegalArgumentException其中loader是类加载器用于加载动态生成的代理类interfaces是要代理的接口数组h是调用处理器所有方法调用都会进入它的invoke方法。3.3 一个完整的 JDK 动态代理示例下面给出完整示例演示如何用 JDK 动态代理为UserService添加日志与耗时统计。第一步定义目标接口和实现类javapublic interface UserService { void save(String name); User findById(Long id); } public class UserServiceImpl implements UserService { Override public void save(String name) { System.out.println(业务逻辑保存用户 name); } Override public User findById(Long id) { System.out.println(业务逻辑查询用户 id); return new User(id, 张三); } }第二步编写调用处理器javaimport 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 { System.out.println(【日志】开始调用方法 method.getName()); long start System.currentTimeMillis(); try { Object result method.invoke(target, args); return result; } finally { long cost System.currentTimeMillis() - start; System.out.println(【日志】方法 method.getName() 执行完成耗时 cost ms); } } }第三步创建代理对象并使用javaimport java.lang.reflect.Proxy; public class JdkProxyDemo { public static void main(String[] args) { UserService target new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInvocationHandler(target) ); proxy.save(李四); User user proxy.findById(1L); System.out.println(查询结果 user.getName()); } }运行后可以看到业务方法执行前后都自动加入了日志和耗时统计而UserServiceImpl本身没有任何修改。3.4 JDK 动态代理的底层工作流程当调用Proxy.newProxyInstance时内部大致经历以下流程参数校验检查interfaces是否合法包括数组是否为空、接口是否可被类加载器访问、是否存在重复接口等。查找或生成代理类先检查代理类缓存看是否为“同一接口组合与类加载器”生成过代理类。缓存命中则复用未命中则通过ProxyGenerator生成代理类字节码。加载代理类使用传入的ClassLoader将字节码加载到 JVM 中得到代理类的Class对象。创建代理实例反射获取代理类中以InvocationHandler为参数的构造方法并调用创建代理对象实例。生成的代理类结构大致如下下面给出简化示意代码javapublic final class $Proxy0 extends Proxy implements UserService { private static Method m0; // hashCode private static Method m1; // equals private static Method m2; // toString private static Method m3; // save private static Method m4; // findById public $Proxy0(InvocationHandler handler) { super(handler); } Override public final void save(String name) { try { super.h.invoke(this, m3, new Object[]{name}); } catch (Throwable e) { throw new RuntimeException(e); } } Override public final User findById(Long id) { try { return (User) super.h.invoke(this, m4, new Object[]{id}); } catch (Throwable e) { throw new RuntimeException(e); } } }从示意代码可见$Proxy0继承了Proxy类并实现了UserService接口。每个接口方法都打包Method对象和参数统一交给InvocationHandler.invoke处理。由于 Java 是单继承代理类已经继承了Proxy因此 JDK 动态代理只能代理接口无法代理普通类这是它的根本性限制。3.5 InvocationHandler 的 invoke 方法详解InvocationHandler接口只有一个方法javaObject invoke(Object proxy, Method method, Object[] args) throws Throwable;三个参数的含义如下proxy当前代理对象。特别需要注意的是如果在invoke内部再次调用proxy上的方法会触发无限递归因为每次调用都会再次进入invoke。真正的目标对象需要开发人员自己保存并调用。method当前被调用方法对应的Method对象可获取方法名、参数类型、返回类型、注解等信息。args调用方法的实际参数数组无参时为null。在invoke中可自由编排逻辑调用前做权限检查、参数校验调用后处理返回值、记录日志过程中捕获异常统一处理甚至可以完全拦截而不调用目标方法。对于Object的hashCode、equals、toString代理类也会转发到invoke编写时通常对它们直接放行。3.6 查看 JDK 生成的代理类字节码理解 JDK 动态代理的直观方法是把生成的代理类字节码导出并反编译。可以通过系统属性sun.misc.ProxyGenerator.saveGeneratedFiles实现javapublic class SaveProxyClassDemo { public static void main(String[] args) { System.setProperty(sun.misc.ProxyGenerator.saveGeneratedFiles, true); UserService target new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInvocationHandler(target) ); proxy.save(测试); } }在 JDK 8 中该属性有效生成的 class 文件保存在当前工作目录对应包路径下。反编译后可以直观看到代理类的继承结构、方法实现、静态 Method 字段初始化逻辑。JDK 9 之后由于模块化限制该属性可能失效可改用-Djdk.proxy.ProxyGenerator.saveGeneratedFilestrue或使用 Arthas、HSDB 等工具查看。3.7 JDK 动态代理的优缺点优点JDK 原生支持属于 Java 标准库无需引入第三方依赖稳定性和兼容性有官方保障。接口契约清晰代理类与目标类实现相同接口客户端可完全面向接口编程符合依赖倒置原则。生成效率高代理类基于接口生成生成逻辑相对简单类结构不复杂。适合面向接口编程的框架Spring AOP 默认对接口代理使用 JDK 动态代理MyBatis Mapper 接口也依赖它。缺点只能代理接口目标类必须实现接口无法代理没有接口的普通类这是最根本的限制。反射调用开销方法调用通过Method.invoke完成虽然 JDK 持续优化但在极端高频场景下仍有一定开销。调试相对复杂代理类动态生成运行时类名形如$Proxy0出现问题时排查难度略高。对 Object 方法处理需谨慎hashCode、equals、toString也会进入invoke需要妥善处理否则可能引发意外行为。4. CGLIB 动态代理深入剖析4.1 CGLIB 的定位与核心思想CGLIBCode Generation Library是一个强大的、高性能的字节码生成库它通过 ASM 框架在运行期动态生成目标类的子类并在子类中覆盖目标类的方法从而在方法调用前后插入增强逻辑。与 JDK 动态代理不同CGLIB 不要求目标类实现接口因此可以代理普通类。CGLIB 的核心思想是继承代理类继承目标类成为目标类的子类然后重写父类中需要被代理的方法。在重写方法中先调用方法拦截器MethodInterceptor.intercept再由拦截器决定是否调用父类方法、如何增强。这样客户端调用代理类的方法时实际进入的是子类的重写方法从而被拦截器捕获。4.2 CGLIB 的核心 APICGLIB 主要涉及以下几个核心接口和类EnhancerCGLIB 的核心类用于生成代理类。通过设置父类、回调等参数调用create()方法生成代理实例。MethodInterceptor方法拦截器接口只有一个intercept方法。所有被代理的方法调用都会进入这个方法开发人员在这里编写增强逻辑。MethodProxy方法代理对象提供invokeSuper方法用于调用父类即目标类的原始方法。它是 CGLIB 高性能的关键避免了反射调用。Callback回调接口的顶层接口MethodInterceptor是它的子接口。CGLIB 还提供了FixedValue、NoOp、LazyLoader等多种回调类型。MethodInterceptor接口定义如下javapublic interface MethodInterceptor extends Callback { Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable; }四个参数的含义obj代理对象本身即 CGLIB 生成的子类实例。method被调用的方法对应的Method对象。args调用方法的实际参数数组。proxy方法代理对象用于调用父类的原始方法。4.3 一个完整的 CGLIB 动态代理示例第一步引入 CGLIB 依赖。在 Maven 项目中添加xmldependency groupIdcglib/groupId artifactIdcglib/artifactId version3.3.0/version /dependency如果使用 Spring 框架Spring 已经内置了重新打包的 CGLIB无需单独引入。第二步定义目标类无需实现接口javapublic class UserService { public void save(String name) { System.out.println(业务逻辑保存用户 name); } public User findById(Long id) { System.out.println(业务逻辑查询用户 id); return new User(id, 张三); } }第三步编写方法拦截器javaimport net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class LogMethodInterceptor implements MethodInterceptor { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println(【日志】开始调用方法 method.getName()); long start System.currentTimeMillis(); try { Object result proxy.invokeSuper(obj, args); return result; } finally { long cost System.currentTimeMillis() - start; System.out.println(【日志】方法 method.getName() 执行完成耗时 cost ms); } } }第四步使用 Enhancer 创建代理对象javaimport net.sf.cglib.proxy.Enhancer; public class CglibProxyDemo { public static void main(String[] args) { Enhancer enhancer new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new LogMethodInterceptor()); UserService proxy (UserService) enhancer.create(); proxy.save(李四); User user proxy.findById(1L); System.out.println(查询结果 user.getName()); } }运行后UserService的方法调用被拦截器捕获日志和耗时统计自动加入而UserService本身没有任何修改。4.4 CGLIB 的底层工作流程CGLIB 生成代理类的过程大致如下创建 Enhancer 并配置设置父类目标类、回调拦截器、是否实现接口等参数。生成代理类字节码CGLIB 使用 ASM 框架动态生成目标类的子类字节码。子类会重写父类中所有非 final、非 private、非 static 的方法。加载代理类通过自定义的 ClassLoader 把生成的字节码加载到 JVM 中得到代理类的 Class 对象。创建代理实例反射调用代理类构造方法传入 Callback 数组创建代理对象。方法调用拦截当调用代理对象的某个方法时实际进入子类重写的方法。该方法会调用MethodInterceptor.intercept由拦截器决定增强逻辑和是否调用父类方法。生成的代理类结构大致如下简化示意javapublic class UserService$$EnhancerByCGLIB$$xxxx extends UserService implements Factory { private MethodInterceptor interceptor; Override public void save(String name) { MethodProxy proxy MethodProxy.create(UserService.class, UserService$$EnhancerByCGLIB$$xxxx.class, (Ljava/lang/String;)V, save, CGLIB$save$0); interceptor.intercept(this, method, new Object[]{name}, proxy); } Override public User findById(Long id) { MethodProxy proxy MethodProxy.create(UserService.class, UserService$$EnhancerByCGLIB$$xxxx.class, (Ljava/lang/Long;)Lcom/example/User;, findById, CGLIB$findById$1); return (User) interceptor.intercept(this, method, new Object[]{id}, proxy); } final void CGLIB$save$0(String name) { super.save(name); } final User CGLIB$findById$1(Long id) { return super.findById(id); } }可以看到CGLIB 生成的代理类继承自目标类重写了需要被代理的方法。重写方法中调用interceptor.intercept并把MethodProxy传入。拦截器通过proxy.invokeSuper(obj, args)调用父类原始方法从而完成增强。4.5 CGLIB 的限制与注意事项CGLIB 虽然强大但也有若干限制和注意事项无法代理 final 类因为 CGLIB 通过继承实现代理如果目标类被final修饰就无法生成子类代理失败。无法代理 final 方法被final修饰的方法不能被子类重写因此无法被拦截。无法代理 private 方法private 方法不参与继承子类无法访问也无法拦截。无法代理 static 方法static 方法属于类本身不属于对象不能通过继承拦截。构造方法不被代理CGLIB 生成的代理类会调用父类构造方法但构造方法本身不被拦截。因此如果目标类只有带参构造方法且没有默认构造需要显式指定构造参数。性能考量CGLIB 生成代理类的速度比 JDK 动态代理慢但一旦生成完成后续方法调用性能优于 JDK 反射调用。因此 CGLIB 适合「代理类生成不频繁、方法调用频繁」的场景。4.6 CGLIB 的优缺点优点不要求接口可以代理没有实现任何接口的普通类覆盖面更广。调用性能好通过MethodProxy.invokeSuper直接调用父类方法避免反射调用性能接近直接调用。与 Spring AOP 深度集成Spring AOP 对无接口的目标类默认使用 CGLIB 代理。支持多种回调除MethodInterceptor外还支持FixedValue、NoOp、LazyLoader等回调适配多种场景。缺点无法代理 final 类和 final 方法这是继承机制带来的根本限制。生成代理类较慢需要生成字节码并加载首次创建代理类开销高于 JDK 动态代理。可能影响类层次代理类是目标类的子类可能影响getClass()、instanceof等行为。需要额外依赖在非 Spring 环境中需要显式引入 CGLIB 依赖。调试难度增加代理类名形如UserService$$EnhancerByCGLIB$$xxxx堆栈信息较复杂。5. JDK 动态代理与 CGLIB 的对比5.1 核心差异对比维度JDK 动态代理CGLIB 动态代理代理方式基于接口实现基于继承实现目标类要求必须实现接口无需接口但不能是 final 类代理类结构实现目标接口继承 Proxy继承目标类方法调用反射调用Method.invokeMethodProxy.invokeSuper生成速度较快较慢调用性能反射调用JDK 8 优化后接近直接调用接近直接调用通常略优于反射限制只能代理接口不能代理 final 类、final 方法、private 方法、static 方法依赖JDK 原生需引入 CGLIB 或使用 Spring 内置版本调试难度代理类名$Proxy0较易识别代理类名较长堆栈略复杂5.2 性能对比从性能维度看两者各有侧重代理类生成速度JDK 动态代理通常快于 CGLIB因为基于接口生成逻辑相对简单。方法调用速度CGLIB 通过MethodProxy直接调用父类方法避免反射开销在高频调用场景下通常优于 JDK 反射调用。但在 JDK 8 之后Method.invoke经过大量优化两者差距已明显缩小。内存占用CGLIB 生成的代理类通常比 JDK 代理类更大因为需要继承目标类并生成方法代理。实际选型时性能不应作为唯一标准。更重要的是看目标类是否实现接口、是否有 final 限制、框架默认行为等因素。5.3 选型建议目标类实现了接口且接口稳定优先使用 JDK 动态代理原生支持、无额外依赖。目标类没有实现接口使用 CGLIB 动态代理。目标类被 final 修饰两者都无法代理需要考虑其他方案如装饰器模式、AspectJ 编译期织入。目标方法被 final 修饰JDK 动态代理不受影响接口方法CGLIB 无法拦截。Spring 项目Spring AOP 默认规则是「有接口用 JDK无接口用 CGLIB」可通过proxyTargetClass强制使用 CGLIB。Spring Boot 2.x 及以后默认proxyTargetClasstrue即默认使用 CGLIB 代理。6. 动态代理在主流框架中的应用6.1 Spring AOPSpring AOP 是动态代理最经典的应用场景。它通过AopProxy接口封装代理创建逻辑根据目标类是否实现接口选择 JDK 动态代理或 CGLIB。JdkDynamicAopProxy基于 JDK 动态代理实现InvocationHandler在invoke中织入通知逻辑。CglibAopProxy基于 CGLIB实现MethodInterceptor在intercept中织入通知逻辑。Spring AOP 的代理创建流程大致如下解析切面配置收集所有 Advisor。通过AnnotationAwareAspectJAutoProxyCreator在 Bean 初始化后判断是否需要代理。如果需要代理根据目标类是否实现接口选择代理方式。创建代理对象替换原始 Bean。方法调用时代理对象拦截并执行通知链。6.2 MyBatis MapperMyBatis 的 Mapper 接口没有实现类但可以通过 JDK 动态代理在运行期生成实现。MapperProxyFactory负责创建 Mapper 代理MapperProxy实现InvocationHandler在invoke中根据方法签名找到对应的MappedStatement执行 SQL 并返回结果。javapublic class MapperProxyT implements InvocationHandler, Serializable { Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } MapperMethod mapperMethod cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } }6.3 RPC 框架Dubbo、gRPC 等 RPC 框架通过动态代理为远程服务生成本地代理。调用方注入接口后实际拿到的是代理对象调用代理方法时代理会封装网络请求、序列化参数、发送到远端、接收响应并反序列化让调用方感觉像在调用本地方法。Dubbo 默认使用 Javassist 字节码生成代理但也支持 JDK 动态代理。Spring Cloud 中的 Feign 则基于 JDK 动态代理为声明式接口生成 HTTP 客户端。6.4 其他应用事务管理SpringTransactional通过 AOP 代理织入事务逻辑。缓存Spring Cache 通过代理拦截方法调用命中缓存时直接返回未命中时执行方法并写入缓存。权限校验通过代理在方法调用前校验权限。日志与监控通过代理记录方法调用日志、耗时、异常等信息。Mock 框架Mockito 等测试框架通过动态代理生成 Mock 对象。7. 面试高频考点解析7.1 为什么 JDK 动态代理只能代理接口JDK 动态代理生成的代理类已经继承了java.lang.reflect.Proxy。由于 Java 是单继承语言一个类只能有一个父类因此代理类无法再继承目标类只能通过实现目标类所实现的接口来完成类型匹配。这是 JDK 动态代理只能代理接口的根本原因。7.2 CGLIB 为什么不能代理 final 类和方法CGLIB 通过生成目标类的子类来实现代理。如果目标类被final修饰则无法被继承自然无法生成子类。如果方法被final修饰则无法被子类重写也就无法被拦截。同理private 方法无法被子类访问static 方法属于类本身都无法被 CGLIB 代理。7.3 Spring AOP 如何选择代理方式Spring AOP 的默认规则是如果目标类实现了至少一个接口默认使用 JDK 动态代理。如果目标类没有实现任何接口使用 CGLIB 动态代理。可以通过EnableAspectJAutoProxy(proxyTargetClass true)或配置spring.aop.proxy-target-classtrue强制使用 CGLIB。Spring Boot 2.x 开始默认proxyTargetClasstrue即默认使用 CGLIB。7.4 JDK 动态代理和 CGLIB 的性能差异需要分两个维度看代理类生成速度JDK 动态代理通常更快因为基于接口生成逻辑简单。方法调用速度CGLIB 通过MethodProxy直接调用父类方法通常快于 JDK 的反射调用。但 JDK 8 之后Method.invoke经过优化差距已不明显。实际场景中代理类通常只生成一次方法调用可能发生成千上万次因此调用性能更值得关注。但在现代 JVM 上两者差距通常不足以成为选型的决定性因素。7.5 为什么在 InvocationHandler 中调用 proxy 的方法会导致死循环InvocationHandler.invoke中的proxy参数是代理对象本身。如果在invoke中调用proxy.someMethod()这个调用会再次进入invoke形成无限递归。正确做法是保存目标对象引用通过method.invoke(target, args)调用目标方法而不是调用代理对象的方法。7.6 如何查看 JDK 动态生成的代理类可以通过以下方式JDK 8设置系统属性sun.misc.ProxyGenerator.saveGeneratedFilestrue。JDK 9设置jdk.proxy.ProxyGenerator.saveGeneratedFilestrue或使用--add-opens参数。使用 Arthas 的jad命令反编译代理类。使用 HSDB 或 JProfiler 等工具在运行时查看。7.7 CGLIB 的 MethodProxy 和 Method 有什么区别Method是 JDK 反射 API调用method.invoke(target, args)需要传入目标对象通过反射执行。MethodProxy是 CGLIB 提供的代理对象调用proxy.invokeSuper(obj, args)会直接调用父类的原始方法避免反射开销。CGLIB 在生成代理类时会为每个被代理方法生成一个对应的CGLIB$method$index方法MethodProxy内部通过 FastClass 机制定位并调用这个方法从而实现高性能调用。8. 总结动态代理是 Java 生态中极其重要的底层技术Spring AOP、MyBatis Mapper、RPC 框架等都建立在其之上。Java 提供了两种主要的动态代理实现JDK 动态代理基于接口JDK 原生支持无需额外依赖代理类实现目标接口并继承Proxy方法调用统一分发到InvocationHandler.invoke。限制是目标类必须实现接口。CGLIB 动态代理基于继承通过 ASM 生成目标类的子类并重写方法方法调用进入MethodInterceptor.intercept。不要求接口但无法代理 final 类、final 方法、private 方法和 static 方法。两者在生成速度、调用性能、限制条件、依赖成本上各有优劣。实际选型时应根据目标类是否实现接口、是否有 final 限制、框架默认行为等因素综合判断。在 Spring 生态中Spring AOP 已经封装了两种代理方式并根据目标类自动选择开发者通常无需直接操作底层 API。理解动态代理的原理不仅能帮助我们在面试中从容应对更能让我们在排查 Spring 事务失效、AOP 不生效、MyBatis Mapper 注入失败等问题时快速定位根因。掌握这一技术是从「会用框架」走向「理解框架」的重要一步。
返回列表