走进 Gang of Four 设计模式:代理模式

📅 2026/7/17 5:41:59
走进 Gang of Four 设计模式:代理模式
走进 Gang of Four 设计模式代理模式说明不仅告诉你怎么用更告诉你为什么这样设计前置知识Java 面向对象基础接口、多态、静态代理与动态代理概念 设计模式分类总览GoF 23 种设计模式可按两个维度交叉分类类/对象处理方式 ×创建型/结构型/行为型目的。维度创建型Creational结构型Structural行为型Behavioral类Class通过继承复用Factory Method工厂方法Adapter类适配器Interpreter解释器Template Method模板方法对象Object通过组合/聚合复用Abstract Factory抽象工厂Builder建造者Prototype原型Singleton单例Adapter对象适配器Bridge桥接Composite组合Decorator装饰Facade外观Flyweight享元Proxy代理Chain of Resp.责任链Command命令Iterator迭代器Mediator中介者Memento备忘录Observer观察者State状态Strategy策略Visitor访问者本文档聚焦结构型-对象模式中的Proxy代理核心思想是通过引入一个中间层Proxy控制对真实对象的访问在不修改原始类的前提下添加额外逻辑。 目录模式概述 · 15 问深度分析Q1: 为什么需要解决了什么问题Q2: 不用会有什么缺陷Q3: 核心思想一句话Q4: 包含哪些角色Q5: 调用流程是怎样的Q6: 为什么这样设计Q7: 为什么用接口/抽象类/组合Q8: 符合哪些面向对象原则Q9: 优缺点与额外成本Q10: 适合/不该用的场景Q11: 与其他模式的区别Q12: 框架中的典型应用Q13: 最小可运行版本Q14: 识别重构信号Q15: 现代改进方案框架源码实战分析Spring AOP 代理机制MyBatis Mapper 动态代理Tomcat RequestFacade 安全代理深度追问Why / How / Trade-off / Evolution / Modern Practice总结参考文献1. 模式概述 · 15 问深度分析Q1: 为什么需要这个模式它解决了什么问题在软件开发中直接访问一个对象有时会带来问题。例如对象创建开销过大、需要进行权限控制、或者对象存在于远程机器上。代理模式解决的核心问题是在不改变原始类代码的前提下控制对该对象的访问并在访问前后添加额外的间接层控制逻辑。Q2: 如果不用这个模式会有什么缺陷不用代理模式你只能将非业务核心逻辑如日志记录、权限校验、事务管理、缓存等硬编码进业务类中。这会导致代码臃肿与耦合核心业务代码充斥着大量与业务无关的周边逻辑。违反单一职责原则一个类既要负责核心业务又要负责安全、日志和事务。难以扩展和复用每次你想给不同类加上相同的安全校验都得复制一遍代码。// ❌ 业务代码中混入了横切逻辑publicclassUserService{publicvoidcreateUser(Useruser){// 权限校验if(!SecurityContext.hasRole(ADMIN)){thrownewSecurityException(无权限);}// 日志记录log.info(开始创建用户...);// 事务开启TransactionManager.begin();try{// 核心业务逻辑userDao.insert(user);TransactionManager.commit();}catch(Exceptione){TransactionManager.rollback();throwe;}log.info(用户创建成功);}}Q3: 核心思想是什么一句话如何概括核心思想通过引入一个代理对象来间接访问目标对象从而在不修改目标对象的前提下控制访问并为其扩展功能。一句话概括“房东只管签合同出租找中介”—— 用一个替身来控制和增强对原对象的访问。Q4: 它包含哪些角色每个角色的职责是什么代理模式通常包含以下 3 个核心角色抽象主题Subject定义了真实主题和代理主题的共同接口。这样在任何使用真实主题的地方都可以用代理主题代替。真实主题Real Subject真正执行核心业务逻辑的对象是代理对象所代表的幕后真实对象。代理Proxy内部持有对真实主题的引用。它与真实主题实现了相同的接口因此可以替代真实主题。它负责在调用真实主题前后执行预处理和善后工作如权限判定、延迟加载、日志记录。Q5: 它们之间如何协作调用流程是怎样的调用流程如下客户端Client拿着抽象主题Subject的引用调用其方法。此时客户端实际调用的是代理Proxy。代理Proxy接收到请求先执行前置处理例如检查用户是否有权限或者判断真实对象是否需要初始化。代理Proxy调用**真实主题Real Subject**的对应方法执行核心业务。代理Proxy接收到真实主题的返回值执行后置处理例如记录日志、提交事务、缓存结果。代理Proxy将最终结果返回给客户端。Q6: 为什么要这样设计每个角色存在的意义是什么抽象主题Subject存在的意义保证了透明性。对客户端而言它不知道也不关心自己拿到的是真身还是代理两者的契约接口完全一致这极大地降低了系统的耦合度。真实主题Real Subject存在的意义专注核心业务逻辑。它不需要感知任何诸如网络传输、权限控制或日志统计的外部环境保持纯粹。代理Proxy存在的意义充当挡箭牌和打杂工。它屏蔽了直接访问真身带来的副作用承接了所有横切关注点Cross-cutting Concerns。Q7: 为什么使用接口、抽象类、组合而不是其他方式为什么用接口/抽象类继承契约为了实现多态。只有当代理和真实对象拥有相同的外壳接口时客户端才能无感知地进行切换。为什么用组合内部持有真实对象而不是让代理类直接继承真实类继承是静态的编译期决定而组合是动态的运行期决定。如果使用继承每个真实类都需要写一个继承它的代理类会导致类爆炸。通过组合一个代理类甚至可以代理一类实现了相同接口的不同真实对象尤其是动态代理。Q8: 这种设计符合哪些面向对象原则开闭原则OCP当你需要增加新的控制逻辑如新增缓存功能时不需要修改真实主题的代码只需要新建一个代理类或者扩展代理逻辑即可。单一职责原则SRP真实主题只负责核心业务代理类负责控制和周边外围逻辑各自职责单一。依赖倒置原则DIP客户端和代理类都依赖于抽象接口Subject而不是依赖于具体的实现类Real Subject。Q9: 它有哪些优点和局限性会带来哪些额外成本优点能协调调用者和被调用者在一定程度上降低了系统的耦合度。可以在不修改原始代码的情况下动态地切入新功能。远程代理可以隐藏对象存在于不同地址空间的事实虚拟代理可以优化性能延迟加载。局限性/额外成本性能损耗增加了一层间接调用请求处理速度可能会稍微变慢。复杂度增加增加了类的数量静态代理或者让代码的调用链路变长增加了调试和理解的成本尤其是多层动态代理。Q10: 它最适合哪些场景哪些场景不应该使用适合使用的场景虚拟代理Virtual Proxy用于大对象的延迟加载Lazy Loading比如网页中先用一张轻量级占位图代替未加载完的大图。保护代理Protection Proxy控制不同用户对真实对象的访问权限。远程代理Remote Proxy为一个位于不同地址空间如微服务、RPC 调用的对象提供局部代表如 Dubbo 的 Stub。智能代理Smart Proxy在方法前后统一加日志、统计耗时或开启/提交事务。不应该使用的场景如果系统调用链路已经非常冗长且性能敏感不应滥用代理。如果扩展功能只需要简单地在真身内部改动一行代码且该类未来几乎不会再扩展其他外围功能引入代理属于过度设计。Q11: 它与容易混淆的其他设计模式有什么区别和联系装饰器模式Decorator vs 代理模式目的不同装饰器关注于增强功能动态地为对象添加职责代理关注于控制访问它往往对客户端隐瞒真实对象的细节。适配器模式Adapter vs 代理模式接口不同适配器会改变接口把 A 接口转成 B 接口代理模式严格保持接口一致。外观模式Facade vs 代理模式粒度不同外观模式为一组子系统定义一个高层组合接口一对多代理模式代表的是单个对象一对一。Q12: Java/JDK、Spring、MyBatis、Tomcat 等框架中有哪些典型应用JDKjava.lang.reflect.Proxy提供了动态代理的底层实现Collections.unmodifiableList()返回一个只读的代理列表当你调用add()时会抛出异常保护代理。SpringSpring AOP面向切面编程的基石就是代理模式。它通过 JDK 动态代理或 CGLIB 动态代理实现了Transactional事务控制和日志切面。MyBatisMapper 接口如UserMapper并没有实现类MyBatis 内部使用 JDK 动态代理为这些接口生成了代理对象拦截方法调用并将其转化为执行 SQL。TomcatRequest 和 Response 对象在传入 Servlet 时往往会被包装成RequestFacade和ResponseFacade外观/代理结合体防止开发者通过强转接触到 Tomcat 容器底层的核心Request对象确保安全。Q13: 如何从零实现一个最小可运行版本以图片懒加载为例// SubjectpublicinterfaceImage{voiddisplay();}// RealSubjectpublicclassRealImageimplementsImage{privateStringfileName;publicRealImage(StringfileName){this.fileNamefileName;loadFromDisk(fileName);}privatevoidloadFromDisk(StringfileName){System.out.println(Loading fileName);}Overridepublicvoiddisplay(){System.out.println(Displaying fileName);}}// ProxypublicclassProxyImageimplementsImage{privateRealImagerealImage;privateStringfileName;publicProxyImage(StringfileName){this.fileNamefileName;// 注意构造时不加载图片}Overridepublicvoiddisplay(){// 延迟加载只在第一次调用 display() 时创建 RealImageif(realImagenull){realImagenewRealImage(fileName);}realImage.display();}}// ClientpublicclassProxyPatternDemo{publicstaticvoidmain(String[]args){ImageimagenewProxyImage(test_10mb.jpg);System.out.println(Image created, but not yet loaded.);// 第一次调用 display()才真正加载image.display();System.out.println();// 第二次调用不需要再加载直接显示image.display();}}Q14: 如何在现有项目中识别可以使用该模式的地方如果代码中频繁出现在执行核心逻辑之前先做某件事的模式如先检查权限再执行、先加载缓存再执行、先记录日志再执行说明需要引入代理模式将这些横切关注点剥离。典型信号业务类中充斥着log.info(start)→ 核心逻辑 →log.info(end)的三段式代码。业务类中每次方法调用前都有if (!hasPermission(...)) throw ...。业务类的方法中包含TransactionManager.begin()和TransactionManager.commit()。Q15: 如果重新设计这个模式在今天会有哪些改进静态代理 → 动态代理静态代理中代理类在编译期就已经写好每增加一个真实对象就需要手动编写一个代理类代码量翻倍。动态代理JDK 动态代理或 CGLIB允许在运行时动态生成代理对象消除了代理类的数量问题。Spring AOP 让代理变得透明开发不再需要手动编写代理类只需声明Transactional、Cacheable等注解Spring 自动为 Bean 生成代理。// 使用注解即可无需手动编写代理ServicepublicclassUserService{TransactionalpublicvoidcreateUser(Useruser){// 只写核心业务逻辑事务由代理自动处理userDao.insert(user);}}Sidecar 模式云原生在 Kubernetes Istio 架构中每个微服务旁边部署一个代理进程Envoy负责处理所有进出流量、服务发现、熔断限流。这是代理模式从代码层面到基础设施层面的升华。2. 框架源码实战分析2.1 Spring AOP 代理机制Spring AOP 是代理模式最经典的工业级应用。它的核心是在运行时为目标对象生成代理在代理中织入切面逻辑事务、安全、日志等。JDK 动态代理 vs CGLIB 代理代理方式适用条件原理JDK 动态代理目标类实现了接口通过Proxy.newProxyInstance()生成实现了相同接口的代理类CGLIB 代理目标类未实现接口通过生成目标类的子类作为代理EmitSubclassSpring 源码核心逻辑简化// Spring AOP 的核心——代理创建publicclassDefaultAopProxyFactoryimplementsAopProxyFactory{OverridepublicAopProxycreateAopProxy(AdvisedSupportconfig)throwsAopConfigException{// 判断使用 JDK 动态代理还是 CGLIBif(config.isOptimize()||config.isProxyTargetClass()||hasNoUserSuppliedProxyInterfaces(config)){// 没有接口 → 使用 CGLIBreturnnewObjenesisCglibAopProxy(config);}else{// 有接口 → 使用 JDK 动态代理returnnewJdkDynamicAopProxy(config);}}}// JDK 动态代理的调用处理程序publicclassJdkDynamicAopProxyimplementsAopProxy,InvocationHandler{// 持有目标对象的引用privatefinalAdvisedSupportadvised;OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{MethodInvocationinvocationnewReflectiveMethodInvocation(proxy,target,method,args,...);// 1. 执行前置通知Before// 2. 执行目标方法ObjectretValinvocation.proceed();// 3. 执行后置通知AfterReturning / After// 4. 返回结果returnretVal;}}运行时调用链当你调用userService.createUser(user)时实际上调用的是 Spring 生成的代理对象的createUser()方法。代理对象进入JdkDynamicAopProxy.invoke()。invoke 方法获取该方法对应的拦截器链包括事务拦截器、日志拦截器等。逐个执行拦截器先开启事务然后调用目标方法最后提交事务或回滚。整个过程中UserService完全不需要知道事务的存在。2.2 MyBatis Mapper 动态代理MyBatis 中我们只需要定义 Mapper 接口而无需编写实现类// 定义接口 —— 不需要写实现类publicinterfaceUserMapper{Select(SELECT * FROM user WHERE id #{id})UserselectById(Longid);}MyBatis 使用 JDK 动态代理为每个 Mapper 接口生成代理对象// MyBatis 核心——Mapper 代理publicclassMapperProxyFactoryT{privatefinalClassTmapperInterface;SuppressWarnings(unchecked)protectedTnewInstance(MapperProxyTmapperProxy){// 使用 JDK 动态代理生成 Mapper 接口的代理对象return(T)Proxy.newProxyInstance(mapperInterface.getClassLoader(),newClass[]{mapperInterface},mapperProxy// 调用处理程序);}}// MapperProxy——InvocationHandlerpublicclassMapperProxyTimplementsInvocationHandler,Serializable{privatefinalSqlSessionsqlSession;privatefinalClassTmapperInterface;OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{// 1. 如果是 Object 的方法toString, hashCode 等直接调用if(Object.class.equals(method.getDeclaringClass())){returnmethod.invoke(this,args);}// 2. 根据 Method 从配置中查找对应的 SQL 语句MappedStatementMappedStatementmsconfiguration.getMappedStatement(method.getName());// 3. 执行 SQL代理的核心职责returnsqlSession.selectOne(ms,args);}}运行时调用链当你调用userMapper.selectById(1)时实际调用的是MapperProxy.invoke()→ 获取 SQL 语句 → 创建SqlSession→ 执行 JDBC 操作 → 映射结果集 → 返回 User 对象。对业务代码来说感觉就像在调用一个本地方法。2.3 Tomcat RequestFacade 安全代理Tomcat 在处理 HTTP 请求时内部使用了org.apache.catalina.connector.Request对象来处理底层 socket 数据。但这个对象暴露了太多内部 API如getCoyoteRequest()、getConnector()等如果直接传给 Servlet 容器开发者可以强转后访问 Tomcat 的内部实现存在安全隐患。解决方案使用RequestFacade作为代理// Tomcat 内部的核心 Request 对象包含大量的内部方法publicclassRequestimplementsHttpServletRequest{// 内部方法——不希望暴露给 Servlet 开发者publicConnectorgetConnector(){...}publicCoyoteRequestgetCoyoteRequest(){...}// 标准 Servlet 方法OverridepublicStringgetParameter(Stringname){...}OverridepublicStringgetRequestURI(){...}}// RequestFacade——代理只暴露标准 Servlet 接口publicclassRequestFacadeimplementsHttpServletRequest{// 持有内部 Request 对象的引用组合privatefinalRequestrequest;publicRequestFacade(Requestrequest){this.requestrequest;}// 只代理标准方法OverridepublicStringgetParameter(Stringname){returnrequest.getParameter(name);}OverridepublicStringgetRequestURI(){returnrequest.getRequestURI();}// 注意没有代理 getConnector()、getCoyoteRequest() 等内部方法// Servlet 开发者无法通过 RequestFacade 访问 Tomcat 内部实现}为什么这样设计这是保护代理的典型应用。Tomcat 通过RequestFacade控制了对底层Request对象的访问权限——只允许开发者调用标准 Servlet API屏蔽了内部实现细节。这既保证了接口的兼容性又保护了容器内部的安全。3. 深度追问3.1 Why为什么解决哪个根本矛盾代理模式解决的是**核心业务职责的纯粹性与系统横切关注点Cross-cutting Concerns的侵入性之间的根本矛盾。**安全、事务、日志、缓存、延迟加载等横切逻辑与核心业务逻辑本质上是正交的但传统的 OOP 缺乏有效的分离机制导致这些关注点散落在各个业务类中造成代码的混乱和重复。代理模式通过创建一个中间层将这些横切关注点从核心业务中剥离出来实现了关注点分离。3.2 How怎么做对象关系与交互机制代理模式的实现有三个层级逐步递进静态代理手动编写代理类编译期就已确定。优点是简单直接缺点是一个代理类只能服务一个接口真实对象增加时代码量翻倍。JDK 动态代理运行时通过Proxy.newProxyInstance()为接口生成代理对象使用InvocationHandler拦截方法调用。优点是无需手动编写代理类缺点是只能代理实现了接口的类。CGLIB 代理运行时通过字节码生成技术ASM创建目标类的子类作为代理。优点是可以代理没有接口的普通类缺点是final方法无法被代理且生成速度比 JDK 代理慢。三种方式的共同核心代理与真实对象实现相同的接口或继承相同的基类客户端面向接口编程对代理的存在无感知。3.3 Trade-off代价牺牲了什么换取什么它牺牲了付出代价性能开销每次方法调用都多了一层间接调用在高频调用场景下性能损耗不可忽略尤其是动态代理的反射调用。调试复杂度代理在 IDE 中断点调试时调用栈比直接调用深 2-3 层。Spring AOP 的代理 AOP 拦截器链可能让调用栈深达 5-8 层。启动时间动态代理在启动时需要生成字节码、加载类会延长应用的启动时间。它换取了获得收益完美的关注点分离核心业务与横切逻辑完全解耦。极高的可扩展性新增横切逻辑只需增加新的代理或切面不改现有业务逻辑。对客户端的完全透明客户端感知不到代理的存在面向接口编程即可。3.4 Evolution演化它是如何从简单设计逐步演化而来的硬编码横切逻辑起点在业务代码中直接写日志、事务、权限检查。最直接但也最混乱。静态代理抽取为每个业务类编写对应的代理类将横切逻辑抽取到代理中。但代码量翻倍维护成本高。JDK 动态代理动态化运行时根据接口动态生成代理对象消除了代理类的数量问题。但只能代理接口。CGLIB 代理突破限制通过生成子类的方式代理普通类突破了 JDK 代理只能代理接口的限制。AOP 框架声明式Spring AOP 结合 IoC 容器让开发者只需要声明Transactional、Cacheable等注解Spring 自动在运行时为目标 Bean 生成合适的代理。开发者几乎感知不到代理的存在。3.5 Modern Practice现代实践它在今天被取代了吗结论代理模式不仅没有被取代反而在现代框架中成为了基础设施级的核心机制。Spring AOP是代理模式最成功的工业应用Transactional、Cacheable、Async等注解全部依赖代理实现。MyBatis Mapper 动态代理让开发者不需要编写 DAO 实现类。Dubbo / gRPC 的远程代理让远程调用像本地调用一样简单。Sidecar 模式——代理模式在基础设施层面的升华在云原生架构Kubernetes Istio中每个微服务旁边部署一个代理进程Envoy负责处理所有进出流量、服务发现、熔断限流、可观测性。业务代码完全不需要关心这些基础设施能力——这正是代理模式透明控制思想的终极体现。4. 总结核心要点代理模式为真实对象提供一个替身通过中间层控制访问并添加额外逻辑三种实现层级静态代理 → JDK 动态代理 → CGLIB 代理与装饰器的区别代理控制访问装饰器增强功能Spring AOP 是代理模式的核心应用——通过Transactional等注解声明式管理横切逻辑在云原生架构中代理模式升级为Sidecar 模式成为基础设施的一等公民一句话代理模式就是中间人——你找明星演出通过经纪人代理谈价格、排档期明星只负责登台表演。5. 参考文献[1] GAMMA E, HELM R, JOHNSON R, et al. Design Patterns: Elements of Reusable Object-Oriented Software[M]. Boston: Addison-Wesley, 1994.[2] SPRING. Spring AOP Documentation[EB/OL]. https://docs.spring.io/spring-framework/reference/core/aop.html, 2024.[3] MyBatis. MyBatis 3 Source Code[EB/OL]. https://github.com/mybatis/mybatis-3, 2024.[4] Apache Tomcat. Tomcat Source Code[EB/OL]. https://tomcat.apache.org/, 2024.