ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java代理模式详解:从静态代理到JDK动态代理与CGLIB,实战避坑指南

Java代理模式详解:从静态代理到JDK动态代理与CGLIB,实战避坑指南 1. 一个真实场景给订单服务加耗时统计我是怎么被代理模式救回来的先讲一个我实际经历过的场景。去年做支付系统对账模块时需要给十几个下游接口统一加上耗时监控、异常日志和重试逻辑。我第一版用最直接的方式写的在每一个接口实现类的方法里手动插入一段long start System.currentTimeMillis()再加上 try-catch 包一层重试。当时觉得没什么反正代码能跑就行。等我把这套逻辑复制到第 8 个接口的时候我自己都忍不住了——每加一个方法就要重复贴一遍一模一样的模板代码业务逻辑和监控逻辑完全耦合在一起。更要命的是产品经理过来说“耗时监控里顺带把入参出参打一下”我整个人是崩溃的因为这意味着我要把十几个接口里的代码全部改一遍。后来我停下来重新想这个问题发现我真正需要的其实是一层包裹业务方法本身不用动但它外面能自动多出一圈“附加逻辑”。这个东西在 Java 世界里有一个现成的名字叫做代理模式。代理模式的核心价值就一句话在不修改原有代码的前提下给目标对象增加额外的控制逻辑。这跟我们生活中找中介租房是同一个道理。房东要出租房子不想自己应付一堆看房、签约、维修的琐事于是把房子委托给中介租客也只跟中介打交道不需要知道房东是谁。中介就是房东的代理它拦截了“租房”这件事在里面加了筛选租客、拟定合同、收取中介费这些额外动作而房东出租房子这个核心功能完全没有变。Java 里的代理分为两大流派静态代理和动态代理。静态代理是在编译期就写死代理类动态代理则是在运行期动态生成代理对象。很多人面试时能背出这两个名词但真到了要自己写一个通用监控组件、或者排查 Spring AOP 相关 bug 的时候就发现对它们的理解完全不够用。这篇文章我会从一次真实的重构出发把静态代理、JDK 动态代理、CGLIB 动态代理三者的代码实现、底层原理、适用场景和踩坑记录讲透最后再用我在项目里真实遇到过的几个代理失效问题收尾。无论你是刚开始学 Java 的初学者还是写了几年业务代码但没深入过框架原理的开发这篇应该都能给你一些有用的东西。2. 静态代理实践能解决一时之痛但接口一多就会想骂人2.1 完整代码示例订单服务的手写代理先看最简单的静态代理。假设我们有个订单服务接口public interface OrderService { String createOrder(String productId, int count); Order queryOrder(String orderId); }接口的实现类长这样public class OrderServiceImpl implements OrderService { Override public String createOrder(String productId, int count) { // 模拟业务处理耗时 try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return ORDER_20250101_001; } Override public Order queryOrder(String orderId) { // 模拟查询耗时 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return new Order(orderId, PAID); } }现在我需要给这两个方法加上耗时统计不动OrderServiceImpl的前提下手写一个代理类public class OrderServiceStaticProxy implements OrderService { // 持有真实的目标对象 private final OrderService target; public OrderServiceStaticProxy(OrderService target) { this.target target; } Override public String createOrder(String productId, int count) { long start System.currentTimeMillis(); try { String orderId target.createOrder(productId, count); System.out.println(createOrder 耗时: (System.currentTimeMillis() - start) ms); return orderId; } catch (Exception e) { System.out.println(createOrder 异常: e.getMessage()); throw e; } } Override public Order queryOrder(String orderId) { long start System.currentTimeMillis(); try { Order order target.queryOrder(orderId); System.out.println(queryOrder 耗时: (System.currentTimeMillis() - start) ms); return order; } catch (Exception e) { System.out.println(queryOrder 异常: e.getMessage()); throw e; } } }调用的时候也很直白把真实的OrderServiceImpl塞进代理类然后所有外部调用都走代理对象OrderService orderService new OrderServiceStaticProxy(new OrderServiceImpl()); orderService.createOrder(SKU_10086, 2); orderService.queryOrder(ORDER_20250101_001);这段代码运行起来你会在控制台看到每个方法的耗时输出。业务类本身没有被改动耗时统计的逻辑也完整地包裹在了代理类中。从结果来说静态代理确实解决了“不想动原代码”的问题。2.2 静态代理的硬伤一个接口一个代理类早晚把自己累死静态代理的思路好理解但它有两个非常现实的问题我用真实项目体验来跟你说明白。第一个问题代理类和目标类必须实现相同的接口而且每个方法都要手写一遍增强逻辑。你注意看上面的代码createOrder和queryOrder两个方法里耗时统计的代码几乎一模一样只是调用的方法和日志前缀不同。如果OrderService接口有 20 个方法这个代理类里就要写 20 遍几乎重复的代码。这哪是解放生产力这就是换一种方式继续写模板代码。第二个问题更致命接口数量一旦多起来代理类的数量会直接爆炸。我当时的项目里同时要对订单服务、用户服务、库存服务、支付渠道服务做监控。每个服务一个接口、一个实现类按静态代理的写法我就得手写 4 个代理类每个代理类里再重复写一遍耗时统计。如果明天来了个新的风控服务也要监控我又得新建一个代理类。加一个服务 新建一个类 复制几十行模板代码这种模式显然是不可持续的。静态代理还有一个隐性问题一旦接口里新增了方法目标实现类和代理类都必须同步修改否则编译都过不去。接口是系统里最常变化的部分这种强耦合关系会让维护成本随项目规模线性增长。我当时面临的就是这样一个局面——静态代理帮我实现了需求但实现的代价是代码量膨胀和难以维护。我知道一定有一种方式能够在运行期动态地生成代理对象而不是让我为每个接口手动创建一个代理类这就是接下来要讲的动态代理。3. JDK 动态代理运行时动态生成代理对象终于告别代理类爆炸3.1 核心代码一个通用的 InvocationHandlerJDK 动态代理和静态代理最大的区别在于静态代理的代理类是在编译期就存在的而 JDK 动态代理的代理类是在运行期由 JVM 动态生成的。你不需要为每个接口手写代理类只需要实现一个InvocationHandler然后把接口交给他它就能在内存中生成一个代理对象。还是上面订单服务的场景用 JDK 动态代理重写只需要两个东西。第一个是InvocationHandler的实现类它是整个代理逻辑的核心public class TimeCostInvocationHandler implements InvocationHandler { // 目标对象被代理的真实对象 private final Object target; public TimeCostInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); try { // 反射调用目标对象的真实方法 Object result method.invoke(target, args); System.out.println(method.getName() 耗时: (System.currentTimeMillis() - start) ms); return result; } catch (Exception e) { System.out.println(method.getName() 异常: e.getMessage()); throw e; } } }第二个是生成代理对象的工厂方法。JDK 提供了一个Proxy类配合newProxyInstance方法就能在运行期创建代理对象public class JdkProxyFactory { public static Object createProxy(Object target) { return Proxy.newProxyInstance( // 第一个参数类加载器一般用目标类的类加载器即可 target.getClass().getClassLoader(), // 第二个参数目标类实现的所有接口代理对象只能强转成这些接口类型 target.getClass().getInterfaces(), // 第三个参数InvocationHandler代理对象的所有方法调用都会进入这里 new TimeCostInvocationHandler(target) ); } }使用起来非常简单OrderService orderService (OrderService) JdkProxyFactory.createProxy(new OrderServiceImpl()); String orderId orderService.createOrder(SKU_10086, 2); Order order orderService.queryOrder(ORDER_20250101_001);和静态代理对比一下你会发现JdkProxyFactory是通用的它不关心你传进来的对象是OrderService还是UserService。任何接口 实现类的组合都用这一套代码就能完成代理。我之前担心的“每新增一个服务就要新建一个代理类”的问题到这里就彻底解决了。3.2 底层原理代理类的字节码是在运行期现场生成的JDK 动态代理能工作的关键在于Proxy.newProxyInstance这个静态方法在运行期做了三件事根据传入的接口列表在内存中动态生成一个代理类的字节码。用传入的类加载器加载这个刚生成的代理类。实例化代理类并传入我们自定义的InvocationHandler通过构造器建立关联。你可以在 JDK 里打开java.lang.reflect.Proxy的源码翻到ProxyClassFactory内部类里面有一个generateProxyClass方法它做的事情就是拼接字节码生成一个名字形如$Proxy0、$Proxy1的类。这个动态生成的$Proxy0类有什么特征它继承了Proxy这个父类并且实现了你传入的所有接口。也就是说$Proxy0和你的目标实现类OrderServiceImpl之间没有任何继承关系它们只是因为implements了同一个OrderService接口所以可以互相转型。当你调用orderService.createOrder(...)时发生的实际调用链是这样的外部调用 orderService.createOrder() - $Proxy0 继承自 Proxy 持有的 InvocationHandler - 调用 handler.invoke(this, method, args) - 在 invoke 内部通过反射调用 OrderServiceImpl.createOrder()刚才代码里的TimeCostInvocationHandler它的invoke方法就是整个调用链的中枢。你可以在里面做任何增强逻辑——耗时统计、参数打印、权限校验、事务管理、重试机制全都可以塞在这里。想看到这个动态生成的类源码可以设置一个系统属性。JDK 8 及之前用sun.misc.ProxyGenerator.saveGeneratedFilesJDK 9 之后改成了jdk.proxy.ProxyGenerator.saveGeneratedFiles设置为true后代理类的 class 文件会输出到指定目录用反编译工具打开你能非常直观地看到$Proxy0里所有方法的实现public final class $Proxy0 extends Proxy implements OrderService { private static Method m1; private static Method m2; private static Method m3; private static Method m4; public $Proxy0(InvocationHandler h) { super(h); } public final String createOrder(String var1, int var2) throws Throwable { try { return (String) super.h.invoke(this, m3, new Object[]{var1, Integer.valueOf(var2)}); } catch (RuntimeException | Error var4) { throw var4; } catch (Throwable var5) { throw new UndeclaredThrowableException(var5); } } public final Order queryOrder(String var1) throws Throwable { try { return (Order) super.h.invoke(this, m4, new Object[]{var1}); } catch (RuntimeException | Error var4) { throw var4; } catch (Throwable var5) { throw new UndeclaredThrowableException(var5); } } }看到method.invoke(target, args)这行代码你就明白为什么 JDK 动态代理在性能上会比手写代码多一些额外开销——它走了一层反射调用。但在 JDK 8 之后反射性能得到了大幅优化绝大多数业务场景下的性能损耗都可以忽略不计。3.3 局限性只认接口不认实现类JDK 动态代理什么都好但它有一个硬性限制目标对象必须实现至少一个接口代理对象只能通过接口类型来使用。原因也很简单看上面的$Proxy0类结构就清楚了它已经继承了Proxy类而 Java 是单继承所以$Proxy0不可能再继承你的OrderServiceImpl。它要具备OrderService的方法唯一办法就是实现OrderService这个接口。这带来的实际影响是如果你的目标类没有实现任何接口比如直接写了一个class OrderServiceImpl { ... }没有implements任何接口用Proxy.newProxyInstance就会直接抛出IllegalArgumentException提示你是类不是接口。还有一个容易被忽视的细节代理对象不能强转成目标实现类。比如OrderServiceImpl里有一个独有的方法calcTotalPrice()这个方法没有定义在OrderService接口里那么通过 JDK 动态代理生成的对象引用类型只能是OrderService你想把它强转成OrderServiceImpl来调用calcTotalPrice()会直接抛ClassCastException。这个坑我在后面会详细展开因为它在 Spring 项目里非常常见。但我当时实际项目里有不少类本身就没有设计接口是纯类结构。这种场景下 JDK 动态代理就没法用了我需要另一个方案——CGLIB。4. CGLIB 动态代理继承方案突破接口限制4.1 代码示例与核心 APICGLIBCode Generation Library是一套基于字节码生成技术的类库它不像 JDK 动态代理那样局限于接口。在 Spring 早期版本中CGLIB 以独立依赖的形式存在后来被整合进了 Spring 的核心包。如果单独使用 CGLIB需要引入依赖dependency groupIdcglib/groupId artifactIdcglib/artifactId version3.3.0/version /dependency如果不引依赖Spring 环境下可以直接用org.springframework.cglib.proxy.Enhancer因为 Spring 内部做了一个 reloc 处理把 CGLIB 的类挪到了自己的包路径下用法完全一样。CGLIB 的核心类是Enhancer和MethodInterceptor。重点看MethodInterceptor它和 JDK 的InvocationHandler作用类似都是在方法调用前后插入增强逻辑但方法签名不同public class TimeCostMethodInterceptor implements MethodInterceptor { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { long start System.currentTimeMillis(); try { // proxy.invokeSuper 代表调用目标类父类的方法 Object result proxy.invokeSuper(obj, args); System.out.println(method.getName() 耗时: (System.currentTimeMillis() - start) ms); return result; } catch (Exception e) { System.out.println(method.getName() 异常: e.getMessage()); throw e; } } }生成代理对象的工厂方法public class CglibProxyFactory { public static Object createProxy(Class? targetClass) { Enhancer enhancer new Enhancer(); // 设置目标类为代理类的父类 enhancer.setSuperclass(targetClass); // 设置方法拦截器 enhancer.setCallback(new TimeCostMethodInterceptor()); // 创建代理对象 return enhancer.create(); } }使用的时候直接拿目标类就能创建代理完全不需要接口// 假设 OrderServiceImpl 是一个没有实现任何接口的纯类 OrderServiceImpl proxy (OrderServiceImpl) CglibProxyFactory.createProxy(OrderServiceImpl.class); proxy.createOrder(SKU_10086, 2); proxy.queryOrder(ORDER_20250101_001);你会发现 CGLIB 创建的代理对象可以直接强转成目标实现类这一点和 JDK 动态代理很不一样。4.2 原理拆解动态生成目标类的子类覆盖父类方法CGLIB 之所以不用接口就能代理核心原因是它走的不是“实现接口”的路线而是继承目标类生成一个子类然后重写目标类的方法。前面代码里的enhancer.setSuperclass(targetClass)意思就是让生成的代理类继承OrderServiceImpl然后在代理类中重写createOrder和queryOrder方法。重写后的方法内部会先调用MethodInterceptor.intercept再通过proxy.invokeSuper(obj, args)调用父类也就是目标类的原始方法。你如果反编译 CGLIB 生成的代理类会看到类似这样的结构public class OrderServiceImpl$$EnhancerByCGLIB$$a1b2c3d4 extends OrderServiceImpl implements Factory { private MethodInterceptor methodInterceptor; public final String createOrder(String var1, int var2) { MethodInterceptor interceptor this.methodInterceptor; if (interceptor ! null) { return (String) interceptor.intercept(this, CREATE_ORDER_METHOD, new Object[]{var1, new Integer(var2)}, CREATE_ORDER_PROXY); } return super.createOrder(var1, var2); } }类名里的EnhancerByCGLIB就是它的标志。方法内优先调用MethodInterceptor如果拦截器为空就调用super.createOrder(...)执行原始逻辑。这个动态生成的子类在运行期由 ASM 库直接写字节码生成不需要经过.java源文件编译的过程所以速度非常快。这就是 CGLIB 和 JDK 动态代理最根本的区别对比维度JDK 动态代理CGLIB 动态代理实现方式生成代理类实现目标接口生成代理类继承目标类前置条件目标必须实现接口目标类可被继承代理对象的类型只能转成接口类型可以直接转成目标类类型目标方法限制接口方法必须 public目标类方法不能是 final4.3 final 方法为什么不能被代理这个坑我印象特别深。有一次在项目里用 CGLIB 代理一个类发现这个类里某个方法死活不会被增强检查了半天配置也没问题最后同事提醒我“你看那个方法前面是不是有个 final 修饰符”没错final 方法无法被 CGLIB 代理。原因很简单CGLIB 靠继承重写方法来实现代理而 Java 语法规定 final 方法不能被重写。编译器直接在编译层面就堵死了这条路代理类生成时根本没法覆盖这个方法所以调用它会直接走目标类的原始逻辑。同样的道理也适用于更极端的情况final 类完全不能用 CGLIB 代理因为 final 类不允许被继承连代理类的生成这一步就直接报错了。来看一个例子public class PaymentService { // 这个方法加了 finalCGLIB 代理后不会走拦截器 public final boolean validate(String orderId) { return true; } public void pay(String orderId) { // 支付逻辑 } }运行测试代码后validate方法的耗时日志永远不会打印而pay方法的耗时日志正常输出。这个现象如果不是提前知道原理排查起来会非常迷惑——因为代码逻辑明明一模一样的两个方法行为却出现如此大的差异。另外补充一点CGLIB 还有一个小限制是关于构造器。JDK 动态代理是通过反射调用目标类构造器来创建代理对象而 CGLIB 生成子类时不会调用父类的构造器。所以在某些特殊的初始化场景下如果父类构造器里有重要逻辑CGLIB 代理可能不会执行这些逻辑。这个细节比较冷门但我在网上看到过有人因为这个排查了一整天。5. 三种代理横向对比面试与项目选型一张表搞定5.1 一张表划重点把静态代理、JDK 动态代理、CGLIB 动态代理放在一起从几个关键维度做一个对比面试时被问到直接按这个框架答对比维度静态代理JDK 动态代理CGLIB 动态代理代理类生成时机编译期手动编写运行期Proxy 生成运行期Enhancer 生成是否要求接口是代理类和目标类实现同一接口是目标类必须实现接口否继承目标类代理对象类型接口类型接口类型目标类类型增强代码位置代理类中逐方法编写InvocationHandler.invokeMethodInterceptor.intercept方法级粒度控制每个方法单独写灵活但繁琐通过 method.getName() 判断相对灵活通过 method.getName() 判断相对灵活对 final 方法可代理同接口不同类可代理接口方法不受 final 限制不可代理性能表现最好直接方法调用有反射开销有字节码操作开销维护成本高接口多则类爆炸低一套 Handler 通用低一套 Interceptor 通用关于性能网上经常有人说 CGLIB 比 JDK 动态代理快。这个说法其实要分版本和场景。老版本 JDK 中JDK 动态代理基于反射实现性能确实不理想而 CGLIB 基于 FastClass 机制直接调用目标方法所以性能更优。但 JDK 8 之后反射机制做了大幅优化差距已经缩小到几乎可以忽略到了 JDK 17 及以后官方甚至持续优化了代理实现很多基准测试里 JDK 动态代理并不输给 CGLIB。所以选型的首要因素不应该是性能而应该是目标类的结构。5.2 主流框架里用的到底是哪一种我们日常接触的框架几乎都在用代理模式。对号入座一下很多以前看不懂的代码瞬间就通了。Spring AOP 是两种混用的。Spring 早期版本的策略是目标类实现了接口就用 JDK 动态代理目标类没有实现接口就用 CGLIB。Spring Boot 2.x 之后默认配置改为强制使用 CGLIB也就是spring.aop.proxy-target-classtrue。这背后的考虑主要是兼容性问题因为很多业务代码里注入的是实现类而不是接口JDK 动态代理生成的代理对象无法强转成实现类会导致依赖注入失败。MyBatis 的 Mapper 是 JDK 动态代理的典型应用。你的 Mapper 接口没有实现类MyBatis 在启动时扫描所有 Mapper 接口通过Proxy.newProxyInstance为每个接口创建一个代理对象。当你调用userMapper.selectById(1)时这个调用的目标并不是某个实现类而是直接进入了MapperProxy作为InvocationHandler的invoke方法在方法内部根据方法签名拼接 SQL、执行查询、封装结果最后返回。这就是为什么 Mapper 接口可以不需要实现类的根本原因。Spring 的声明式事务标注了 Transactional 的方法本质上是代理调用的。框架在运行期为你的 Service 类创建一个代理对象事务的开启、提交、回滚逻辑全部写在了代理的拦截逻辑里。这也解释了为什么同一个类内部调用带 Transactional 注解的方法事务会失效——因为那个调用发生在被代理对象内部走的不是代理对象而是 this 本身的直接调用。这个问题我再往下详细讲。5.3 项目实战中的选型建议根据我自己的项目经验给出几条相对实用的选型建议如果你只是在写业务代码不涉及自定义框架封装大部分情况下根本不需要自己写动态代理。Spring AOP 已经帮你封装好了直接声明切面即可。如果你需要给一些没有实现接口的类添加通用增强逻辑CGLIB 是唯一选择。如果你在封装框架比如给一堆第三方服务做统一开关、限流、监控优先考虑 JDK 动态代理。原因很朴素它不引入额外依赖JDK 原生支持排查问题时的类名可读性也更好$Proxy0一眼就能认出来。需要代理的类如果是 final 的或者包含大量 final 方法任何动态代理方案都不好用考虑换一种设计思路比如使用门面或包装模式。这个部分理解到位之后后面要讲的踩坑案例就不是孤立的问题了它们其实都源于对底层机制理解不透彻。6. 实战避坑四个代理失效/异常案例的完整排查链路下面这几个案例都来自我真实工作以及和同事一起排查过的线上问题。每一个的背后都是对代理机制理解不到位导致的建议收藏线上遇到类似问题能省一晚上。6.1 自调用导致代理失效this 偷偷绕过了代理这是项目里最常见的一种代理失效场景先看这段代码Service public class OrderActionService { public void createAndPay() { // 直接调用同类中的另一个方法 this.createOrder(); } Transactional public void createOrder() { // 创建订单逻辑 } }线上发现一个问题createAndPay里调用的createOrder方法上的Transactional注解完全没有生效异常发生后数据没有回滚。排查链路是这样的先确认是不是 Spring 事务配置问题。检查了配置、检查了启动类注解都没有问题。确认 Transactional 是否对 public 方法生效。方法确实是 public。最后想到代理。Spring 容器中注入的OrderActionService其实是一个由 CGLIB 生成的代理子类外部通过orderActionService.createAndPay()时先进入的是拦截器逻辑。但createAndPay方法内部用的是this.createOrder()直接调用自身对象的方法而这个this是指 CGLIB 代理子类内部的原始目标对象根本没有经过代理的拦截器事务自然就不会开启。解决办法改成通过注入的代理对象来调用或者把createOrder逻辑拆分到另一个独立的 Service 类中再注入进来调用。这个问题的本质原理就是——代理只对“外部通过代理对象发起的调用”生效内部this调用会自动绕过代理。6.2 ClassCastException接口与实现类的转型陷阱这个案例非常典型我在多个项目里都见过相似的写法。假设有一个UserService接口和它的实现类UserServiceImplpublic interface UserService { String getUserName(String userId); } Service public class UserServiceImpl implements UserService { Override public String getUserName(String userId) { return Tom; } // 这个方法没有声明在接口里 public String getUserAvatar(String userId) { return http://avatar/tom.png; } }然后你在某个 Controller 里这样注入并调用Autowired private UserServiceImpl userService; userService.getUserAvatar(1001);在 Spring Boot 2.x 默认使用 CGLIB 的情况下这段代码是可以正常工作的因为 CGLIB 代理子类可以强转成UserServiceImpl。但在 Spring 5 之前如果该目标类实现了接口且没有开启proxy-target-classtrueSpring 会选择 JDK 动态代理代理对象只实现了UserService接口把它注入到UserServiceImpl类型的字段中就会直接报ClassCastException。排查链路看异常栈找到ClassCastException: $Proxy60 cannot be cast to com.xxx.service.UserServiceImpl。看到$Proxy开头的类名百分百是 JDK 动态代理生成的代理对象。回想 Spring 版本与代理策略判断是 JDK 代理还是 CGLIB。解决方式要么改为注入接口类型UserService要么配置spring.aop.proxy-target-classtrue强制使用 CGLIB。这里有个非常实用的经验如果你在任何日志或异常栈里看到$Proxy开头的类名说明对象是 JDK 动态代理看到EnhancerByCGLIB开头的类名说明对象是 CGLIB 动态代理。这个识别技巧能直接帮你定位大量疑似“类型错误”的问题。6.3 final 方法静默失效没有报错但代理就是不生效这个案例前面已经提到了。我在封装一个通用的缓存组件时给目标类加了 CGLIB 代理结果发现有部分方法没有命中缓存。排查了很久最后才发现这些方法全部带有 final 修饰符。关键点在于CGLIB 代理 final 方法时不会报错、不会抛异常它只是静默地不进行任何增强直接走原始方法逻辑。这种“静默失效”比报错更难排查因为你不会第一时间想到是代理的问题。排查策略对目标类里的方法做一次扫描看哪些方法被 final 修饰。给 CGLIB 代理的对象方法做增强之前先确认目标类的可继承性。如果目标类里 final 方法特别多而且都需要增强建议改用 JDK 动态代理 接口方式或者调整设计。6.4 另一个总被忽视的问题代理对象的 getClass() 已经变了有时候你在代理对象上调用obj.getClass()拿到的并不是目标类的 Class 对象而是代理类自己的 Class。这在做对象序列化、类型判断、Map 结构转换时容易引发隐蔽的 bug。比如OrderServiceImpl original new OrderServiceImpl(); OrderServiceImpl proxy (OrderServiceImpl) CglibProxyFactory.createProxy(OrderServiceImpl.class); System.out.println(original.getClass().getName()); // com.example.OrderServiceImpl System.out.println(proxy.getClass().getName()); // com.example.OrderServiceImpl$$EnhancerByCGLIB$$a1b2c3d4如果你在代码里用clazz OrderServiceImpl.class来做过某些类型分支判断代理对象会直接走进这个分支之外的路。类似的还有通过getClass().getAnnotation(...)获取注解信息在代理对象上获取到的注解可能是空的因为注解没有被继承到子类中。解决方式在系统设计时要意识到“对象可能是代理对象”这件事。需要获取真实类型时可以借助 Spring 提供的工具类AopProxyUtils.getSingletonTarget(obj)或AopUtils.getTargetClass(obj)来剥离代理层。自己封装框架时尽量避免依赖getClass()做精确类型判断。6.5 识别代理对象的快速技巧最后分享一个排查代理问题的实用小技巧。当你怀疑某个对象是代理对象时最快的确认方式就是输出它的类名System.out.println(userService.getClass().getName());看输出结果包含$Proxy字样 → JDK 动态代理包含$$EnhancerByCGLIB$$字样 → CGLIB 动态代理不是这两种 → 大概率是原始对象这个方法对定位 Spring 事务失效、AOP 切面不生效、类型转换异常这几类问题比逐个检查配置高效得多。我在项目里教了组里的新人这个方法之后他们自己排查代理相关 bug 的效率提高了非常多。7. 最后再说点实际的从我自己的体会来说代理模式的本质就是“拦截”二字。理解了这一点你再看 Spring AOP、MyBatis Mapper、事务传播机制、异步调用 Async 的实现逻辑思路一下子就通了。它们做的事情本质上都是对某个方法调用进行拦截在调用前后插入通用逻辑然后继续执行原本的业务代码。静态代理让你明白拦截这个概念是怎么手写实现的JDK 动态代理让你学会用一套通用代码替代无数个繁琐的代理类CGLIB 动态代理帮你摆脱接口的限制。三者各有各的适用场景没有绝对优劣。如果你正在学习这块的内容我建议你动手把文中的几个例子全部跑一遍再顺手打印出代理类的 class 文件反编译看一眼比死记硬背十遍概念都管用。如果是在项目中遇到了代理相关的诡异问题先别急着改代码用getClass().getName()确认一下你拿到的到底是不是代理对象再去分析失效原因往往能少走很多弯路。
返回列表