设计模式 07 · 代理模式

发布时间:2026/9/24 19:28:07

设计模式 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的自调用失效坑,根源就在代理机制。记住代理和装饰器结构相同、意图相反——代理管控制,装饰器管增强。下一篇我们就正式讲装饰器模式,看它如何像洋葱一样层层包裹,给对象动态叠加职责。
延伸阅读

更多相关文章

2026/9/21 9:43:09

Reia数据库架构揭秘:Turso如何支持离线与在线数据同步

Reia数据库架构揭秘:Turso如何支持离线与在线数据同步 【免费下载链接】Reia RPG game action-adventure MMO built with Godot and Rust. 项目地址: https://gitcode.com/gh_mirrors/re/Reia Reia作为一款基于Godot和Rust构建的动作冒险MMORPG游戏&#xff…

2026/9/23 8:14:48

深入解析信号量:从并发编程基石到生产者-消费者实战

1. 项目概述:从“抢车位”到“信号量”的认知跃迁如果你写过并发程序,或者调试过多线程的bug,那你一定对“竞态条件”这个词深恶痛绝。想象一个经典的场景:一个公共停车场,只有10个车位。如果没有管理,会发…

2026/9/22 6:25:15

PyTorch模型搭建与训练全流程实战指南

1. PyTorch模型搭建基础认知PyTorch作为当前最受欢迎的深度学习框架之一,其动态计算图特性让模型搭建变得像搭积木一样直观。我仍记得第一次用nn.Module构建神经网络时那种"原来如此"的顿悟感——相比其他框架的静态图设计,PyTorch允许我们在运…

2026/9/24 19:26:52

从YOLOV5目录格式到猪圈目标检测:数据准备实战指南

简介:猪圈摄像头场景下的生猪检测数据集,类别为单一pig,按YOLOV5标准目录格式整理,可直接接入现有训练流程,减少数据预处理工作。图像均截取自猪圈监控视频,分辨率覆盖640至1080,每帧包含多个生…

2026/9/24 19:26:52

从零手写线性回归:原理、梯度下降与工程实践完全指南

从零实现一个线性回归模型,是我觉得入门机器学习最值得做的一件小事。很多人一开始就扎进复杂的神经网络、Transformer,结果被概念和数学砸得晕头转向,反而连最基本的“模型是怎么学习”的都没搞明白。这个项目标题里的“性回归模型”&#x…

2026/9/24 19:26:52

JavaWeb图书管理系统源码部署与二次开发实战指南

简介:这套JavaWeb图书管理系统完整项目包,定位于课程设计与期末大作业场景,覆盖图书的查询、借阅、归还等核心功能,既适合JavaWeb初学者对照学习,也可作为二次开发基底。项目共278个文件,其中Java源文件45个…

2026/9/24 19:26:52

Multi-Agent 编排实战:SubAgent 调度、等待机制与框架选型

最近在调 Multi-Agent 系统的时候,正好赶上圈里在刷“cursor waiting for subagent”这个状态提示。很多人一看 waiting 就以为卡死了,其实它背后是一整套 SubAgent 调度过程,只是产品层把它简化成了一个转圈图标。我一度也在 Microsoft Agen…

2026/9/24 19:26:52

前端开发者必补的后端与部署技能选型指南

1. 前端开发者为什么必须补上后端与部署这一课做了六年前端,我越来越强烈地感受到一个事实:只会写页面的人,正在被快速边缘化。这不是贩卖焦虑,而是我这两年带团队、面人、接私活的真实体感。以前一个项目,前端切完图交…

2026/9/24 19:21:52

热点新闻推荐系统毕设全栈实战:Django+Vue+深度学习

每年毕业季,我都会遇到一批被“推荐系统”这个题目吸引的同学,热点新闻推荐系统又是其中最常见的一款。选这个题的人多,但真正能把它做明白的不多——多数人不是不会调模型,而是不知道整个项目该怎么闭环:数据从哪来、…

2026/9/23 12:07:00

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/23 12:06:55

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/24 0:00:21

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:21

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:21

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/22 16:34:32

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/22 20:01:30

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/22 13:25:41

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码