实战指南)
1. 从“远程桌面服务即将停止”说起为什么我们需要代理前几天一个朋友在群里发了一张截图上面赫然写着“远程桌面授权模式尚未配置远程桌面服务将在 118 天后停止工作。” 他问我这是怎么回事。我一看就乐了这其实是一个典型的“代理”问题——只不过这里的“代理”是 Windows 系统里负责处理远程桌面连接授权的“RD 连接代理”服务。这个服务就像一个“中介”负责验证和转发用户的连接请求到正确的远程桌面主机。如果这个“中介”代理配置不当或者失效了整个远程桌面服务就会出问题。这让我想到在软件设计里“代理模式”扮演着几乎一模一样的角色。它也是一个“中介”一个“替身”或者一个“包装器”。当你不能、不想或者不方便直接访问某个对象时代理就出场了。它帮你处理那些繁琐的、重复的、或者需要额外控制的事情比如权限检查、日志记录、性能监控、延迟初始化等等让你能更“纯粹”地关注核心业务逻辑。今天我们就来彻底搞懂 Java 世界里两种最核心的代理实现静态代理和动态代理。无论你是刚接触设计模式的新手还是想深入理解 Spring AOP 底层原理的老鸟这篇文章都会带你从“是什么”、“为什么”到“怎么用”并最终落脚到“如何选”让你不仅会用更能理解其背后的设计哲学和实现细节。我们尤其会聚焦于 JDK 动态代理和 CGLIB 这两种在框架中比如 Spring AOP至关重要的技术剖析它们的区别与选择策略。2. 静态代理手把手打造一个专属“中介”静态代理顾名思义就是在编译期就确定下来的代理关系。我们需要手动为每一个被代理的类编写一个对应的代理类。这种方式非常直观就像你为了办一件事专门雇了一个私人助理。2.1 核心角色与 UML 图解在深入代码之前我们先通过一个经典的场景来理解静态代理中的几个核心角色。假设我们有一个“数据查询”服务现在需要为这个服务添加执行耗时统计的功能。抽象主题Subject定义了被代理对象和代理对象的共同接口。这样客户端就可以像使用真实对象一样使用代理对象。这通常是一个接口。真实主题Real Subject真正执行业务逻辑的对象也就是被代理的对象。代理Proxy持有对真实主题的引用并在其接口方法被调用时可以附加额外的操作如日志、鉴权然后再将请求转发给真实主题。它们的关系用 UML 类图表示非常简单代理类和真实类都实现同一个接口并且代理类内部包含一个真实类的实例引用。2.2 一个完整的代码示例为数据查询添加耗时监控让我们用代码来具象化这个过程。首先定义我们的抽象主题——DataService接口。// 1. 抽象主题 (Subject) public interface DataService { String queryData(String param); void updateData(String data); }接着创建真实主题——真正干活的DataServiceImpl。// 2. 真实主题 (Real Subject) public class DataServiceImpl implements DataService { Override public String queryData(String param) { // 模拟一个耗时的数据库查询 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return “查询结果: ” param; } Override public void updateData(String data) { // 模拟数据更新操作 System.out.println(“更新数据: ” data); } }现在关键来了。我们不希望修改DataServiceImpl的代码遵循“开闭原则”但又想给它的每个方法加上执行时间统计。这时静态代理类DataServiceProxy就派上用场了。// 3. 代理 (Proxy) public class DataServiceProxy implements DataService { // 持有真实主题的引用 private DataService realService; public DataServiceProxy(DataService realService) { this.realService realService; } Override public String queryData(String param) { long start System.currentTimeMillis(); // 转发请求给真实对象 String result realService.queryData(param); long end System.currentTimeMillis(); System.out.println(“queryData 方法执行耗时: ” (end - start) “ms”); return result; } Override public void updateData(String data) { long start System.currentTimeMillis(); // 转发请求给真实对象 realService.updateData(data); long end System.currentTimeMillis(); System.out.println(“updateData 方法执行耗时: ” (end - start) “ms”); } }最后看看客户端如何调用public class Client { public static void main(String[] args) { // 创建真实对象 DataService realService new DataServiceImpl(); // 创建代理对象并将真实对象传入 DataService proxy new DataServiceProxy(realService); // 客户端通过代理对象调用方法完全无感知 String data proxy.queryData(“testParam”); System.out.println(data); proxy.updateData(“newData”); } }运行这段代码你会在控制台看到类似这样的输出queryData 方法执行耗时: 101ms 查询结果: testParam 更新数据: newData updateData 方法执行耗时: 0ms注意这里updateData的耗时是 0ms是因为Thread.sleep只存在于queryData中。这个细节恰恰说明了代理的透明性——它只负责增强不改变原有逻辑。2.3 静态代理的优缺点与适用场景优点直观清晰代理类和被代理类的关系一目了然符合面向对象的设计思想。无侵入性可以在不修改目标对象代码的前提下扩展其功能。职责清晰将核心业务逻辑DataServiceImpl与横切关注点如日志、监控在DataServiceProxy中分离。缺点类爆炸如果系统中有大量需要代理的类或者接口方法很多你就需要编写大量的代理类导致项目臃肿难以维护。想象一下如果有 50 个服务类需要加日志你就得写 50 个几乎雷同的代理类。紧耦合代理类和被代理类在编译期就绑定在一起。如果接口 (DataService) 发生变更比如增加了一个新方法那么所有实现了该接口的代理类都必须跟着修改违反了“对修改关闭”的原则。灵活性差增强逻辑如这里的耗时统计是硬编码在代理类中的。如果想动态地、按需地为不同方法添加不同的增强比如只对queryData做缓存对updateData做事务管理静态代理就显得力不从心。适用场景静态代理适用于代理类较少、接口稳定、增强逻辑简单且固定的场景。例如在早期的小型项目或某些工具类中为少数几个核心服务类添加统一的日志或权限控制使用静态代理是简单有效的。3. 动态代理运行时“凭空”创造代理对象动态代理就是为了解决静态代理的“类爆炸”和“灵活性差”而生的。它的核心思想是在程序运行期间动态地在内存中创建代理类及其对象而无需手动编写源代码。Java 主要提供了两种主流的动态代理机制基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理。3.1 JDK 动态代理基于接口的“契约”代理JDK 动态代理是 Java 标准库 (java.lang.reflect) 自带的。它有一个关键限制它只能为接口创建代理。这是因为它的原理是在运行时动态生成一个实现了指定接口的新类。3.1.1 核心组件InvocationHandlerInvocationHandler是 JDK 动态代理的灵魂。它是一个调用处理器接口我们只需要实现它的invoke方法。所有对代理对象方法的调用都会被 JVM 路由到这个invoke方法中。public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }proxy: 动态生成的代理对象本身通常在这个方法里用不到慎用否则可能引起递归调用。method: 当前被调用的方法对象Method实例。args: 调用方法时传入的参数。我们的增强逻辑如日志、事务就写在invoke方法里并通过method.invoke(target, args)来调用原始目标对象的方法。3.1.2 完整实现用 JDK 动态代理重构耗时监控让我们用 JDK 动态代理来实现和之前静态代理一样的功能。首先实现一个通用的InvocationHandlerimport java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class TimingInvocationHandler implements InvocationHandler { // 持有真实目标对象 private final Object target; public TimingInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); // 调用真实对象的方法 Object result method.invoke(target, args); long end System.currentTimeMillis(); System.out.println(method.getName() “ 方法执行耗时: ” (end - start) “ms”); return result; } }然后在客户端通过Proxy.newProxyInstance来创建代理对象import java.lang.reflect.Proxy; public class JdkDynamicProxyClient { public static void main(String[] args) { // 1. 创建真实对象 DataService realService new DataServiceImpl(); // 2. 创建 InvocationHandler InvocationHandler handler new TimingInvocationHandler(realService); // 3. 动态创建代理对象 DataService proxy (DataService) Proxy.newProxyInstance( realService.getClass().getClassLoader(), // 类加载器 new Class[]{DataService.class}, // 代理需要实现的接口数组 handler // 调用处理器 ); // 4. 使用代理对象 System.out.println(proxy.queryData(“dynamicParam”)); proxy.updateData(“dynamicData”); // 打印代理对象的类名很有趣 System.out.println(“代理对象的类是” proxy.getClass().getName()); } }运行后输出效果和静态代理一致但最后一行会打印出类似$Proxy0这样的类名。这就是 JVM 在运行时为我们动态生成的代理类它实现了DataService接口。3.1.3 底层原理浅析与注意事项当你调用Proxy.newProxyInstance时JDK 内部大致做了以下几件事根据传入的接口数组在内存中动态生成一个类的字节码。这个类会实现所有这些接口。这个生成的类如$Proxy0的每个方法内部逻辑都差不多调用我们传入的InvocationHandler的invoke方法。使用传入的ClassLoader将这个字节码加载到 JVM 中。利用反射 API 创建这个新类的实例并将其返回。重要提示由于 JDK 动态代理是基于接口的所以被代理的目标对象必须至少实现一个接口。如果你尝试代理一个没有实现任何接口的普通类Proxy.newProxyInstance会抛出IllegalArgumentException。3.2 CGLIB 动态代理基于继承的“子类”代理CGLIB (Code Generation Library) 是一个强大的、高性能的代码生成库。它通过继承的方式来实现代理。因此它可以为没有实现接口的普通类创建代理。Spring AOP 在目标对象没有实现接口时默认就使用 CGLIB。3.2.1 核心组件MethodInterceptorCGLIB 的核心是MethodInterceptor接口它的作用和 JDK 的InvocationHandler类似。public interface MethodInterceptor extends Callback { Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable; }obj: 动态生成的代理对象CGLIB 增强后的子类对象。method: 被拦截的方法目标类的方法。args: 方法参数。proxy: 用于调用父类即原始目标类方法的代理方法。强烈建议使用proxy.invokeSuper(obj, args)而不是method.invoke(target, args)因为前者直接调用字节码性能更高且能避免某些递归问题。3.2.2 完整实现用 CGLIB 代理普通类首先我们需要一个没有接口的普通类作为目标// 一个没有实现任何接口的普通类 public class ConcreteDataService { public String findData(String id) { try { Thread.sleep(80); } catch (InterruptedException e) { e.printStackTrace(); } return “Concrete Data for ” id; } public final void finalMethod() { System.out.println(“这是一个 final 方法无法被代理增强。”); } }然后实现MethodInterceptorimport net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CglibTimingInterceptor implements MethodInterceptor { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 可以在这里进行方法过滤例如不代理 final 方法或 toString 等方法 if (“finalMethod”.equals(method.getName())) { return proxy.invokeSuper(obj, args); // 直接调用不加增强 } long start System.currentTimeMillis(); // 使用 MethodProxy.invokeSuper 调用父类目标类方法性能更好 Object result proxy.invokeSuper(obj, args); long end System.currentTimeMillis(); System.out.println(“CGLIB - ” method.getName() “ 方法执行耗时: ” (end - start) “ms”); return result; } }最后使用Enhancer类创建代理import net.sf.cglib.proxy.Enhancer; public class CglibDynamicProxyClient { public static void main(String[] args) { // 1. 创建 Enhancer 对象相当于 JDK Proxy 的工厂类 Enhancer enhancer new Enhancer(); // 2. 设置父类即要被代理的类 enhancer.setSuperclass(ConcreteDataService.class); // 3. 设置回调即我们的 MethodInterceptor enhancer.setCallback(new CglibTimingInterceptor()); // 4. 创建代理对象注意这里创建的是目标类的子类对象 ConcreteDataService proxy (ConcreteDataService) enhancer.create(); // 5. 使用代理对象 System.out.println(proxy.findData(“123”)); proxy.finalMethod(); // 这个方法不会被增强 System.out.println(“代理对象的类是” proxy.getClass().getName()); System.out.println(“代理对象的父类是” proxy.getClass().getSuperclass().getName()); } }运行后你会看到findData方法被增强了而finalMethod则没有。同时打印的类名会是类似ConcreteDataService$$EnhancerByCGLIB$$xxxxxxxx这样的形式这正是 CGLIB 生成的子类。3.2.3 CGLIB 的限制与性能考量无法代理 final 类或 final 方法因为 CGLIB 通过生成子类来工作所以对于 final 修饰的类或方法它无能为力。构造函数调用代理对象的创建会调用两次目标类的构造函数这是一个常见的误解。实际上CGLIB 生成的子类在实例化时会调用一次父类的无参构造函数如果存在的话这是 Java 对象初始化的标准流程然后我们通过enhancer.create()创建的是这个子类的实例。目标对象本身父类实例并没有被额外创建一次。增强逻辑发生在子类重写的方法里。性能在早期版本中CGLIB 生成的代理对象在方法调用上可能比 JDK 动态代理稍慢因为涉及到继承层次和方法重写。但随着 JVM 优化和 CGLIB 自身的改进这个差距已经很小甚至在很多场景下 CGLIB 更快因为它直接操作字节码而 JDK 代理需要反射调用。Spring 等框架通常会对生成的代理类进行缓存以最大化性能。4. JDK 动态代理 vs CGLIB深入对比与框架选型这是面试和实际框架使用中最常被问到的问题。它们的区别远不止“一个基于接口一个基于继承”这么简单。4.1 本质区别对比表特性JDK 动态代理CGLIB 动态代理实现原理通过实现接口在运行时生成接口的代理类。通过继承目标类在运行时生成目标类的子类。依赖Java 标准库 (java.lang.reflect.Proxy和InvocationHandler)无需额外 Jar。需要引入cglib库Spring Core 已包含。目标要求必须至少实现一个接口。目标类不能是 final 的。目标方法如果是 final则无法被增强。性能在 JDK 1.8 及以后性能有显著优化。调用代理方法时本质是通过InvocationHandler进行反射调用。生成代理类开销稍大但方法调用时直接通过重写的方法执行通常比 JDK 反射调用更快。现代 JVM 下两者差异不大。生成类类名格式$ProxyN(N为数字)。类名格式TargetClass$$EnhancerByCGLIB$$xxxxxx。局限性只能代理接口中声明的方法。无法代理 final 类和方法构造函数会被调用符合 Java 继承规范。4.2 Spring AOP 的默认选择与配置Spring AOP 作为应用最广泛的 AOP 实现其底层就是基于动态代理。它的选择策略是默认策略Spring 4.x / 5.x如果目标对象实现了至少一个接口则默认使用JDK 动态代理。如果目标对象没有实现任何接口则默认使用CGLIB。强制使用 CGLIB 你可以在 Spring 配置中强制使用 CGLIB即使目标类实现了接口。这样做通常是为了代理类上的方法而不仅仅是接口方法或者是为了获得更好的性能在某些历史版本或特定场景下。XML 配置aop:aspectj-autoproxy proxy-target-class“true”/注解配置EnableAspectJAutoProxy(proxyTargetClass true)性能与“坑”代理内部方法调用这是 Spring AOP 一个经典的“坑”。在同一个类中一个方法 A 调用另一个方法 B即使 B 方法上有Transactional或Cacheable等注解其增强也不会生效。因为 A 调用 B 是this.b()的直接调用绕过了代理对象。解决方法通常是自我注入 (Autowired注入自身) 或从 AOP 上下文获取代理对象。CGLIB 与构造函数由于 CGLIB 是继承代理类实例化时会调用父类的构造函数。如果目标类的构造函数有复杂的初始化逻辑或副作用需要注意。4.3 如何选择一个实战决策流程图面对一个具体的增强需求你应该如何选择代理方式可以遵循以下思路开始 | v 目标对象是否有接口 | | 是 否 | | v v 考虑因素 必须使用 CGLIB | v 是否需要代理类自身的方法非接口方法 | | 是 否 | | v v 选择 CGLIB 考虑因素 | v 目标类或方法是否为 final | | 是 否 | | v v 无法代理/重构代码 优先选择 JDK 动态代理 | 更标准依赖少 v 结束简单来说有接口且只关心接口方法-JDK 动态代理。这是最纯粹、最符合面向接口编程原则的方式。无接口或需要代理类自身的特定方法-CGLIB。对 final 类或方法有增强需求- 这是一个设计警讯可能需要重构代码因为代理模式无法在此生效。5. 超越基础动态代理的高级应用与模式变体理解了基本原理后动态代理的能力远不止加个日志这么简单。它为实现许多高级设计模式和应用提供了底层支持。5.1 实现延迟初始化虚拟代理虚拟代理用于控制访问一个创建开销很大的对象。例如加载一个高分辨率图片在图片真正加载完成前先显示一个占位符。// 接口 public interface ExpensiveObject { void process(); } // 真实对象创建成本高 public class ExpensiveObjectImpl implements ExpensiveObject { public ExpensiveObjectImpl() { // 模拟昂贵的初始化如加载大文件、建立网络连接 heavyInitialConfiguration(); System.out.println(“ExpensiveObjectImpl 被真实创建了。”); } private void heavyInitialConfiguration() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } Override public void process() { System.out.println(“处理业务逻辑...”); } } // 虚拟代理 public class VirtualProxyHandler implements InvocationHandler { private ExpensiveObject realObject; // 开始为 null Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 在第一次调用时才初始化真实对象 if (realObject null) { realObject new ExpensiveObjectImpl(); } // 将调用委托给真实对象 return method.invoke(realObject, args); } } // 客户端使用 public class Client { public static void main(String[] args) { ExpensiveObject proxy (ExpensiveObject) Proxy.newProxyInstance( Client.class.getClassLoader(), new Class[]{ExpensiveObject.class}, new VirtualProxyHandler() ); System.out.println(“代理对象已创建真实对象尚未创建...”); // 直到调用 process 方法真实对象才会被创建 proxy.process(); } }5.2 实现远程代理RPC/Stub的基石远程代理是 RPC远程过程调用框架的基石。本地存根Stub就是一个代理对象它负责将方法调用及其参数序列化通过网络发送给远程服务端接收结果并反序列化返回给客户端。Dubbo、gRPC 等框架的客户端接口本质上就是通过动态代理通常是 JDK 动态代理实现的。// 一个简化的模拟 public class RemoteServiceProxy implements InvocationHandler { private String host; private int port; public RemoteServiceProxy(String host, int port) { this.host host; this.port port; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 序列化方法名、参数类型、参数值 String methodName method.getName(); Class?[] parameterTypes method.getParameterTypes(); // 使用 JSON、Hessian、Protobuf 等序列化 args... String requestBody serialize(methodName, parameterTypes, args); // 2. 通过网络发送请求简化为打印 System.out.println(“发送请求到 ” host “:” port “, 内容: ” requestBody); // 实际代码会使用 HttpClient、Netty 等 // 3. 模拟接收响应并反序列化 String simulatedResponse “{‘result’: ‘remote data’}”; Object result deserialize(simulatedResponse, method.getReturnType()); return result; } // 省略序列化/反序列化方法... }5.3 实现保护代理与智能引用保护代理可以控制对真实对象的访问权限。智能引用代理可以在访问真实对象时附加一些内务操作例如引用计数记录对象被引用的次数当计数为0时自动释放资源。线程安全锁在调用真实对象的方法前加锁调用后释放。缓存第一次调用时缓存结果后续调用直接返回缓存。// 一个简单的缓存代理示例 public class CacheInvocationHandler implements InvocationHandler { private MapString, Object cache new ConcurrentHashMap(); private Object target; public CacheInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 只为 ‘getXXX’ 方法添加缓存 if (method.getName().startsWith(“get”)) { String cacheKey generateCacheKey(method, args); Object cachedValue cache.get(cacheKey); if (cachedValue ! null) { System.out.println(“从缓存获取数据: ” cacheKey); return cachedValue; } Object result method.invoke(target, args); cache.put(cacheKey, result); System.out.println(“存入缓存: ” cacheKey); return result; } // 非 get 方法直接调用 return method.invoke(target, args); } private String generateCacheKey(Method method, Object[] args) { /* 生成唯一键 */ } }6. 实战避坑指南动态代理的那些“坑”与最佳实践在实际项目中应用动态代理尤其是结合 Spring 等框架时会遇到一些典型问题。了解它们能让你少走弯路。6.1 “this” 调用导致的增强失效这是 Spring AOP 中最常见的问题之一。Service public class UserServiceImpl implements UserService { public void methodA() { System.out.println(“执行 methodA”); this.methodB(); // 问题在这里直接调用了 this.methodB() } Transactional // 假设有事务注解 public void methodB() { System.out.println(“执行 methodB”); } }当你从外部调用userService.methodA()时methodA内部的this.methodB()调用的是UserServiceImpl这个原始对象上的方法而不是经过 Spring 代理可能是 JDK 或 CGLIB 生成后的对象。因此methodB上的Transactional注解会失效。解决方案自我注入推荐Spring 可以通过注入自身代理来解决循环依赖但需要开启EnableAspectJAutoProxy(exposeProxy true)。Service public class UserServiceImpl implements UserService { Autowired private UserService selfProxy; // 注入代理后的自己 public void methodA() { System.out.println(“执行 methodA”); selfProxy.methodB(); // 通过代理调用 } Transactional public void methodB() { ... } }从 AopContext 获取当前代理需暴露代理EnableAspectJAutoProxy(exposeProxy true) Configuration public class AppConfig { ... } // 在方法中 UserService proxy (UserService) AopContext.currentProxy(); proxy.methodB();重构设计将methodB抽到另一个 Service 中通过服务间调用来触发代理。6.2 代理对象的类型识别与相等性判断由于代理对象是运行时生成的类它的getClass()和原始目标类不同。这会影响instanceof操作和某些依赖类类型的操作。DataService real new DataServiceImpl(); DataService jdkProxy createJdkProxy(real); System.out.println(real instanceof DataService); // true System.out.println(jdkProxy instanceof DataService); // true (因为它实现了接口) System.out.println(real.getClass()); // class com.example.DataServiceImpl System.out.println(jdkProxy.getClass()); // class com.sun.proxy.$Proxy0 System.out.println(real.getClass().equals(jdkProxy.getClass())); // false // 对于 CGLIB 代理 ConcreteDataService cglibProxy createCglibProxy(); System.out.println(cglibProxy instanceof ConcreteDataService); // true (因为它是子类) System.out.println(cglibProxy.getClass()); // class com.example.ConcreteDataService$$EnhancerByCGLIB$$...最佳实践在业务逻辑中尽量避免直接依赖具体的类类型进行比较。更多地依赖接口和多态。如果必须判断Spring 提供了AopUtils工具类来帮助识别代理import org.springframework.aop.support.AopUtils; // 判断是否是代理 AopUtils.isAopProxy(myBean); // 判断是否是 JDK 动态代理 AopUtils.isJdkDynamicProxy(myBean); // 判断是否是 CGLIB 代理 AopUtils.isCglibProxy(myBean); // 获取原始目标类 Class? targetClass AopUtils.getTargetClass(myBean);6.3 性能考量与代理类缓存虽然动态代理在运行时生成类会有一些开销但这在绝大多数应用场景中都是微不足道的。真正的性能考量点在于代理类创建开销每次调用Proxy.newProxyInstance或Enhancer.create()都会生成新的代理类字节码并加载。务必避免在循环或高频调用中创建代理。框架级缓存像 Spring 这样的框架在容器启动时就会为需要代理的 Bean 创建好代理对象并将其缓存起来。整个应用生命周期内我们使用的都是同一个缓存的代理实例。所以代理创建的开销通常是一次性的在启动时完成。方法调用开销JDK 动态代理通过反射调用InvocationHandler.invoke而 CGLIB 通过 FastClass 机制直接调用方法后者通常更快。但在现代 JVM 上尤其是 JDK 1.8 之后这个差距已经非常小不应作为首要选型依据。建议在性能敏感的底层代码如每秒处理数十万次的工具方法中谨慎使用重量级的代理增强。对于普通的业务服务层Service动态代理的开销完全可以接受。6.4 调试与日志看清代理的本质当代理行为不符合预期时调试可能会有点棘手因为你看到的对象类型是代理类。以下技巧有助于调试打印对象类名如我们之前所做的System.out.println(proxy.getClass().getName())可以立刻告诉你这是 JDK 代理 ($Proxy) 还是 CGLIB 代理 ($$EnhancerByCGLIB$$)。使用 IDE 调试器在调试时可以查看代理对象的字段。JDK 代理对象内部有一个h字段指向InvocationHandlerCGLIB 代理对象内部有CGLIB$CALLBACK_0等字段指向MethodInterceptor。顺着这些引用就能找到你自定义的增强逻辑。开启 Spring 调试日志在application.properties中设置logging.level.org.springframework.aopDEBUG可以看到 Spring AOP 创建代理的详细过程。动态代理是 Java 高级编程和框架设计中不可或缺的一环。从简单的日志增强到复杂的 RPC、事务管理、安全控制其身影无处不在。理解静态代理与动态代理特别是 JDK 动态代理与 CGLIB 的原理、区别与适用场景不仅能让你更好地使用 Spring 等框架更能让你具备设计和实现类似灵活性架构的能力。下次当你再看到“远程桌面连接代理”或者任何“代理”这个词时希望你能会心一笑想起背后这套优雅而强大的设计模式。