Java代理模式深度解析:从静态代理到JDK/CGLIB动态代理实战 1. 从“中间人”到“代码魔术”为什么我们需要代理模式在Java开发中尤其是当你需要为一个已有的类添加一些额外功能比如日志记录、性能监控、事务管理或者权限校验但又不想、也不能直接修改这个类的源代码时你会怎么做直接修改源码违反了“开闭原则”也让代码变得臃肿且难以维护。这时候一个经典的设计模式——代理模式Proxy Pattern就派上了用场。你可以把代理想象成一个“中间人”或者“秘书”。比如你想约见一位大忙人目标对象直接联系他可能很困难或者他需要你提前准备好所有材料。这时他的秘书代理对象就出现了。你联系秘书秘书会帮你预约时间、检查你的材料是否齐全甚至帮你处理一些前置或后置的事务比如记录会面时间、通知相关部门最后再帮你安排与目标对象的会面。在整个过程中你感觉是在和目标对象直接交互但实际上是代理对象在为你打理一切。在Java世界里这个“秘书”就是代理对象它持有对真实对象目标对象的引用并在调用真实对象的方法前后插入我们需要的“增强”逻辑。根据这个“秘书”是提前雇佣好的编译期确定还是根据情况临时生成的运行期动态创建我们将代理分为静态代理和动态代理。理解它们的区别和实现不仅是应对面试八股文的必备技能更是写出高扩展性、低耦合度代码的实用工具。接下来我们就深入这个“中间人”的世界看看静态代理如何按部就班动态代理又如何施展“代码魔术”。2. 静态代理手写“中间人”的清晰与局限静态代理顾名思义代理关系在编译期就已经确定下来了。我们需要手动创建一个代理类这个代理类实现了和目标对象相同的接口或者继承自同一个父类并在内部持有一个目标对象的实例。所有对目标方法的调用都会经过这个代理类的方法进行转发我们就在这个转发过程中加入自己的逻辑。2.1 静态代理的标准实现步骤让我们通过一个最经典的场景来理解为服务层的业务方法添加日志记录。首先定义业务接口。这是代理模式和目标对象都需要遵守的“契约”。// 1. 定义业务接口 public interface UserService { void addUser(String username); void deleteUser(String id); }接着创建目标对象即接口的真实实现类。// 2. 目标对象真实实现 public class UserServiceImpl implements UserService { Override public void addUser(String username) { System.out.println(添加用户: username); // 模拟业务逻辑... } Override public void deleteUser(String id) { System.out.println(删除用户ID: id); // 模拟业务逻辑... } }现在关键角色登场——静态代理类。它也必须实现UserService接口。// 3. 静态代理类 public class UserServiceStaticProxy implements UserService { // 持有目标对象的引用 private UserService target; // 通过构造器注入目标对象 public UserServiceStaticProxy(UserService target) { this.target target; } Override public void addUser(String username) { // **前置增强**在调用真实方法前执行 System.out.println([静态代理] 开始记录日志 - 准备执行 addUser, 参数: username); long startTime System.currentTimeMillis(); // 调用目标对象的方法 target.addUser(username); // **后置增强**在调用真实方法后执行 long endTime System.currentTimeMillis(); System.out.println([静态代理] 方法执行完毕 - 执行 addUser 耗时: (endTime - startTime) ms); } Override public void deleteUser(String id) { System.out.println([静态代理] 开始记录日志 - 准备执行 deleteUser, 参数: id); long startTime System.currentTimeMillis(); target.deleteUser(id); long endTime System.currentTimeMillis(); System.out.println([静态代理] 方法执行完毕 - 执行 deleteUser 耗时: (endTime - startTime) ms); } }最后在客户端调用方中我们不再直接使用UserServiceImpl而是使用它的代理类。// 4. 客户端使用 public class Client { public static void main(String[] args) { // 创建目标对象 UserService realService new UserServiceImpl(); // 创建代理对象并将目标对象传入 UserService proxy new UserServiceStaticProxy(realService); // 通过代理对象调用方法 proxy.addUser(张三); System.out.println(----------); proxy.deleteUser(001); } }运行上面的Client类你会看到类似下面的输出清晰地展示了代理在方法执行前后插入的日志和耗时统计[静态代理] 开始记录日志 - 准备执行 addUser, 参数: 张三 添加用户: 张三 [静态代理] 方法执行完毕 - 执行 addUser 耗时: 0ms ---------- [静态代理] 开始记录日志 - 准备执行 deleteUser, 参数: 001 删除用户ID: 001 [静态代理] 方法执行完毕 - 执行 deleteUser 耗时: 0ms2.2 静态代理的优缺点与适用场景静态代理的优点非常明显结构清晰易于理解和上手。代理类和目标类的关系一目了然符合面向对象的设计思想。对于小型项目或者需要增强的方法数量很少且固定的情况静态代理是一个不错的选择。然而它的缺点也同样突出这直接导致了其在复杂项目中的局限性冗余与僵化每一个需要被代理的类我们都需要手动编写一个对应的代理类。如果UserService接口有10个方法即使你只想代理其中的2个代理类也必须实现全部10个方法并在不想增强的方法里简单调用目标对象的方法。这会产生大量的模板代码。紧耦合代理类与目标类/接口紧密耦合。一旦接口UserService发生变更比如增加或修改了一个方法那么所有实现了该接口的代理类都必须同步修改维护成本高。不灵活无法在运行时动态地为一个新出现的类创建代理。代理关系在编码时就已经写死了。正因为这些局限当我们需要为大量类或方法提供类似增强如日志、事务时静态代理就显得力不从心。这时我们就需要更强大的武器——动态代理。3. JDK动态代理基于接口的运行时“造物”动态代理的核心思想是在程序运行时动态地在内存中创建代理类及其对象。我们无需为每个目标类手动编写代理类代码。在Java中实现动态代理主要有两种方式JDK原生动态代理和第三方库如CGLIB。我们先看JDK自带的方案。JDK动态代理的核心是java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。它的一个关键限制是它只能为接口创建代理无法代理没有实现任何接口的普通类。这是因为它生成的代理类默认继承了Proxy类Java是单继承所以只能通过实现接口的方式来实现代理。3.1 JDK动态代理的实现机制我们来改造上面的例子用JDK动态代理实现同样的日志增强功能。首先业务接口UserService和目标类UserServiceImpl保持不变。然后我们需要创建一个InvocationHandler的实现类。这个类是增强逻辑的集中地。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; } /** * 代理对象任何方法被调用时都会触发此方法。 * param proxy 代理对象本身通常很少直接使用 * param method 被调用的方法对象反射对象 * param args 调用方法时传入的参数 * return 方法的返回值 * throws Throwable */ Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 前置增强 System.out.println([JDK动态代理] 开始记录日志 - 准备执行 method.getName() , 参数: (args ! null ? String.join(, , (CharSequence[]) args) : 无)); long startTime System.currentTimeMillis(); // 利用反射调用目标对象的方法 Object result method.invoke(target, args); // 后置增强 long endTime System.currentTimeMillis(); System.out.println([JDK动态代理] 方法执行完毕 - 执行 method.getName() 耗时: (endTime - startTime) ms); // 返回目标方法执行的结果 return result; } }最后在客户端我们使用Proxy.newProxyInstance方法来动态创建代理对象。import java.lang.reflect.Proxy; public class JdkDynamicProxyClient { public static void main(String[] args) { // 1. 创建目标对象 UserService realService new UserServiceImpl(); // 2. 创建调用处理器并传入目标对象 InvocationHandler handler new LogInvocationHandler(realService); // 3. 动态创建代理对象 // 参数1类加载器通常用目标类的类加载器 // 参数2代理类需要实现的接口数组 // 参数3调用处理器实例 UserService proxy (UserService) Proxy.newProxyInstance( realService.getClass().getClassLoader(), // 类加载器 realService.getClass().getInterfaces(), // 目标对象实现的接口 handler // 自定义的InvocationHandler ); // 4. 通过代理对象调用方法 System.out.println(代理对象的类型: proxy.getClass().getName()); proxy.addUser(李四); System.out.println(----------); proxy.deleteUser(002); } }运行这段代码输出与静态代理类似但你会注意到代理对象的类名非常特殊类似com.sun.proxy.$Proxy0这就是JVM在运行时为我们动态生成的代理类。3.2 深入理解Proxy.newProxyInstance这个方法是我们接触JDK动态代理的入口理解它的三个参数至关重要ClassLoader loader用于加载动态生成的代理类的类加载器。通常传入目标对象的类加载器target.getClass().getClassLoader()确保代理类和目标类在同一个类加载器环境下能访问到相同的接口。Class?[] interfaces代理类需要实现的接口列表。因为JDK动态代理是基于接口的所以必须指定。通常传入目标对象实现的所有接口target.getClass().getInterfaces()。InvocationHandler h最重要的部分即我们自定义的调用处理器。代理对象所有方法的调用都会被路由到它的invoke方法中。在这里我们通过反射调用原始方法并环绕它添加增强逻辑。一个重要的实操心得在InvocationHandler的invoke方法内部如果你调用method.invoke(proxy, args)即用代理对象自己作为目标会导致无限递归调用最终栈溢出。一定要调用method.invoke(target, args)target才是我们传入的真实对象。3.3 JDK动态代理的优缺点分析优点解耦与通用代理类与目标类完全解耦。一个InvocationHandler可以用于代理多个不同的接口和类只要增强逻辑相同。这避免了静态代理的大量重复编码。灵活可以在运行时动态地为不同的接口创建代理非常灵活。符合设计原则很好地遵循了“对扩展开放对修改关闭”的开闭原则。缺点只能代理接口这是其最根本的限制。如果你的业务类没有实现任何接口JDK动态代理就无能为力了。反射调用性能开销通过Method.invoke进行反射调用其性能比直接方法调用要差。但在绝大多数应用场景下这点开销是可以接受的尤其是相比于它带来的灵活性优势。内部细节黑盒动态生成的代理类对我们来说是黑盒调试时可能不太直观。4. CGLIB动态代理突破接口限制的“继承”魔法为了解决JDK动态代理必须基于接口的限制我们就需要用到第三方库最著名的就是CGLIBCode Generation Library。CGLIB通过继承目标类的方式创建子类并在子类中重写父类的方法来实现代理。因此它甚至可以代理没有实现任何接口的普通类。4.1 CGLIB动态代理的实现步骤首先需要引入CGLIB的依赖。如果你使用Maven可以在pom.xml中添加dependency groupIdcglib/groupId artifactIdcglib/artifactId version3.3.0/version !-- 请使用最新稳定版本 -- /dependency这次我们创建一个没有实现接口的普通类作为目标。// 目标类没有实现任何接口 public class ProductService { public void saveProduct(String name) { System.out.println(保存产品: name); } public final void finalMethod() { System.out.println(这是一个final方法无法被代理增强); } }注意我们特意加了一个final方法。CGLIB通过生成子类来代理而final方法是不能被重写的所以它无法被代理增强。接下来创建一个方法拦截器它类似于JDK动态代理中的InvocationHandler。import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class LogMethodInterceptor implements MethodInterceptor { /** * 拦截目标类所有非final方法的调用 * param obj 代理对象CGLIB生成的子类对象 * param method 被拦截的方法目标类的方法 * param args 方法参数 * param proxy 用于调用父类即目标类原始方法的代理对象 * return 方法的返回值 * throws Throwable */ Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 前置增强 System.out.println([CGLIB动态代理] 开始记录日志 - 准备执行 method.getName() , 参数: (args ! null ? String.join(, , (CharSequence[]) args) : 无)); long startTime System.currentTimeMillis(); // 关键调用父类目标类的原始方法。 // 注意这里用的是 MethodProxy.invokeSuper而不是 method.invoke。 Object result proxy.invokeSuper(obj, args); // 后置增强 long endTime System.currentTimeMillis(); System.out.println([CGLIB动态代理] 方法执行完毕 - 执行 method.getName() 耗时: (endTime - startTime) ms); return result; } }最后使用CGLIB的Enhancer类来创建代理对象。import net.sf.cglib.proxy.Enhancer; public class CglibProxyClient { public static void main(String[] args) { // 1. 创建Enhancer对象相当于JDK动态代理的Proxy类 Enhancer enhancer new Enhancer(); // 2. 设置父类即目标类 enhancer.setSuperclass(ProductService.class); // 3. 设置回调对象即我们的方法拦截器 enhancer.setCallback(new LogMethodInterceptor()); // 4. 创建代理对象 ProductService proxy (ProductService) enhancer.create(); // 5. 通过代理对象调用方法 System.out.println(代理对象的类型: proxy.getClass().getName()); System.out.println(代理对象的父类: proxy.getClass().getSuperclass().getName()); proxy.saveProduct(笔记本电脑); System.out.println(----------); // 尝试调用final方法 proxy.finalMethod(); // 这个方法不会被拦截增强直接执行目标类的逻辑 } }运行这段代码你会看到saveProduct方法被成功增强而finalMethod则直接执行输出中不会有代理添加的日志。代理对象的类名类似ProductService$$EnhancerByCGLIB$$...表明它是ProductService的子类。4.2 CGLIB的核心MethodProxy与性能优化在MethodInterceptor.intercept方法中我们使用MethodProxy.invokeSuper(obj, args)来调用原始方法。这里的MethodProxy是CGLIB的核心优化之一。与JDK动态代理的Method.invoke(target, args)相比MethodProxy.invokeSuper避免了反射调用。它通过生成两个FastClass快速类来建立方法索引直接进行方法调用其性能在大量调用时通常优于JDK动态代理的反射机制。这是CGLIB除了能代理类之外的另一大优势。4.3 CGLIB动态代理的优缺点与坑点优点可以代理普通类无需目标类实现接口应用范围更广。性能通常更好通过FastClass机制方法调用效率高于反射。功能强大除了方法拦截还支持回调过滤、固定值返回等高级特性。缺点与注意事项无法代理final类或final方法因为继承是它的基础final类无法被继承final方法无法被重写。需要引入第三方库增加了项目的依赖。构造方法会被调用两次由于CGLIB通过继承实现在创建代理对象时会先调用父类目标类的构造器再调用子类代理类的构造器。如果目标类的构造器中有副作用代码如计数器需要注意这个问题。目标类必须有默认构造器因为CGLIB生成子类时需要调用父类的无参构造器。如果目标类只有带参数的构造器创建代理时会失败。5. 实战中的抉择JDK代理 vs CGLIB代理 vs 静态代理了解了三种代理的实现后在实际项目中该如何选择呢这里有一个清晰的决策路径和对比表格。决策路径如果目标对象有实现接口优先考虑JDK动态代理。它是Java标准库的一部分无需额外依赖且符合面向接口编程的思想。如果目标对象没有实现接口只能选择CGLIB动态代理。如果增强逻辑极其简单且代理的类和方法非常固定、数量极少可以考虑使用静态代理代码最直观。在Spring AOP等框架中早期版本默认对实现接口的类使用JDK动态代理对未实现接口的类使用CGLIB。Spring Boot 2.x 之后默认统一使用CGLIB因为它能应对更多场景如代理非public方法。你可以在配置中通过spring.aop.proxy-target-classtrue/false来切换。特性对比表格特性静态代理JDK动态代理CGLIB动态代理实现方式手动编写代理类运行时动态生成代理类实现接口运行时动态生成代理类继承目标类目标要求需实现相同接口或继承相同父类目标必须实现至少一个接口目标类不能是final方法不能是final性能最好直接调用较差基于反射调用较好基于FastClass直接调用灵活性差每代理一个类需写一个代理类高一个处理器可代理多个接口高一个拦截器可代理多个类依赖无JDK自带需要引入CGLIB库适用场景简单、固定的代理需求基于接口的代理Spring AOP旧版默认代理普通类Spring AOP新版默认一个重要的实操心得关于“代理对象”的类型当你使用JDK动态代理时proxy instanceof UserService为true但proxy instanceof UserServiceImpl为false。因为代理对象实现的是接口而不是目标类。而CGLIB代理对象proxy instanceof ProductService为true因为它是目标类的子类。这个细微差别在某些依赖类型检查的框架或代码中可能会产生影响。6. 动态代理的底层窥探与高级应用理解了怎么用我们不妨再深入一步看看动态代理背后发生了什么以及它的一些高级玩法。6.1 动态生成的代理类长什么样我们可以通过设置系统属性将运行时动态生成的代理类字节码保存到磁盘然后反编译查看。这对于调试和理解原理非常有帮助。对于JDK动态代理在JVM启动参数或代码开头添加System.getProperties().put(sun.misc.ProxyGenerator.saveGeneratedFiles, true);运行程序后会在项目根目录的com/sun/proxy下找到$Proxy0.class等文件。用反编译工具如JD-GUI打开你会看到它继承了Proxy类并实现了我们指定的接口每个方法内部都调用了InvocationHandler.invoke。对于CGLIB代理在创建Enhancer时设置System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, ./cglib_proxy_classes); Enhancer enhancer new Enhancer(); // ... 其他设置运行后会在指定目录生成代理类的.class文件。反编译后可以看到它继承了目标类并重写了非final方法在重写的方法中调用了MethodInterceptor.intercept。6.2 高级应用选择性代理与方法过滤我们并不总是想代理目标类的所有方法。例如对于Object类的toString(),hashCode()等方法我们可能不想增强。这时就需要在调用处理器或拦截器中进行方法过滤。以JDK动态代理为例修改LogInvocationHandlerOverride public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName method.getName(); // 过滤掉Object类的基础方法不进行增强 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(target, args); } // 也可以根据方法名过滤例如只增强以add或delete开头的方法 if (!methodName.startsWith(add) !methodName.startsWith(delete)) { return method.invoke(target, args); } // 以下是增强逻辑... System.out.println([选择性代理] 开始增强方法: methodName); long startTime System.currentTimeMillis(); Object result method.invoke(target, args); long endTime System.currentTimeMillis(); System.out.println([选择性代理] 方法执行耗时: (endTime - startTime) ms); return result; }在CGLIB中可以使用CallbackFilter来实现更精细的方法回调控制为不同的方法指定不同的MethodInterceptor甚至直接调用父类方法不增强。6.3 在主流框架中的应用动态代理是许多Java框架的基石Spring AOP (Aspect-Oriented Programming)其核心实现就是基于动态代理。Transactional,Cacheable,Async等注解的生效背后都是Spring为你创建了代理对象在方法调用前后插入了事务管理、缓存处理、异步执行等逻辑。MyBatisMapper接口并没有实现类但我们可以直接调用其方法执行SQL。这正是因为MyBatis在启动时为每个Mapper接口动态生成了代理对象将方法调用转化为对SqlSession的调用。RPC框架如Dubbo客户端持有的服务接口实例实际上是一个代理对象。当你调用接口方法时代理对象会将方法名、参数等信息序列化通过网络发送到服务端拿到结果后再返回给你。理解动态代理是理解这些框架如何“无侵入”地增强我们代码的关键。下次当你使用Transactional注解时可以想想Spring是如何通过一个“看不见”的代理对象帮你管理数据库连接和事务提交回滚的。这种设计极大地提升了代码的模块化和可维护性。