ARTICLE DETAIL

资讯详情

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

Java代理模式深度解析:从静态代理到JDK动态代理与CGLIB实战

Java代理模式深度解析:从静态代理到JDK动态代理与CGLIB实战 1. 代理模式不是“包装一下”那么简单先看它解决什么问题1.1 一个没有代理的代码职责全搅在一起如果你去看Spring AOP的源码会发现一个有意思的现象Spring容器里很多Bean并不是你写的那个类而是它的代理。有人觉得这是框架“搞鬼”其实背后的理论基础正是Java设计模式里的代理模式。代理模式也是我平时项目里用得最多、看源码时绕不开的一个模式很多经典问题比如“为什么Transactional有时候会失效”“MyBatis的Mapper接口没有实现类却能被调用”“RestController里注入的Service到底是个什么东西”归根到底都和代理模式有关。我先举一个最常见的反面例子。假设你有一个支付接口现在需要给支付方法加上权限校验、日志记录、事务控制。public class PayService { public void pay(String orderId) { // 权限校验 if (!checkPermission()) { throw new SecurityException(无权限); } try { // 开启事务 System.out.println(开始支付 orderId); // 执行业务 // 提交事务 } catch (Exception e) { // 回滚事务 } // 记录日志 System.out.println(支付完成 orderId); } }这段代码的问题很明显业务逻辑和横切逻辑全缠在一起。今天要在支付方法里加日志明天要在退款方法里加日志后天可能还要给优惠券方法加权限校验。如果每个方法都复制粘贴一遍代码很快就失控了改一处增强逻辑要动所有方法完全违背了单一职责原则。代理模式的想法很朴素把“业务行为”和“增强行为”拆开。业务方法只负责业务权限校验、日志、事务这些事交给代理对象做。调用方只需要面向同一个接口编程代理在方法前后插入增强逻辑再调用真实对象。1.2 代理模式的三要素和调用链代理模式有三个角色角色职责抽象主题Subject定义真实对象和代理对象共同的行为接口真实主题RealSubject真正执行业务逻辑的对象代理Proxy持有真实主题引用对外暴露相同接口拦截并增强方法调用调用过程是客户端调用proxy.method()代理先执行增强逻辑然后调用realSubject.method()。如果不需要增强代理可以直接把请求转发给真实对象如果确实要拦截代理可以在调用前后做各种事情。用一个生活化的类比明星不会直接接粉丝的电话和商业合作经纪人站在中间先过滤骚扰、谈合同、排档期最后才让明星上台演出。经纪人就是代理明星是真实对象商业合作是接口定义的行为。粉丝不需要直接找明星只需要对接经纪人体验上没差别但中间多了很多控制空间。1.3 代理模式和装饰器模式到底有什么区别很多初学者会把代理模式和装饰器模式搞混因为从类图上看两者几乎一模一样都是持有一个目标对象都实现相同的接口都在方法调用前后做一层包装。我的理解是这样装饰器模式的重点是“动态增加职责”比如给一杯咖啡加奶、加糖外层装饰器一层层套上去每一层都增强了原有行为代理模式的重点是“控制访问”比如权限校验、延迟加载、远程调用代理往往是想隐藏真实对象的存在不让客户端直接触达。一句话总结装饰器让对象“变得更丰富”代理让访问“变得更可控”。代码结构可以长得一样但设计意图不同。面试时如果被问到能说出意图差异比背类图有用得多。2. 静态代理最笨的方式反而最能讲清代理的本质2.1 一个最简代码接口、真实主题、代理类动态代理虽然强大但我始终建议先手写一个静态代理。原因很简单只有亲手写过代理类你才会真正理解“代理对象持有真实对象引用并把调用转发过去”这件事到底是怎么回事。先定义一个接口public interface UserService { void addUser(String username); }真实业务类public class UserServiceImpl implements UserService { Override public void addUser(String username) { System.out.println(保存用户 username); } }代理类public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target target; } Override public void addUser(String username) { System.out.println(代理新增用户前校验权限); target.addUser(username); System.out.println(代理新增用户后记录日志); } }使用方式UserService target new UserServiceImpl(); UserService proxy new UserServiceProxy(target); proxy.addUser(张三);运行结果会多出两行代理增强日志。这个例子虽然简单但把代理模式的核心结构全部体现出来了代理类和真实类实现同一个接口代理类通过构造方法拿到真实对象调用方不直接操作UserServiceImpl而是操作UserServiceProxy。2.2 为什么用组合而不是继承看到这里你可能想问代理类为什么不直接继承UserServiceImpl然后重写addUser方法继承确实也能达到类似效果但在设计上是有问题的。继承会绑定父类的具体实现。如果父类方法变了子类的覆写逻辑可能受到牵连。代理对象不应被限制成只能代理某一个具体实现类。使用接口加组合的方式UserServiceProxy可以代理任何一个实现了UserService的类今天可以代理UserServiceImpl明天换一个VipUserServiceImpl代理类不用改。组合的方式让代理类只依赖抽象接口不依赖具体细节符合依赖倒置原则。这也是我在实际开发里更推荐“组合优于继承”的原因。很多框架底层的代理机制本质上也是在运行时动态组合一个代理类而不是简单继承业务类去做覆写。2.3 静态代理的致命伤静态代理最大的问题就是“代码爆炸”。如果UserService接口里有20个方法代理类就要写20个转发方法如果系统里有10个业务接口都需要日志增强就要写10个代理类如果突然想在所有代理方法上再加一个耗时统计你就要把这10个代理类全部改一遍。这比直接把逻辑写在业务类里也好不了多少。静态代理适合理解原理但在真实业务中我更推荐使用动态代理。动态代理的精髓在于把“增强逻辑”抽成一个独立对象让它在运行时统一处理所有方法这样不管接口有多少方法代码只需要维护一份。3. JDK动态代理InvocationHandler把“增强”变成了可复用对象3.1 Proxy.newProxyInstance的三个参数JDK从1.3开始就内置了动态代理核心类是java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler。创建代理对象使用Proxy.newProxyInstance它需要三个参数UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInvocationHandler(target) );第一个参数ClassLoader用来加载运行时生成的代理类通常直接使用目标类的类加载器。第二个参数interfaces目标对象实现的接口数组运行时生成的代理类会实现这些接口这样调用方才能把它强转成接口类型。第三个参数InvocationHandler方法调用的统一处理者。代理对象上任何一个接口方法的调用都会转到这里来。最核心的就是这个InvocationHandler。你把增强逻辑写在它的invoke方法里就相当于把“代理行为”和“具体业务类”彻底解耦了。3.2 在invoke里做日志和权限控制先写一个最常用的日志代理import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.Arrays; 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() ] 开始执行参数 Arrays.toString(args)); long start System.currentTimeMillis(); Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println(方法 [ method.getName() ] 执行结束耗时 cost ms返回 result); return result; } }调用方式UserService target new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInvocationHandler(target) ); proxy.addUser(张三);这里有个容易被忽略的细节invoke方法里的method是接口方法的Method对象method.invoke(target, args)这行才是真正把调用转发给原始业务对象。如果你想在转发前加权限校验只需要在method.invoke之前写判断逻辑如果你想做重试只需要在捕获异常时再调用一次。换句话说所有接口方法都会被这个统一的invoke处理这就是动态代理能解决静态代理“代码爆炸”问题的根本原因。3.3 JDK动态代理为什么只能代理接口这是面试里被问烂了的问题也是理解JDK代理机制的关键点。JDK动态代理生成的代理类结构大致是这样// 反编译后的结构示意不是真实运行代码 public final class $Proxy0 extends Proxy implements UserService { private static Method m1; public final void addUser(String username) { super.h.invoke(this, m1, new Object[]{username}); } }可以看到$Proxy0已经继承了java.lang.reflect.Proxy类。Java是单继承语言一个类不可能同时继承Proxy又继承你的UserServiceImpl所以只能靠“实现接口”来给代理类定义方法。如果目标类没有实现任何接口JDK动态代理就无能为力了因为没有接口可以让$Proxy0去实现。这时候就需要CGLIB出场。3.4 一个真实场景用JDK动态代理做方法级幂等控制我去年在项目里做过一个通用的幂等组件就是基于JDK动态代理实现的。思路非常简单在接口方法上打一个Idempotent注解代理拦截到方法后先去Redis查请求号如果已经处理过就直接返回旧结果否则执行业务方法并把结果缓存起来。public class IdempotentInvocationHandler implements InvocationHandler { private final Object target; public IdempotentInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (!method.isAnnotationPresent(Idempotent.class)) { return method.invoke(target, args); } String requestId extractRequestId(args); Object cached getFromCache(requestId); if (cached ! null) { return cached; } Object result method.invoke(target, args); saveToCache(requestId, result); return result; } }这个组件的好处是业务类完全不知道幂等逻辑的存在只要加一个注解就能获得幂等能力。这正是代理模式最有价值的地方增强逻辑被抽出来复用业务代码保持干净。4. CGLIB代理没有接口时用继承绕过限制4.1 Enhancer和MethodInterceptor的基本用法CGLIBCode Generation Library不是JDK自带的但Spring框架内部集成了它。它的核心思路是直接在运行时生成目标类的子类通过覆写父类方法来实现代理所以不需要接口。单独使用CGLIB时需要引入依赖dependency groupIdcglib/groupId artifactIdcglib/artifactId version3.3.0/version /dependency看一个最简单的例子。假设有一个OrderService没有实现任何接口public class OrderService { public void createOrder(String orderNo) { System.out.println(创建订单 orderNo); } }使用Enhancer创建代理import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CglibProxyFactory { public static T T createProxy(T target) { Enhancer enhancer new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback(new MethodInterceptor() { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println(CGLIB拦截前); Object result proxy.invokeSuper(obj, args); System.out.println(CGLIB拦截后); return result; } }); return (T) enhancer.create(); } }使用OrderService target new OrderService(); OrderService proxy CglibProxyFactory.createProxy(target); proxy.createOrder(PO20250101);intercept方法里的四个参数要理解清楚objCGLIB生成的子类实例。method父类的Method对象。args方法入参。proxyCGLIB的MethodProxy用于调用父类方法。4.2 为什么CGLIB不能代理final方法这个坑我在刚接触CGLIB时踩过。CGLIB是生成一个目标类的子类然后重写父类的方法。如果目标类被final修饰或者某个方法被final修饰编译器根本不允许重写代理自然就失效了。具体现象是类如果是finalCGLIB会直接抛异常方法如果是finalCGLIB不会拦截这个方法调用时走的是父类原始逻辑没有任何增强。私有方法同样无法被代理因为子类看不到父类私有方法。所以在使用Spring AOP时如果要确保切面生效类和方法都不能是final方法还得是public或protected。这也是为什么很多人写Transactional加在private方法上不生效的原因之一。还有一点要注意在CGLIB的intercept中调用父类方法一定要用proxy.invokeSuper(obj, args)不要手动写method.invoke(obj, args)。因为obj是CGLIB生成的子类实例如果你用method.invoke(obj, args)这个obj上的方法可能是被覆写过的会再次进入intercept造成死循环。4.3 JDK代理和CGLIB该选谁对比项JDK动态代理CGLIB代理实现方式基于接口和反射基于继承和字节码生成目标要求目标类必须实现接口目标类不能被final修饰方法不能被final修饰生成机制Proxy.newProxyInstanceEnhancer.create创建成本较低相对较高Spring旧版默认策略有接口时使用无接口时使用Spring Boot 2.x默认策略已改为优先使用CGLIB已改为优先使用CGLIBSpring Boot 2.x开始官方把spring.aop.proxy-target-class默认值改成了true意思是即使目标类有接口默认也使用CGLIB代理。所以现在你直接注入一个Service很多时候拿到的其实是CGLIB创建的代理对象而不是JDK动态代理。日常开发中其实不需要你手动二选一Spring已经自动处理了。但面试时你得能说清楚JDK代理基于接口灵活且没有额外依赖CGLIB基于子类不需要接口但final方法和类无法代理。5. 框架源码里的代理模式Spring、MyBatis、Retrofit都在用它5.1 Spring AOP与声明式事务Spring AOP是整个Spring框架里代理模式最集中的体现。Transactional注解本身不做事真正起作用的是Spring在Bean初始化阶段判断这个类是否需要事务增强如果需要就生成一个代理对象放进容器。调用流程是Controller注入Service时实际上拿到的是代理对象调用service.method()时调用链会经过TransactionInterceptor它在方法执行前开启事务方法正常结束后提交事务方法抛出异常时回滚事务。这也引出了网上常见的“事务失效”问题。如果同一个类内部一个普通方法调用了另一个带Transactional的方法且调用发生在原始对象内部没有经过代理对象那么事务注解是无效的。给一段最能说明问题的代码Service public class OrderService { public void createOrder(String orderId) { // this调用没有经过代理 generateOrderNo(orderId); } Transactional public void generateOrderNo(String orderId) { System.out.println(该方法上的事务注解不会生效); } }外部调用createOrder时进入的是代理对象代理对象调用target.createOrder()但在createOrder内部this指向的是原始目标对象而不是代理对象所以generateOrderNo上面的Transactional完全被跳过。解决办法有三个把generateOrderNo拆到另一个Bean中通过注入这个Bean来调用。在OrderService里注入自己的代理对象比如通过Lazy注入OrderService用它来调用generateOrderNo。设置exposeProxytrue然后通过AopContext.currentProxy()拿到当前代理对象。我更推荐第一种方案拆类会让职责更清晰也避免依赖Spring内部机制。5.2 MyBatis为Mapper接口生成代理实现MyBatis里有一个非常经典的动态代理应用UserMapper只是一个接口没有任何实现类但你却能直接调用它的方法。public interface UserMapper { Select(SELECT * FROM user WHERE id #{id}) User findById(Long id); }调用的时候UserMapper mapper sqlSession.getMapper(UserMapper.class); User user mapper.findById(1L);底层就是MyBatis通过JDK动态代理为UserMapper生成一个代理对象。调用findById时MapperProxy这个InvocationHandler会解析接口方法上的注解或XML映射找到对应的MappedStatement最终执行SQL并返回结果。这就是代理模式的神奇之处你只定义了一个接口运行时动态代理替你实现了接口。所以说看到接口没有实现类不要觉得奇怪背后往往藏着一个代理。5.3 Retrofit和RPC框架的“无实现类”接口Java生态里这种套路非常多。Retrofit也是用动态代理把接口方法变成HTTP请求public interface ApiService { GET(user/list) CallUserList getUserList(); }Retrofit会为ApiService生成代理对象调用getUserList()时代理对象解析方法上的GET注解拼接URL构造OkHttp请求再返回结果。业务代码完全感知不到网络请求的细节。RPC框架更明显。Dubbo里DubboReference注入的接口代理本地调用一个空方法代理对象会负责把方法名、参数序列化通过网络发送给远程服务提供方再接收结果返回。调用远程服务就像调用本地方法一样这种体验正是代理模式带来的隔离能力。5.4 业务代码里什么时候真正需要代理框架已经替我们做了很多代理的事那业务代码里还需要自己写代理吗我的经验是当你有“同一套增强逻辑要应用到多个不相关的方法或类”时才值得考虑动态代理。比如统一日志、权限校验、方法级幂等、接口限流、失败重试。但更推荐的做法是在Spring生态里直接用AOP注解或切面而不是手写Proxy.newProxyInstance。因为Spring AOP已经封装好了创建代理、管理Bean生命周期、处理异常等复杂逻辑你只需要写清楚切面规则和增强逻辑。如果脱离Spring框架写一个纯Java的工具类自己用JDK动态代理或CGLIB做通用拦截那也很合适。我在之前的项目里用动态代理做过一个方法级幂等组件效果很好但前提是团队对代理原理有共识。如果只是为了让某一段代码少写几行强行引入动态代理反而会增加认知成本。代理模式是工具不是目的。6. 我踩过的代理失效的坑以及面试被追问的细节6.1 同类内部方法调用Transactional失效这个坑我在前文已经提到了但值得单独拿出来再讲一遍因为它是线上环境最容易出问题的一类Bug。举个更贴近业务的例子订单创建时需要扣库存、生成订单号、发通知。你可能会这样写Service public class OrderService { Transactional public void createOrder(OrderDTO dto) { this.deductStock(dto.getSkuId()); this.generateOrderNo(dto.getOrderId()); this.sendMessage(dto.getUserId()); } }表面看起来所有方法都在同一个事务里但只要你把deductStock、generateOrderNo、sendMessage这些方法挨个加上Transactional其实并不会让事务边界变得更正确。真正决定事务的是最外层createOrder这个方法是否经过代理对象。如果createOrder是入口方法并且它本身就有Transactional那问题不大但如果入口方法没有事务注解里面调用的方法虽然有注解也因为this调用全部失效。排查这类问题时我会先在调用链上打印对象的类名System.out.println(service.getClass().getName());如果输出的是com.example.OrderService说明没有走代理如果输出的是com.example.OrderService$$EnhancerBySpringCGLIB$$...说明走的是CGLIB代理。看到真实类名基本就能定位到内部调用问题。6.2 代理对象的类型判断容易翻车写框架或者公共组件时经常需要判断传入对象是不是某个类型。在存在代理的情况下这种判断会变得很微妙。JDK动态代理生成的$Proxy0并不是目标类的子类它只是实现了目标接口。所以如果你写if (target instanceof UserServiceImpl) { // do something }当target是JDK动态代理对象时这个判断会返回false因为你那个代理对象和UserServiceImpl没有继承关系。而如果你用的是CGLIB代理对象是目标类的子类instanceof UserServiceImpl又会返回true。这种不确定性容易导致框架代码表现不一致。所以我的建议是在公共逻辑里尽量基于接口或注解做判断不要依赖具体实现类。6.3 动态代理与Spring循环依赖的暗坑Spring解决循环依赖依赖三级缓存允许单例Bean在属性还没完全填充时就提前暴露。如果这个Bean还需要代理增强情况就会更复杂早期暴露出去的可能是一个还没完成增强的原始对象等后续创建完代理之后另一个Bean拿到的还是之前暴露的原始对象导致增强不生效。Spring Boot 2.6以后官方默认禁止了循环依赖遇到这种情况启动时会直接报错。如果你在升级Spring Boot时碰到“The dependencies of some of the beans in the application context form a cycle”不要想着怎么强行打开开关而是应该把循环依赖的代码结构拆掉这是更健康的做法。6.4 代理对象创建的成本和缓存JDK动态代理和CGLIB创建代理对象都有成本尤其是CGLIB运行时需要生成字节码成本更高。Spring里的Bean默认是单例所以创建一次代理对象的成本被分摊到了整个应用生命周期。如果你在业务代码里频繁创建代理对象比如在for循环里创建性能会明显变差。我见过有人把代理创建做成每次调用都重新生成完全没有必要。如果是自己手写动态代理应该把代理对象缓存起来或者在容器启动阶段创建好。一个代理对象通常绑定一个目标对象和一个InvocationHandler只要目标对象不变它就可以复用。6.5 面试常问的几个对比问题我把代理模式相关的面试问题整理成了一份清单方便你自查问题关键回答思路JDK动态代理和CGLIB有什么区别JDK代理必须是接口基于反射CGLIB不要求接口基于继承不能代理final类和方法。Spring AOP默认使用哪种代理旧版有接口用JDK无接口用CGLIBSpring Boot 2.x默认CGLIB。代理模式和装饰器模式的区别装饰器强调增强功能代理强调控制访问。Transactional为什么会失效同类内部调用、方法非public、类或方法为final、异常被吞掉等。代理模式和适配器模式的区别适配器是为了让不兼容的接口协同工作会修改接口定义代理不改变接口保持接口一致只做控制与转发。还有一个比较偏的追问Proxy.newProxyInstance生成的代理类在什么情况下会提前存在HotSpot会对相同ClassLoader、相同接口集合、相同InvocationHandler类型的代理类做缓存所以重复创建同一个接口的代理不一定每次都生成新的类。这个点知道即可工作中很少用到。最后说一点我个人的体会。学代理模式一定不要只盯着类图看最好去读一段框架源码。Spring的JdkDynamicAopProxy、MyBatis的MapperProxy代码都不长读完之后你对“代理对象、InvocationHandler、MethodProxy”这几个概念的理解会完全不一样。如果哪天你在业务里遇到“调用没生效”“事务时好时坏”这类问题先把对象类名打印出来看一眼大概率会发现是代理对象和真实对象在互相纠缠。代理模式就是这样理解它不需要死记定义跑通一个动态代理Demo再跟着框架源码走一遍就够了。
返回列表