公司动态

设计模式 07 · 代理模式

📅 2026/8/5 21:43:45
设计模式 07 · 代理模式
从这一篇开始,我们离开对象怎么造出来(创建型),进入结构型模式——它们关心的是另一件事:已经造好的类和对象,该如何组合、连接、搭配,才能既满足需求又保持松耦合。结构型七个模式,几乎都是在玩包装和组合的艺术。我们从其中实战出镜率最高的一个开场——代理模式(Proxy)。它是 Spring AOP 的底层基石,几乎每个用过 Spring 的人都在享受它的红利,却未必知道它的原理。代理的思想在生活里随处可见:你买房不直接对接房东,而是找中介;明星不亲自谈商演,而是通过经纪人。中介和经纪人,就是代理——他们和本人对外提供同样的能力(都能谈房、谈商演),但你和他们打交道,而不是和本人;他们还能在这个过程里加一些本人不方便做的事(筛选、议价、收中介费)。代理模式在代码里干的是一模一样的事:给某个对象找一个替身,外界通过替身来访问真实对象,替身则可以在调用真实对象前后,插入一些额外的动作——记日志、做权限校验、加缓存、开事务。这一篇我们就从给订单服务加一圈横切逻辑这个真实需求出发,一步步走过代理的三种形态:手写的静态代理 → 运行时生成的 JDK 动态代理 → 基于继承的 CGLIB,讲清它们各自的原理、局限和取舍,最后落到 Spring AOP 到底用了哪个、怎么选的。这篇文章按这条线索展开:先说清代理要解决什么问题、它的核心结构;再手写一个静态代理,看它怎么把日志逻辑和业务分开;接着指出静态代理一个接口配一个代理类的类爆炸天花板;然后引出JDK 动态代理——运行时凭空生成代理,一套逻辑通吃所有接口;再看它只能代理接口的局限,以及CGLIB如何用继承补上这一环;最后对比三者、落到 Spring AOP,并划清代理和下一篇装饰器的界限。贯穿例是给OrderService加日志、权限、缓存。目录代理要解决什么:给对象找个替身静态代理:手写一个替身静态代理的天花板:类爆炸JDK 动态代理:运行时凭空生成CGLIB:连接口都不要,直接继承三种代理对比,与 Spring AOP代理 vs 装饰器,以及现实身影一、代理要解决什么:给对象找个替身先看需求。我们有一个订单服务,业务逻辑很纯粹:publicinterfaceOrderService{voidcreateOrder(longuserId);}publicclassOrderServiceImplimplementsOrderService{publicvoidcreateOrder(longuserId){System.out.println(创建订单,用户:userId);// 纯业务逻辑}}现在产品提了一堆附加要求:每次下单前后要打日志、下单前要校验用户有没有权限、高频查询要加缓存、涉及金额要开事务。这些要求有个共同特点——它们都不是订单业务本身,而是横切在业务外围的通用关注点(专业叫法是 cross-cutting concern,横切关注点)。最粗暴的做法,是把这些逻辑直接塞进createOrder:publicvoidcreateOrder(longuserId){System.out.println([日志] 开始下单);// 混入日志if(!hasPermission(userId))return;// 混入权限System.out.println(创建订单,用户:userId);// 真正的业务System.out.println([日志] 下单完成);// 又是日志}这就把第一篇批过的毛病全犯了:业务逻辑和横切逻辑搅在一起(违反单一职责),每个方法都要抄一遍日志和权限代码(大量重复),而且想给别的服务也加同样的逻辑,还得再抄。我们需要的是:在完全不动OrderServiceImpl业务代码的前提下,给它套上一圈横切逻辑。这正是代理的使命。代理模式的结构非常简单,三个角色:抽象主题(Subject):真实对象和代理共同的接口(OrderService),保证两者对外长得一样;真实主题(RealSubject):干实事的那个(OrderServiceImpl);代理(Proxy):持有真实对象的引用,对外实现同样的接口,在转发调用的前后插入额外逻辑。关键就在那句对外实现同样的接口——因为代理和真实对象长得一模一样,所以调用方拿到代理时,根本感觉不到自己面对的是替身,可以无缝替换。这也是代理能偷偷加逻辑的前提。二、静态代理:手写一个替身最直接的实现,是手写一个代理类,让它也实现OrderService接口,内部持有真实对象,在调用前后加上横切逻辑。这叫静态代理——静态是指这个代理类在编译期就由你写好、确定下来了。publicclassOrderServiceProxyimplementsOrderService{privatefinalOrderServicetarget;// 持有真实对象publicOrderServiceProxy(OrderServicetarget){this.targettarget;}OverridepublicvoidcreateOrder(longuserId){// —— 调用前的横切逻辑 ——System.out.println([日志] 开始下单,用户:userId);longstartSystem.currentTimeMillis();target.createOrder(userId);// 转发给真实对象干活// —— 调用后的横切逻辑 ——System.out.println([日志] 下单完成,耗时:(System.currentTimeMillis()-start)ms);}}用起来,调用方拿到的是代理,但因为类型都是OrderService,它浑然不觉:OrderServiceservicenewOrderServiceProxy(newOrderServiceImpl());service.createOrder(1001L);// 自动带上了日志和耗时统计这一步的收益很实在:OrderServiceImpl一个字没改,日志逻辑却加上了,而且业务和日志彻底分离——业务归业务类,横切归代理类,各司其职。这正是单一职责和开闭原则的体现:要加横切逻辑,不改原类,而是加一个代理。静态代理清晰、直观,在代理类不多的时候很好用。但它有一个随着系统变大就会暴露的硬伤。三、静态代理的天花板:类爆炸想象一下,系统里不止OrderService,还有UserService、ProductService、PaymentService……几十个 service。如果都想加上日志 耗时统计这套逻辑,用静态代理意味着什么?你得为每一个接口,手写一个几乎一模一样的代理类:OrderServiceProxy、UserServiceProxy、ProductServiceProxy……每个代理类里,那段前面打日志、中间转发、后面记耗时的代码几乎完全重复,只是转发的目标和方法不同。这就是静态代理的两个致命问题:类爆炸:有多少个要代理的接口,就得写多少个代理类。几十个 service 就是几十个代理类,全是重复样板。难以维护:哪天日志格式要改一下,你得挨个去改几十个代理类,一个都不能漏。更麻烦的是,一个接口如果有多个方法,代理类里每个方法都要写一遍转发和横切逻辑,重复进一步放大。静态代理的根本局限在于:代理类必须在编译期就为每一个具体接口写死。而打日志、记耗时这套逻辑其实和具体是哪个接口、哪个方法毫无关系——它是通用的。我们真正想要的是:把这套通用的横切逻辑只写一次,让它能自动应用到任意接口、任意方法上。能做到这一点吗?能——但代理类不能再由我们手写了,得让程序在运行时自动生成。这就是动态代理。四、JDK 动态代理:运行时凭空生成动态代理的核心思想:不再手写代理类,而是在程序运行时,由 JVM 动态地、凭空地生成一个代理对象。你只需要把要插入的横切逻辑写在一个地方,剩下的生成代理类、实现接口、转发调用全交给 JDK 自动完成。JDK 自带的动态代理,靠两个核心 API:InvocationHandler接口和Proxy类。第一步,把横切逻辑写进一个InvocationHandler。它只有一个invoke方法,所有被代理的方法调用,最终都会汇集到这个invoke里来:publicclassLogHandlerimplementsInvocationHandler{privatefinalObjecttarget;// 真实对象,注意是 Object,不限定类型publicLogHandler(Objecttarget){this.targettarget;}// 对代理对象的任何方法调用,都会转到这里OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{// —— 前置横切逻辑(只写这一次!)——System.out.println([日志] 调用方法:method.getName());longstartSystem.currentTimeMillis();Objectresultmethod.invoke(target,args);// 反射调用真实对象的方法// —— 后置横切逻辑 ——System.out.println([日志] method.getName() 耗时:(System.currentTimeMillis()-start)ms);returnresult;}}第二步,用Proxy.newProxyInstance在运行时生成代理对象:OrderServicetargetnewOrderServiceImpl();OrderServiceproxy(OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader(),// 类加载器target.getClass().getInterfaces(),// 要实现哪些接口newLogHandler(target));// 横切逻辑proxy.createOrder(1001L);// 走的是动态生成的代理,自动带日志关键的飞跃在这里:同一个LogHandler,可以套在任何对象上。因为它持有的是Object target、通过反射的Method来调用,完全不关心具体是哪个接口。给UserService、ProductService加日志?一行newProxyInstance换个 target 就行,再也不用为每个接口手写代理类了。第三节那个类爆炸问题,被彻底解决——横切逻辑只写一次,通吃所有接口。那Proxy.newProxyInstance到底做了什么?它在运行时动态生成了一个类(名字通常形如$Proxy0),这个类实现了你指定的那些接口,每个方法内部都转调你的handler.invoke()。这个类是 JVM 在内存里现造出来的,编译期根本不存在。这正是动态二字的含义,也是它比静态代理强大的根本原因。不过,细心的你可能注意到了那行target.getClass().getInterfaces()——JDK 动态代理是基于接口的。这既是它的机制,也是它的局限。五、CGLIB:连接口都不要,直接继承JDK 动态代理有一个硬性前提:被代理的类必须实现了接口。因为它生成的代理类,是靠实现和真实对象相同的接口来伪装成真实对象的。如果你有一个类压根没实现任何接口,只是个光秃秃的类,JDK 动态代理就无能为力了。可现实中确实有很多类没有接口。这时候就轮到CGLIB(Code Generation Library)登场了。它换了一个思路:既然不能靠实现接口来伪装,那就靠继承——动态生成一个目标类的子类,当作代理。// CGLIB:通过继承目标类来生成代理,不需要接口EnhancerenhancernewEnhancer();enhancer.setSuperclass(OrderServiceImpl.class);// 代理类 目标类的子类enhancer.setCallback(newMethodInterceptor(){OverridepublicObjectintercept(Objectobj,Methodmethod,Object[]args,MethodProxyproxy)throwsThrowable{System.out.println([日志] 调用:method.getName());Objectresultproxy.invokeSuper(obj,args);// 调用父类(即原方法)System.out.println([日志] 完成);returnresult;}});OrderServiceImplproxy(OrderServiceImpl)enhancer.create();proxy.createOrder(1001L);CGLIB 动态生成的代理类,是目标类的一个子类,它重写了父类的方法,在重写的方法里插入横切逻辑、再通过invokeSuper调用父类的原始实现。因为是子类,所以它天然就是目标类的一种(is-a),同样能无缝替换,而且完全不需要目标类实现任何接口。但靠继承也带来了它自己的局限,根源是 Java 继承的规则:无法代理final类:final类不能被继承,CGLIB 生成子类的路子直接断了;无法代理final方法:final方法不能被重写,这些方法上的横切逻辑就加不上。所以 JDK 动态代理和 CGLIB 恰好互补:一个靠实现接口(要求有接口),一个靠继承子类(要求可被继承)。到这里,代理的三种形态就集齐了,该做个总对比,并看看 Spring 是怎么用它们的。六、三种代理对比,与 Spring AOP把三种代理放在一起,一张表看清:静态代理JDK 动态代理CGLIB代理类产生时机编译期,手写运行时,JVM 生成运行时,生成子类实现机制手写实现接口反射 实现接口继承(生成子类)是否要求接口要(要么接口要么继承)必须有接口不要求接口主要局限类爆炸、难维护只能代理接口方法不能代理 final 类/方法适用代理对象很少、逻辑简单目标有接口目标无接口用一张图把这三者的关系和演进串起来看最清楚:现在回到最实际的问题:Spring AOP 用的是哪种?答案是——两种都用,按情况自动选择。Spring AOP 的默认策略是:如果目标类实现了接口,默认用JDK 动态代理;如果目标类没有实现接口,就用CGLIB(生成子类)。这套横切逻辑在 Spring AOP 里有个专门的名字叫通知(Advice),而在哪些方法上织入由切点(Pointcut)决定。你平时用的Transactional(声明式事务)、Cacheable(声明式缓存)、Async(异步)、以及各种自定义注解拦截,底层全都是动态代理在干活:Spring 在启动时为你的 Bean 生成一个代理,把事务的开启/提交、缓存的读写、日志记录等逻辑,以代理的方式织入到方法调用的前后。一个极其常见的坑:正因为 Spring 用的是代理,所以Transactional、Cacheable这类注解有个著名的自调用失效问题——在同一个类里,方法 A 直接调用本类的方法 B(this.B()),这个调用不经过代理对象,而是直接走了原始对象的this,于是 B 上的事务/缓存注解不生效。理解了代理是一个套在外面的替身对象、只有经过它的调用才会被增强,这个坑就一目了然了。解决办法是让调用重新经过代理(比如注入自己、或拆到另一个 Bean)。这个坑几乎每个 Spring 开发者都踩过,而它的根源就是这一篇讲的代理机制。七、代理 vs 装饰器,以及现实身影代理在整个技术栈里无处不在,认出它们:Spring AOP:如上,Transactional等全靠它,是代理最重要的工业级应用。MyBatis 的 Mapper:你只写了一个OrderMapper接口,从没写实现类,却能直接调用——因为 MyBatis 用 JDK 动态代理为接口生成了代理,在invoke里把方法调用翻译成 SQL 执行。RPC 框架(Dubbo、Feign):你调用一个远程服务接口,本地其实拿到的是个代理,它在invoke里把调用序列化、发网络请求、再把结果返回,让远程调用看起来像本地调用。java.lang.reflect.Proxy:JDK 动态代理的官方入口,前面用过。按代理的用途,还能细分出几个经典变体(它们结构相同,目的不同):远程代理(RPC,代理远端对象)、虚拟代理(延迟加载,访问时才真正创建重对象,如懒加载图片)、保护代理(权限控制,校验通过才转发)、缓存代理(缓存结果)。它们全是同一套代理结构的不同应用。最后,划清一条极易混淆的界限——代理 vs 装饰器(下一篇的主角)。这两个模式的代码结构几乎一模一样:都实现同一接口、都持有一个被包装对象、都在转发前后加逻辑。区别不在结构,而在意图:代理:目的是控制访问。代理通常自己决定要不要、以及如何访问真实对象(比如权限不够就直接不转发),它和真实对象是替身关系,代理替你管着真实对象。你甚至可能不知道真实对象的存在。装饰器:目的是增强功能。装饰器是由外部把被包装对象传进来,层层叠加新能力,你很清楚自己在一层层地包装。它和被包装对象是增强关系。一句话记:代理重在控制(管你能不能访问、怎么访问),装饰器重在增强(给你叠加新功能)。结构相似,意图相反,这也是为什么它们被分成两个模式。下一篇讲装饰器时,我们会从另一头把这条界限再夯实一遍。小结。代理模式给对象找一个替身,让外界通过替身访问真实对象,从而在不改动真实对象的前提下,把日志、权限、缓存、事务这些横切逻辑织入进来。它有三种形态:静态代理手写清晰但会类爆炸;JDK 动态代理在运行时基于接口凭空生成代理,一套InvocationHandler通吃所有接口,解决了类爆炸,但只能代理有接口的类;CGLIB改用继承生成子类,补上了无接口的场景,代价是不能代理final。Spring AOP 正是有接口用 JDK、无接口用 CGLIB地把这两者用到了极致,而Transactional的自调用失效坑,根源就在代理机制。记住代理和装饰器结构相同、意图相反——代理管控制,装饰器管增强。下一篇我们就正式讲装饰器模式,看它如何像洋葱一样层层包裹,给对象动态叠加职责。