尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

代理模式与装饰器模式对比:从UML到Java代码实现

代理模式与装饰器模式对比:从UML到Java代码实现 代理模式和装饰器模式是Java开发里最容易混淆的两个结构型模式网上讲它俩的文章很多但多数停留在“代理控制访问、装饰器增强功能”这种概念层面真正落到代码层面去抠细节的少。我在项目中两种模式都用过不少也见过不少同事把Decorator写成Proxy、或者反过来最后代码结构变得很奇怪。这篇文章我把两者的UML结构、代码示例、使用场景、以及行为型设计模式的分类一次性讲透尽量用实际项目的视角去对比帮你在面试和写代码时都能快速判断该用哪个。行为设计模式这类模式我顺手做了系统性梳理因为面试时经常连在一起问而且理解行为型模式之间的分工反过来也能帮你看清Proxy和Decorator在GoF二十三种模式里的定位。这个视角挺重要——很多资料是平铺直叙地讲单个模式很少告诉你它们之间的边界在哪。我会用“职责边界”和“客户端感知”这两个维度去串联整篇文章读完你会对模式体系有个更立体的认知。1. 内容整体设计与思路拆解1.1 为什么Proxy和Decorator总是被放在一起比较先说结论这两个模式在UML类图上长得实在太像了都是“持有一个被代理/被装饰对象的引用并且实现/继承同一个抽象接口”。如果不看调用端代码单看类图几乎分不清谁是谁。这是它们被反复对比的根本原因。有一个更底层的维度代理模式和装饰器模式都采用“组合优于继承”的委托机制。也就是不通过子类覆写来扩展行为而是把目标对象作为字段持有在中转方法里调用目标方法。这个共性让很多人在设计阶段就把两者混用了。等你写了一段时间后会发现其实它们要解决的核心问题完全不同Proxy的核心是控制访问Decorator的核心是动态增强。控制访问意味着你可能根本不想让客户端直接触达目标对象增强则意味着目标对象本身仍然是主角只是你给它“叠buff”。说白了从设计意图上看阶段不同、时机不同、开放程度也不同。我在实际项目里见过一个典型案例给支付接口做个日志记录同事直接弄了一个LogPaymentProxy代理里打完日志调真实支付方法看上去没毛病。但后面要加缓存、加重试、加限流每一个需求都往这个类里堆代码最后这个“Proxy”膨胀到上千行每次改动都心惊胆战。这就是典型的把装饰器用成了代理而且还没抽象出公共层导致扩展全堆在一个类里。正确做法是如果你只是想在调真实支付前记录日志而且未来可能叠加多种增强逻辑那就应该用装饰器模式把LogDecorator、CacheDecorator、RetryDecorator分开每一层只干一件事。如果你想保护真实支付对象不让客户端直接碰它你才用代理模式。两个模式的目的完全不同结构相似纯属巧合。1.2 判断模式的本质维度客户端感知与职责边界我判断一个类是Proxy还是Decorator最核心的两个维度客户端是否感知中间层存在。代理模式下客户端通常只知道接口类型不知道背后有代理对象在转发甚至不知道有真实对象的存在它只跟代理交互。装饰器模式下客户端通常显式地创建被装饰对象和装饰器把它们嵌套起来客户端完全清楚自己拿到的是增强后的对象甚至清楚增强链路是什么样。中间层对目标对象的控制程度。代理往往可以拦截、拒绝、延迟、路由请求目标对象不一定要被调用装饰器则几乎没有“拦截”语义它是“先做自己的事再无条件调用被装饰对象”的链式结构。装饰器之间是透明的嵌套代理则是取代关系。这两个维度可以在写代码时用来做即时判断。假如你写完一个类发现它加了很多if判断来决定“是否调用真实方法”那大概率是代理。如果你写的类只是机械地往目标方法前后织入逻辑、无脑透传参数那大概率是装饰器。类设计阶段的自我纠偏比事后重构成本低得多这两个判断维度建议先记在脑子里。2. 构型对比代理模式的三种形式与装饰器的递归嵌套2.1 静态代理与动态代理的实现套路静态代理最直接的写法是“一个代理类对应一个接口/目标类”在编译期就确定代理关系。代码示例public interface PaymentService { void pay(String orderId, double amount); } public class RealPaymentService implements PaymentService { Override public void pay(String orderId, double amount) { System.out.println(真实支付 orderId 金额 amount); } } public class PaymentProxy implements PaymentService { private final PaymentService target; public PaymentProxy(PaymentService target) { this.target target; } Override public void pay(String orderId, double amount) { System.out.println([代理] 记录请求日志); if (amount 10000) { throw new IllegalArgumentException(单笔金额超限); } target.pay(orderId, amount); System.out.println([代理] 发送通知); } } // 客户端 PaymentService proxy new PaymentProxy(new RealPaymentService()); proxy.pay(A1001, 999.0);这是最基础的静态代理。注意这个代理类的关键点在于可以在方法里做校验、拦截、甚至直接不调用target.pay。这种控制能力在装饰器里通常是不出现的因为装饰器的核心语义是必须保证增强链完整执行。动态代理在Java里常用JDK动态代理要求目标对象实现接口。它跟静态代理的区别在于代理类在运行期生成不需要手动编写适合做横切逻辑。下面是我常用的一个例子public class LogInterceptor implements InvocationHandler { private final Object target; public LogInterceptor(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(调用方法前 method.getName()); Object result method.invoke(target, args); System.out.println(调用方法后 method.getName()); return result; } public static Object wrap(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInterceptor(target)); } } // 使用 PaymentService proxy (PaymentService) LogInterceptor.wrap(new RealPaymentService()); proxy.pay(A1002, 200.0);JDK动态代理只能给接口做代理这是Java单继承模型决定的Proxies通过接口生成。如果目标类没有实现接口就得用CGLib它通过继承来生成代理子类。我实际项目里用Spring AOP时Spring会自动判断对象是否实现接口来选JDK还是CGLib但这个底层逻辑最好了解面试和排坑都会用到。2.2 装饰器模式的递归包装结构装饰器的标准结构是抽象组件、具体组件、抽象装饰器、具体装饰器四个角色。核心在于具体装饰器持有Component引用且自己也是Component这样才能不断嵌套。public interface DataSource { String read(); void write(String data); } public class FileDataSource implements DataSource { Override public String read() { return 文件原始数据; } Override public void write(String data) { System.out.println(写入文件 data); } } public abstract class DataSourceDecorator implements DataSource { protected final DataSource wrapper; public DataSourceDecorator(DataSource wrapper) { this.wrapper wrapper; } } public class EncryptionDecorator extends DataSourceDecorator { public EncryptionDecorator(DataSource wrapper) { super(wrapper); } Override public void read() { String data wrapper.read(); System.out.println(解密后读取 data); } Override public void write(String data) { String encrypted 加密: data; wrapper.write(encrypted); } } public class CompressionDecorator extends DataSourceDecorator { public CompressionDecorator(DataSource wrapper) { super(wrapper); } Override public void read() { System.out.println(解压前处理); wrapper.read(); } Override public void write(String data) { System.out.println(压缩数据); wrapper.write(data); } } // 客户端嵌套使用 DataSource ds new CompressionDecorator( new EncryptionDecorator( new FileDataSource())); ds.write(小明);这个例子是GoF《设计模式》里IO流的经典复刻几乎能直接对应Java的BufferedInputStream、DataInputStream嵌套结构。装饰器的关键就在构造函数里传入一个被装饰对象然后在覆写方法里一层层往下调用。每加一个新增强功能只需要新写一个装饰器不需要改动已有的类。这里有个容易踩坑的地方很多人在装饰器里加了if判断比如“如果数据长度大于1024才压缩”。这本质上没问题但会破坏装饰器的透明性。装饰器的语义是“对客户端透明地增强”如果内部做了条件分支客户端就失去了对增强行为的可预期性。所以我建议复杂的条件增强逻辑放在装饰器的上游或下游控制器里装饰器本身保持纯粹的线性增强。2.3 静态代理、动态代理、装饰器的对照表下面这个表总结了我个人理解的关键差异点面试时直接背这个框架能讲清楚。维度静态代理动态代理装饰器编译期关系手写代理类编译期确定运行期动态生成代理类运行期构造器嵌套编译期无直接关系核心目的控制访问/扩展机制横切逻辑复用动态叠加增强能力客户端感知通常不感知代理存在不感知代理存在明确感知装饰器链调用链特征单层转发单层转发多层递归调用目标对象调用可能被拦截不调用可能被拦截不调用必须一路透传到最底层典型实现手写Proxy类JDK Proxy / CGLib / Spring AOPJava IO流装饰器是否改变接口不改变不改变不改变我特别想把第三行“客户端感知”讲透因为这是绝大多数资料不会强调的。静态代理和动态代理的目标是让客户端“无感”客户端拿到的是一个看起来跟真实对象一样的对象但内部做的可能是指纹认证、权限校验、路由转发甚至调用的根本不是本地实现而是远程服务。装饰器则不同你new它的时候就要明确传一个被装饰者给它客户端完全清楚这个对象被“加工过”。2.4 动态代理与AOP的关系以及和装饰器的配合使用Spring AOP是我用过最多的动态代理落地场景。它的底层原理就是Spring容器启动时发现某个Bean需要被切面增强就会返回一个代理对象而不是原始Bean对象。你可以通过Aspect定义一个切面在目标方法执行前后做日志、事务、权限控制。Aspect Component public class LogAspect { Around(execution(* com.example.service.*.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println(before); Object result pjp.proceed(); System.out.println(after); return result; } }AOP的定位是“横切逻辑”比如在一个银行系统里转账、查询、开户都要做日志但日志跟业务无关不可能每个业务方法都写一遍。AOP用切面统一解决这正是动态代理的用武之地。但是动态代理解决不了“分层增强”的问题。如果你需要对同一个服务叠加日志、缓存、重试、限流四种能力并且希望这四种能力互相独立、可插拔AOP的点切面也能做但配置会变得很重而且切面的顺序控制很麻烦。这时候装饰器反而更合适你可以在业务代码里手动组装PaymentService service new RetryDecorator( new CacheDecorator( new LogDecorator(realService)));我还试过把装饰器和AOP结合用AOP负责横切的全局逻辑比如全链路traceId装饰器负责单业务链路的局部增强比如某个服务的重试策略。这样各有分工代码既不会被全局横切淹没局部语义也不会因为局部装饰逻辑入侵全局Aspect。3. 一图看懂行为设计模式的系统性分类很多人学设计模式最大的困惑是二十三种模式名字都记不全更别说什么时候用了。我觉得关键在于把行为型模式先分个组它不像结构型模式那样专注于类与类的组合方式行为型模式更关注“对象之间的职责分配与交互算法”。我先给出我自己的一套分类逻辑再去逐个说明用途。3.1 分类维度交互协调、算法封装、状态转换、行为巡回我习惯把行为型模式分成四组这个分组方式不一定是标准答案但从记忆和使用角度非常实用对象交互协调组观察者模式、中介者模式、责任链模式、命令模式。这一组解决的是“多个对象之间如何通信、如何解耦、如何传递请求”。算法与状态封装组策略模式、模板方法模式、状态模式、解释器模式。这一组解决的是“算法流程怎么组织状态怎么驱动行为”。遍历与访问控制组迭代器模式、访问者模式、备忘录模式。这一组解决的是“在不暴露内部结构的前提下如何遍历对象、访问对象、恢复对象”。行为回溯与保存组可以先跟上一组合并但为了好记我单独拎出来。这个分组不是唯一正确但能帮你在面对一个具体问题时快速定位该从哪个模式库里去寻找方案。面试时被问到“责任链模式跟状态模式有啥区别”你脑子里就能立刻想到责任链是交互组的状态是状态转换组的它们解决的问题域完全不同。3.2 对象交互协调组观察者、中介者、责任链、命令这一组是实际项目中用得最多的行为型模式。观察者模式解决“一对多通知”问题经典案例是消息队列的发布订阅模型订单创建后需要通知库存系统、积分系统、短信系统如果订单服务直接new这些系统对象耦合就太高了。观察者把通知对象抽象成订阅者列表发布者只管广播事件。public interface OrderEventListener { void onOrderCreated(Order order); } public class OrderService { private final ListOrderEventListener listeners new ArrayList(); public void addListener(OrderEventListener listener) { listeners.add(listener); } public void createOrder(Order order) { System.out.println(创建订单核心逻辑); for (OrderEventListener listener : listeners) { listener.onOrderCreated(order); } } }中介者模式解决“多对多网状交互”问题。比如一个聊天室如果每个人都要跟其他人直接通信就会有N*(N-1)/2条链路。引入聊天室这个中介者后每个人只需要跟聊天室通信链路从网状变成星型。责任链模式解决“请求的多个处理器按顺序处理”的问题。经典场景是审批流请假1天部门经理审批3天总监审批5天以上老板审批。每个审批者都持有下一个审批者的引用当前审批者处理不了就传给下一个。我见过不少团队把它跟if-else做比较其实两者的区别是责任链支持运行时动态调整链条if-else是写死的。命令模式解决“请求的发送者与执行者解耦”的问题经常用于支持撤销、重做、日志记录等操作。把“开灯”封装成一个Command对象按钮只负责触发Command真正执行灯开关的是另一个对象这样按钮类不需要依赖电灯类。3.3 算法与状态封装组策略、模板方法、状态策略模式是我个人最喜欢的模式它解决“算法族的自由切换”问题。面试最常见的场景是支付方式选择微信支付、支付宝支付、银行卡支付每种支付算法独立成一个类客户端在运行期选择具体策略。它的核心思想是把“做什么”和“怎么做”分离。public interface PayStrategy { void pay(double amount); } public class WechatPayStrategy implements PayStrategy { Override public void pay(double amount) { System.out.println(微信支付 amount); } } public class AlipayPayStrategy implements PayStrategy { Override public void pay(double amount) { System.out.println(支付宝支付 amount); } } public class PaymentContext { private PayStrategy strategy; public void setStrategy(PayStrategy strategy) { this.strategy strategy; } public void executePay(double amount) { strategy.pay(amount); } }模板方法模式解决“算法骨架固定细节步骤可覆写”的问题。比如做一杯咖啡和一杯茶过程都是煮水、冲泡、倒杯、加调料但冲泡的东西和加什么调料不同。父类定义算法骨架子类覆写抽象步骤这个模式在框架代码里极其常见比如AbstractApplicationContext的refresh()方法。状态模式跟策略模式长得像但意图完全不同。策略模式的客户端主动切换策略状态模式则是由对象内部状态变化自动驱动行为切换。最经典的例子是TCP连接状态连接建立后可以传输数据传输收到FIN后进入关闭等待各状态下同一个方法的反应完全不同。状态模式把“状态的逻辑判断”从一长串if-else中拆散到各个状态类里代码维护性显著提升。3.4 遍历与访问控制组迭代器、访问者、备忘录迭代器模式可能是Java程序员最无感的模式因为Java集合框架已经内置了Iterator接口。但它的核心思想值得记住在不暴露集合内部结构的前提下让外部代码可以顺序遍历集合元素。自己实现一个迭代器的时候才能体会到它的妙处——你可以在迭代器里做过滤、做排序、做延迟加载。访问者模式是最难理解、也最容易被过度设计的行为型模式。它的核心场景是对象结构稳定但作用于对象上的操作经常变化。比如一个文件目录树由文件夹和文件节点组成你需要在这个结构上做压缩、杀毒、统计大小等不同操作。如果直接在每个节点类里加方法节点类会被各种业务操作污染。用访问者模式每个操作写成一个Visitor节点类只负责accept访问者。备忘录模式解决“对象状态的回滚与恢复”问题最典型的场景就是文档编辑器的撤销。它最大的坑在于存档对象可能很大频繁快照会有性能问题。所以实际项目中我对备忘录的推荐是只保存变更差异不要全量快照这个看法可能跟教科书里说的不一样但被线上事故教育过之后你会发现这个约束真的很重要。3.5 行为设计模式记忆清单与判断口诀以下是我个人整理的行为型模式速查表每个模式配一句话描述和典型场景。千万不要死记理解分类后你会发现这些模式之间是有关联的模式一句话核心典型场景观察者发布事件订阅者自动响应消息通知、事件驱动中介者引入中心枢纽减少网状依赖聊天室、GUI控件协调责任链请求沿链传递直到被处理审批流、过滤器链路命令请求封装为对象支持撤销重做编辑器Undo、任务队列策略算法族可互换客户端选择策略支付方式、压缩算法模板方法骨架固定子类填细节变化Spring的JdbcTemplate状态状态驱动行为切换替代if-elseTCP连接、订单状态流转迭代器隐藏内部结构顺序遍历元素Java集合遍历访问者结构稳定操作变化多操作隔离编译器AST处理备忘录保存/恢复对象内部状态游戏存档、撤销操作判断口诀其实是我写代码时反复用的分享给你对象间要解耦通知 → 观察者/中介者/事件总线请求不确定谁来处理 → 责任链操作要支持回退 → 命令备忘录多种同类算法自由切 → 策略算法骨架固定但步骤要变 → 模板方法对象有多态状态行为 → 状态模式3.6 行为型模式与结构型模式的组合实践写到这里我想强调一个很容易被忽略的点模式是组合使用的不是孤立套用的。一个真实项目里极大概率同时用到了多个模式而且它们各自服务不同的代码层次。举个例子我曾经维护过一个订单流程引擎。这个引擎的最外层用了状态模式管理订单状态流转中间层用策略模式处理不同支付渠道的差异触发支付时通过观察者模式通知库存、物流、积分子系统如果某个渠道失败则通过责任链模式依次尝试备用渠道。整个系统看起来复杂但每个模式的角色边界非常清晰新同学接手时看类名和包结构就能大概猜出模块职责。这个例子想说明的是你可以把模式当作积木块而不是成套的体系。代理模式、装饰器模式、策略模式可以嵌套使用Proxy包装了一层控制访问的逻辑里面的真实对象是一个Decorator的嵌套链而链的底层才是真正复杂的业务系统。这种组合在Java IO、Spring框架源码里都有大量体现。4. 实操过程与核心环节实现4.1 写一个同时包含Proxy与Decorator的最小可运行项目我自己练习这两个模式时习惯写一个最小项目同时把两个模式都放进去。这里就把完整的Java示例贴出来尽量简单但结构完整你可以直接复制运行。背景需求一个文件上传服务需要做到以下几点使用代理模式控制文件大小超过限制就直接拒绝。使用装饰器模式为上传服务叠加加密和压缩能力。接口与基础实现public interface FileUploader { void upload(String fileName, byte[] data); } public class S3FileUploader implements FileUploader { Override public void upload(String fileName, byte[] data) { System.out.println(上传到S3 fileName 大小 data.length 字节); } }代理部分控制访问public class FileSizeLimitProxy implements FileUploader { private final FileUploader target; private final int maxSize; public FileSizeLimitProxy(FileUploader target, int maxSize) { this.target target; this.maxSize maxSize; } Override public void upload(String fileName, byte[] data) { if (data.length maxSize) { throw new IllegalArgumentException(文件过大超过限制 maxSize); } target.upload(fileName, data); } }装饰器部分增强能力public abstract class FileUploaderDecorator implements FileUploader { protected final FileUploader uploader; public FileUploaderDecorator(FileUploader uploader) { this.uploader uploader; } } public class EncryptDecorator extends FileUploaderDecorator { public EncryptDecorator(FileUploader uploader) { super(uploader); } Override public void upload(String fileName, byte[] data) { System.out.println(加密数据); byte[] encrypted (E: new String(data)).getBytes(); uploader.upload(fileName, encrypted); } } public class CompressDecorator extends FileUploaderDecorator { public CompressDecorator(FileUploader uploader) { super(uploader); } Override public void upload(String fileName, byte[] data) { System.out.println(压缩数据); byte[] compressed (C: new String(data)).getBytes(); uploader.upload(fileName, compressed); } }客户端组装public class Main { public static void main(String[] args) { // 装饰器链先压缩再加密最终落到真实上传 FileUploader decorated new EncryptDecorator( new CompressDecorator( new S3FileUploader())); // 代理在最外层做访问控制 FileUploader proxy new FileSizeLimitProxy(decorated, 1024); proxy.upload(report.pdf, hello world.getBytes()); } }运行上面代码输出结果是加密、压缩、上传三个动作依次执行而文件大小校验在最外层被触发。这个例子很好地展示了两者的分工代理负责“能不能调”装饰器负责“调的时候叠加什么能力”。4.2 关键决策JDK动态代理还是CGLib代理在实际项目里动态代理用得比静态代理多得多但选JDK还是CGLib是有讲究的我整理成几条经验目标类实现了接口优先用JDK动态代理。这是JDK原生支持没有任何额外依赖生成代理对象快。目标类没有实现接口或目标方法不是接口方法只能考虑CGLib或者改用Spring AOP由容器决定。Spring Boot 2.x默认如果目标类没有接口会使用CGLib通过继承生成子类有接口时默认用JDK动态代理。但Spring Boot官方后来倾向于强制CGLib因为CGLib可以覆盖更多场景代价是代理类需要额外引入cglib依赖。使用CGLib时需要注意final方法无法被代理因为继承无法覆写final方法Spring AOP对final方法无法增强private方法同样无法代理。一个我记得很清楚的线上事故团队成员给一个Service类加了final关键字整个事务注解全部失效排查了很久才发现是CGLib无法代理final类Spring AOP只能绕过事务不回滚。这个例子很典型足以体现“底层代理原理”对业务代码的直接影响。4.3 如何快速判断一个现有代码是代理还是装饰器在工作中比起自己写你更常遇到的是维护别人写的代码。我总结了一套三连问判断法能快速识别现有代码的定位第一问这个中间类是否可能“不调用真实对象的方法”如果可能它更像代理。比如权限拦截后直接抛异常或者限流后直接短路这些都是代理的典型动作。装饰器通常不会短路请求它是一路透传的。第二问调用端创建对象时是否显式传入了被包装对象装饰器通常是显式构造的如new BufferedInputStream(new FileInputStream(...))代理则经常由工厂方法或框架容器创建调用端拿到的只是接口引用不清楚内部是否有代理。第三问中间类是否存在多层嵌套装饰器以嵌套链为常态代理通常只隔了一层。如果一个包装类又被另一个包装类包着而且链路很长这个结构基本可以认定是装饰器链。这套判断法不是百分之百准确但够用。真拿不准时直接看类的作者给它取的类名和注解——命名里带Proxy的大概率是代理带Decorator、Wrapper、Enhancer的大概率是装饰器。规约和命名也是团队协作中重要的一部分。4.4 代理模式和装饰器模式在框架源码中的应用框架源码是最生动的教学材料。我觉得看源码不是为了背代码而是看它在什么场景选了哪个模式。先看JDK IO流这是装饰器模式最教科书式的体现。FileInputStream是被装饰者BufferedInputStream是具体装饰器DataInputStream是另一个装饰器。你是可以new DataInputStream(new BufferedInputStream(new FileInputStream(data.txt)))的这就是三层装饰结构的现实写法。再看Spring的ProxyFactoryBean和JdkDynamicAopProxy这是代理模式的经典实现。当Spring创建Bean时如果检测到切面配置它会返回一个代理对象而非原始Bean对象。JdkDynamicAopProxy实现了InvocationHandler核心是invoke方法里会执行拦截器链。Spring事务管理的核心原理就是通过代理在业务方法执行前后开启/提交/回滚事务。再看MyBatis的Mapper我们平时用的UserMapper接口并没有实现类框架默认给每个接口生成一个MapperProxy每次调用方法其实是调用MapperProxy.invoke()然后通过JDK动态代理绑定到SQL语句。这个设计既用了动态代理又用了它“拦截转发”的能力是一种很极致的应用。5. 常见问题与排查技巧实录5.1 代理对象与目标对象——关于this调用导致代理失效这是Spring AOP和事务失效最经典的问题。场景如下一个Service里有A方法和B方法A方法内部直接调用B方法但B方法上标注了事务注解。看起来事务应该生效实际上不会。原因是this调用是稳定地经过原始对象不是代理对象所以B方法上的增强逻辑直接被跳过了。我在项目中遇到过一次库存扣减失败数据还是写进去了排查到最后就是this.deductStock()导致的。解决办法很简单有两种路径要么把B方法拆到另一个Service里让外部调用触发代理要么自身注入代理对象比如在类里注入Autowired private OrderService self;然后调用self.deductStock()。还有一种是使用AopContext.currentProxy()需要开启exposeProxy属性。这个坑每个用Spring做动态代理的开发都应该尽早知道。5.2 装饰器模式导致方法调用链日志重复装饰器链越长越容易出现同一个方法被多个装饰器打印日志看起来就像日志重复。这个问题不是模式的问题而是职责规划的问题。我建议装饰器链中日志装饰器只放一层不要每一层都打印日志如果确实需要分层打印要在日志里带上当前装饰器的类名。另一个相关的问题是装饰器顺序。加密和压缩的顺序会影响输出内容先压缩再加密和先加密再压缩效果完全不同。我就见过同事因为装饰器顺序不对导致加密后的数据无法解密。经验是装饰器的顺序应该是从“离业务最远的通用能力”到“离业务最近的定制能力”实际使用中建议把这个顺序写在配置里或者用常量定义拼接顺序避免未来被调整乱。5.3 动态代理报警告InvocationHandler的参数细节写JDK动态代理时一个很常见的低级错误是在invoke方法里错误使用参数个数比如把(Object proxy, Method method, Object[] args)写漏或者写错导致编译报错或者运行期抛IllegalArgumentException。另一个高频错误是invoke方法里没有处理代理对象上的equals、hashCode、toString方法。如果你的拦截器对这3个方法做统一增强会导致集合操作、日志输出时出现诡异行为。我自己的经验是在invoke方法里优先对Object的方法放行Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getDeclaringClass() Object.class) { return method.invoke(this, args); } // 业务增强逻辑 return method.invoke(target, args); }5.4 模式误用的典型症状与重构建议这里整理一些我帮同事review代码时常见的模式误用场景以及对应的重构建议症状原因重构建议代理类疯狂膨胀超过1000行把多种增强逻辑堆在代理里改用装饰器链策略模式拆分职责装饰器里出现了短路return装饰器误用了代理的拦截语义把拦截逻辑上提到代理层客户端直接new了真实对象并手动调用绕过了代理限制提供工厂方法统一返回代理一个装饰器里塞了多个职责加密压缩日志没抽象出多个装饰器类拆分成多个单一职责装饰器访问控制用了装饰器组装控制语义错位改用代理AOP实现权限控制我见过最夸张的一个例子一个被命名为XxxProxy的类里既有缓存逻辑、又有日志逻辑、还有限流逻辑甚至还有一个简单的幂等判断。那已经不是代理模式了那就是一个上帝类。后来我们把它拆成了一个代理负责权限和限流三个装饰器分别负责缓存、日志、幂等中间还引入了一个策略模式处理不同渠道的幂等策略。拆分后每个类重新变得小而清晰测试也好写了很多。5.5 代理模式装饰器模式的组合避坑清单如果你决定在一个系统里同时使用这两种模式我建议你建立以下组合规范虽然不能覆盖所有场景但能帮你躲过绝大多数坑。第一代理层只做横切控制不做业务增强。权限校验、限流、路由、延迟加载这些放在代理里加密、压缩、日志、缓存、重试这些增强能力的叠加放在装饰器里。这样职责一旦清晰后来人不会乱。第二装饰器链尽量在装配工厂里定义不要在业务方法内到处new。比如定义一个FileUploaderFactory由工厂负责创建完整的装饰器链业务方只从工厂拿成品。这样做还有一个额外好处将来要调整链顺序只需要改工厂一处不需要改动业务调用方。第三如果代理是Spring AOP生成的装饰器链里不要依赖代理对象。换句话说装饰器链要基于原始业务对象构建而代理要包裹整个装饰器链。顺序是Proxy - Decorator1 - Decorator2 - RealService不要写成Decorator1 - Proxy - RealService。否则代理增强逻辑可能被跳过或重复执行。6. 项目决策树遇到需求时如何快速选型说了这么多对比最后分享一个我写在团队Wiki里的决策小抄。当团队收到一个新需求不知道应该用哪个模式时就对着这个树走一遍大部分情况都能快速落定是否需要给外部调用方隐藏内部实现 ├─ 是走代理 │ ├─ 是否需要拦截/控制/延迟加载 │ ├─ 是代理模式静态或动态 │ └─ 否直接暴露实现 └─ 否是否需要对目标对象做多层能力叠加 ├─ 是装饰器模式 └─ 否直接实现/继承这个树的判断逻辑并不复杂核心是区分控制与增强。如果需求描述里出现“限制、拦截、校验、鉴权、路由、屏蔽”这些词汇基本可以走代理路线。如果出现“增强、叠加、装饰、缓存、日志、加密、压缩”这些词汇优先考虑装饰器路线。实际工作中我也遇到过一些需求既可以走代理也可以走装饰器。这时候我的原则是先看未来扩展方向。如果增强能力不停增加、需要灵活组合优先装饰器如果重点是保护真实对象即便没有增强需求也要严格控制访问入口优先代理。值得一提的是现在的开源框架里经常两者兼有。比如Netty的ChannelHandler链本质上是责任链但每个Handler对事件的处理又像一个装饰器Dubbo的Consumer端调用链里既有代理屏蔽远程调用细节也有装饰器超时重试、优雅降级。所以不要试图把真实代码强行归类到某一个模式的纯净形态中——模式是工具不是教条。根据我个人的项目经验还有一个小建议可以分享学习这两个模式时建议照着文章里的最小示例自己敲一遍、改一遍、破坏一遍。比如故意把代理里的拦截逻辑删掉看调用顺序变成什么样或者故意调换装饰器顺序看输出变化。多改几次你就能在没有类图的情况下靠直觉判断哪个区域该用代理、哪个区域该用装饰器。这种直觉在真实需求评审中非常值钱。
返回列表