ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

责任链模式详解:从过滤器到业务校验与审批流

责任链模式详解:从过滤器到业务校验与审批流 在一个稍复杂一点的系统中纯粹写一段“处理请求”的代码并不难难的是把多个处理者按顺序组织起来并且让每个处理者在处理完自己的部分后能决定是继续传递、中断流程还是改换处理方向。责任链模式Chain of Responsibility Pattern就是为了解决这类问题而存在的设计模式。它把多个处理者串成一条链请求沿着链依次经过每个节点直到某个节点决定处理完毕或请求被明确阻止为止。很多开发者第一次接触责任链模式是从过滤器Filter、拦截器Interceptor或中间件Middleware开始的但真正理解它的价值是在项目中遇到“校验规则越来越多”“审批流程不断变化”“消息处理器堆成一排 if-else”这类问题时。这篇博客会把责任链模式从概念、结构、代码示例、适用场景一直讲到常见误区和排错思路目标是让你不仅能看懂示例还能在真实项目中判断该不该用、怎么用、用完之后如何维护。1. 先理解责任链模式要解决的问题1.1 没有责任链时代码容易变成一长串判断先看一个非常常见的业务场景用户提交订单时需要做多重校验比如参数非空、用户状态正常、商品库存充足、账户余额足够。常规写法很容易写成下面这样public void createOrder(OrderRequest request) { if (request null || request.getUserId() null) { throw new IllegalArgumentException(请求参数不合法); } if (!userService.isNormal(request.getUserId())) { throw new IllegalStateException(用户状态异常); } if (!stockService.hasEnoughStock(request.getSkuId(), request.getQuantity())) { throw new IllegalStateException(库存不足); } if (!accountService.hasEnoughBalance(request.getUserId(), request.getAmount())) { throw new IllegalStateException(余额不足); } orderService.save(request); }这段代码在业务只有四五个校验时问题不大一眼能看完。但实际项目中校验项会继续增长比如加风控、加限购、加优惠券使用条件、加订单频率限制。每增加一条规则createOrder方法就要继续加一行if方法长度会越来越长耦合关系也会越来越重。一旦出现以下信号就该考虑用责任链模式一段逻辑里有多个判断并且判断顺序很重要。后续大概率还会继续增加新的判断类型。希望某个判断模块能被独立复用、独立测试。希望新增一条规则时尽量少改动原方法。1.2 责任链模式的核心思想责任链模式属于行为型设计模式。它的核心思想是把请求的发送者和请求的处理者解耦让多个处理者都有机会处理同一个请求。每个处理者内部持有下一个处理者的引用当前处理者处理完或判断不需要自己处理后就把请求交给下一个节点。这里要区分两种处理方式传递型责任链每个节点都参与处理处理完后继续传给下一个节点。典型的例子是流程审批、日志过滤器、请求拦截器。终止型责任链某个节点处理完毕后直接返回不再继续向下传递。典型的例子是异常处理链、订单风控链。两种方式没有绝对优劣取决于业务需要。设计时先确定当前场景属于哪一种代码结构会清晰很多。1.3 和 if-else 相比责任链模式带来什么用 if-else 直接写逻辑优点是直观、调试简单缺点是所有规则都是编译期写死的顺序新增规则必须修改原方法。责任链模式把规则变成独立的处理器对象每个处理器只关心自己的责任。新增规则时新增一个处理器类再把它挂到链上即可。但这里要提醒一句责任链模式不是银弹。如果只有两个判断条件硬套责任链反而增加类数量和阅读成本。它适合的是“规则数量较多、顺序可能变化、规则需要复用”的场景。2. 责任链模式的三个核心组成部分责任链模式通常包含三个角色角色英文名职责抽象处理者Handler定义处理请求的接口并持有下一个处理者的引用具体处理者ConcreteHandler实现自己的处理逻辑决定是否继续传递客户端Client组装责任链并发送请求如果用 Java 来表达抽象处理者最常见的写法是public abstract class AbstractHandler { protected AbstractHandler next; public void setNext(AbstractHandler next) { this.next next; } public abstract void handle(Request request); }每个具体处理者都继承这个抽象类。处理完自己的逻辑后如果还有下一个节点就调用next.handle(request)。这种写法的优点在于结构简单、容易理解。缺点也明显客户端需要手动把节点一个个串起来一旦漏掉某个setNext调用链条就会断。所以很多项目会引入构建器Builder或管理类来组装责任链这部分后面会专门说明。3. 用 Java 写一个最小可运行的责任链示例3.1 场景定义订单请求校验用一个订单校验场景来演示规则包括参数校验、用户状态校验、库存校验。每个处理器处理成功后把请求交给下一个处理器处理失败时直接抛出异常或返回失败结果。先定义请求对象和统一返回结果public class OrderRequest { private String userId; private String skuId; private int quantity; private BigDecimal amount; // getter / setter 省略 }public class Response { private boolean success; private String message; public static Response ok() { Response response new Response(); response.success true; response.message 处理成功; return response; } public static Response fail(String message) { Response response new Response(); response.success false; response.message message; return response; } }3.2 抽象处理者定义抽象处理者时把返回结果统一成Response方便客户端判断链在处理哪个节点时失败public abstract class OrderHandler { protected OrderHandler next; public void setNext(OrderHandler next) { this.next next; } public Response handle(OrderRequest request) { Response response doHandle(request); if (!response.isSuccess()) { return response; } if (next ! null) { return next.handle(request); } return response; } protected abstract Response doHandle(OrderRequest request); }这里把doHandle设计成受保护抽象方法让子类只关心自己的校验逻辑不需要重复编写“失败返回、成功传递”的公共逻辑。这样既统一了链式调用流程也减少了子类代码冗余。3.3 三个具体处理者参数校验处理器public class ParamValidHandler extends OrderHandler { Override protected Response doHandle(OrderRequest request) { if (request null || request.getUserId() null || request.getSkuId() null) { return Response.fail(请求参数不合法); } return Response.ok(); } }用户状态校验处理器public class UserStatusHandler extends OrderHandler { Override protected Response doHandle(OrderRequest request) { // 实际项目中这里会调用用户服务查询用户状态 if (blocked.equals(request.getUserId())) { return Response.fail(用户状态异常); } return Response.ok(); } }库存校验处理器public class StockHandler extends OrderHandler { Override protected Response doHandle(OrderRequest request) { // 实际项目中这里会调用库存服务 if (request.getQuantity() 0 || request.getQuantity() 100) { return Response.fail(库存不足); } return Response.ok(); } }这个示例里使用字符串blocked和数据边界100是为了说明结构真实项目要替换成服务查询和真实库存上限。3.4 客户端组装并执行责任链客户端需要手动把三个处理器串起来public class Client { public static void main(String[] args) { OrderHandler handler1 new ParamValidHandler(); OrderHandler handler2 new UserStatusHandler(); OrderHandler handler3 new StockHandler(); handler1.setNext(handler2); handler2.setNext(handler3); OrderRequest request new OrderRequest(); request.setUserId(user123); request.setSkuId(sku123); request.setQuantity(1); request.setAmount(new BigDecimal(100)); Response response handler1.handle(request); System.out.println(response.isSuccess() : response.getMessage()); } }运行后请求会依次经过参数校验、用户状态校验、库存校验。如果所有校验通过链路会走到最后一个节点的next此时next null直接返回成功结果如果某个节点失败会立即返回失败结果不再继续后续节点。4. 责任链组装方式的演进从手动 setNext 到 Builder4.1 手动组装容易漏节点上面的手动setNext方式理解起来很直观但真实的项目里处理器数量可能超过十个。如果每次都在客户端代码里写十几次setNext很容易出现两个问题漏掉某个节点的setNext链直接断掉但代码不会报错只有运行时才发现后面的规则没执行。顺序调整时要改客户端代码客户端代码作为组装方耦合了所有处理器的顺序。4.2 引入 Builder 统一组装可以在抽象处理者内部增加一个静态 Builder把处理器列表统一组装成链public abstract class OrderHandler { protected OrderHandler next; public void setNext(OrderHandler next) { this.next next; } public abstract Response doHandle(OrderRequest request); public static Builder builder() { return new Builder(); } public static class Builder { private OrderHandler first; private OrderHandler last; public Builder add(OrderHandler handler) { if (first null) { first handler; last handler; } else { last.setNext(handler); last handler; } return this; } public OrderHandler build() { return first; } } }使用方式变成OrderHandler chain OrderHandler.builder() .add(new ParamValidHandler()) .add(new UserStatusHandler()) .add(new StockHandler()) .build(); Response response chain.handle(request);这样新增节点时只需在 Builder 链式调用里增加一个add顺序一眼就能看清漏挂节点的可能性也大大降低。4.3 使用 Spring 时的自动化组装方式如果项目使用 Spring可以让每个具体处理器注册为 Spring Bean再通过ListOrderHandler自动注入Component public class OrderHandlerChain { private final OrderHandler firstHandler; public OrderHandlerChain(ListOrderHandler handlerList) { // 先按 Order 注解排序再组装成链 ListOrderHandler sorted handlerList.stream() .sorted(Comparator.comparingInt(OrderHandler::getOrder)) .collect(Collectors.toList()); OrderHandler first null; OrderHandler current null; for (OrderHandler handler : sorted) { if (first null) { first handler; current handler; } else { current.setNext(handler); current handler; } } this.firstHandler first; } public Response handle(OrderRequest request) { if (firstHandler null) { return Response.fail(责任链为空); } return firstHandler.handle(request); } }这种情况下新增一个处理器就新增一个Component类并实现 getOrder 顺序控制即可既不需要改动客户端也不需要改动既有处理器。这是微服务项目中比较常见的做法前提是团队能接受“隐式注入”带来的排查成本。5. 责任链模式一般用在什么场景5.1 过滤器与拦截器链最典型的使用场景是 Web 框架中的过滤器、拦截器、中间件比如 Java Web 里的 Servlet Filter、Spring MVC 的 HandlerInterceptor、Netty 的 ChannelHandler、Express 的中间件。请求到达业务代码之前会依次经过多个节点做日志记录、登录校验、权限校验、跨域设置、参数解析等。这些框架内部使用责任链模式正是因为请求处理流程天然是顺序化的而且每个节点关注点不同组合方式需要灵活变化。5.2 审批流与状态流转审批流是责任链模式的经典业务场景。请假申请、报销申请、发布工单都可能需要多级审批员工提交后先由组长审批组长通过后传到部门经理部门经理通过后传到人事或财务。这里要注意审批流和责任链不完全等价。责任链节点之间是直接引用关系而真正的审批流通常会把流程定义持久化节点之间通过数据库记录绑定支持撤回、驳回、并行审批。如果需求只是“按固定顺序依次审批”责任链模式可以承担如果需求包含动态流程、条件跳转、会签、撤回责任链模式只是其中一小部分还需要流程引擎来配合。5.3 参数校验与风控规则业务层面的多重校验非常适合责任链模式。每个校验器只负责一类规则比如参数校验器、用户状态校验器、库存校验器、风控校验器、限购校验器。新增一种校验时不影响其他校验器也不影响已经稳定的业务主流程。风控场景更是如此。一个请求可能需要经过黑白名单、频控、设备指纹、行为特征、交易金额等多个维度的检查每个维度都是一个独立节点。某个节点命中风险规则后可以直接终止链并返回拦截结果。5.4 日志输出、消息消费、事件处理日志输出框架里多个 Appender 可以组合使用一个日志事件可能同时输出到控制台、文件、远程日志系统这是“每个节点都处理”的典型实现。消息消费场景中一条消息可能需要先做格式转换、再做内容过滤、再做业务处理也可以按责任链组织。事件处理系统里同一个事件可以经过多个处理器每个处理器只处理自己关注的事件类型。这种方式适合用于对扩展点要求高的系统。5.5 对比模板方法模式与策略模式写代码时经常把责任链、模板方法、策略搞混。简单区分模板方法模式一个算法骨架固定子类重写其中某一步强调的是“同一个算法的固定流程”。策略模式一组算法可以互相替换客户端选择其中一个强调的是“算法之间的替换”。责任链模式多个处理者按顺序组合请求依次经过各节点强调的是“处理者之间的传递”。选型时可以先问自己究竟是多个步骤串行、每个步骤都可能处理请求还是说多个方案选一个前者优先考虑责任链后者优先考虑策略模式。6. 责任链模式的优缺点和取舍6.1 优点责任链模式的主要优点可以从代码维护角度来说请求发送方与处理方解耦。发送方不需要知道链上有哪些节点只需要把请求交给第一个节点。新增节点成本低。符合开闭原则新增处理器不需要修改原有处理器。每个处理器职责单一。方便单独调试、单独测试、单独复用。顺序可以灵活调整。通过 Builder 或配置改变处理器顺序不需要改业务逻辑。6.2 缺点和风险链路过长时请求需要逐层传递如果处理器内部有远程调用会带来额外的延迟。调用链在代码层面不好追踪尤其使用 Spring 自动注入时责任链顺序分散在各处。调试不直观。如果某个请求没有按预期走到后面的节点需要确认是哪个节点拦截了请求。存在“责任链空转”的可能。链上没有节点能处理请求时需要有一个兜底逻辑否则请求会静默丢失。使用不当会增加类数量。简单判断也拆成多个类会让代码更零散。6.3 什么时候不建议使用判断条件很少只有两三个固定 if且不打算继续扩展。业务规则之间互相依赖后一个节点的判断依赖前一个节点的中间结果。这种情况下把规则拆成独立节点反而要额外传递上下文。性能敏感的全链路场景每个节点都只是简单判断但节点数量太多拆分的收益抵不上调用开销。团队刚接手项目对责任链模式不熟强行使用会提高阅读门槛。7. 责任链模式代码示例从纯 Java 到实际项目封装7.1 使用责任链做用户请求参数校验下面用一个更完整的例子展示一个请求先通过参数校验链再进入业务处理链。这里引入HandlerContext作为上下文用于在各个节点之间传递中间数据。public class HandlerContext { private OrderRequest request; private MapString, Object ext new HashMap(); public HandlerContext(OrderRequest request) { this.request request; } public OrderRequest getRequest() { return request; } public void put(String key, Object value) { ext.put(key, value); } public Object get(String key) { return ext.get(key); } }抽象处理器改成基于上下文工作public abstract class AbstractHandler { protected AbstractHandler next; public void setNext(AbstractHandler next) { this.next next; } public void handle(HandlerContext context) { if (!doHandle(context)) { return; } if (next ! null) { next.handle(context); } } protected abstract boolean doHandle(HandlerContext context); }此时doHandle的返回值表示“是否继续执行”。如果某个节点校验失败可以在返回false前把结果写入上下文由入口方法统一读取public class TokenValidHandler extends AbstractHandler { Override protected boolean doHandle(HandlerContext context) { OrderRequest request context.getRequest(); if (request.getToken() null || request.getToken().isEmpty()) { context.put(code, 401); context.put(message, 登录状态已失效); return false; } return true; } }入口方法通过context判断整条链的处理结果这种方式比抛异常更友好也比较适合接口层做统一响应封装。7.2 责任链模式处理多级审批流程再看一个多级审批场景。每一级审批者判断当前申请金额是否在自己的审批权限内超出权限则传递给下一级。public abstract class Approver { protected Approver next; public void setNext(Approver next) { this.next next; } public void approve(ApplyRequest request) { if (canApprove(request)) { system.out.println(getName() 审批通过); } else if (next ! null) { next.approve(request); } else { System.out.println(当前无人有审批权限); } } protected abstract boolean canApprove(ApplyRequest request); protected abstract String getName(); }具体审批者public class Manager extends Approver { Override protected boolean canApprove(ApplyRequest request) { return request.getAmount().compareTo(new BigDecimal(1000)) 0; } Override protected String getName() { return 部门经理; } }public class Director extends Approver { Override protected boolean canApprove(ApplyRequest request) { return request.getAmount().compareTo(new BigDecimal(10000)) 0; } Override protected String getName() { return 总监; } }组装时从低级别审批者开始把高级别审批者挂在后面Approver manager new Manager(); Approver director new Director(); manager.setNext(director); ApplyRequest request new ApplyRequest(new BigDecimal(5000)); manager.approve(request);这个例子说明一点责任链节点的顺序并不一定按“先到先处理”组织也可以按责任范围组织。低级别节点不能处理时才把请求向上传递。7.3 生产环境中如何把链配置外置化如果责任链顺序经常变化可以把节点顺序放到配置中心或数据库里程序启动时读取配置再按顺序组装。这样可以做到调顺序不发布代码。典型配置结构可以是 JSON{ chainName: orderValidChain, handlers: [ paramValidHandler, tokenValidHandler, userStatusHandler, stockHandler, riskHandler ] }程序启动时读取配置根据处理器名称从 Spring 容器中取出对应的 Bean再依次组装。这里要注意处理器名称必须和配置中的名称一一对应否则启动时就要报错不要等到请求进来才发现节点缺失。8. 参数、上下文和异常处理设计8.1 请求对象、上下文对象和返回对象的职责划分很多责任链实现写到最后会变得混乱原因在于没有划分清楚三个对象的职责对象职责是否可变请求对象 Request承载外部传入的原始数据尽量只读上下文 Context保存链上节点产生的中间状态和临时结果可写返回对象 Response保存最终处理结果在链的末尾统一封装不要让每个节点都直接修改请求对象也不要让请求对象变成各种临时结果的容器。中间结果应该放在上下文里。这样排查问题时请求对象、上下文、返回结果各查各的不容易产生歧义。8.2 每个节点是否要做参数校验节点内的参数校验要区分两种情况入口节点的职责就是参数校验那么它应当对请求对象的核心字段做完整校验。非入口节点则主要校验自己依赖的数据是否存在比如从上下文读取某个字段时先判断是否为空。如果每个节点都重复做全量参数校验会浪费性能如果每个节点都不做任何校验又可能在后续节点出现空指针。比较好的做法是入口节点负责基础参数各节点只校验自己要使用的上下文数据。8.3 链中节点执行失败时如何处理有三种常见的失败处理方式抛异常并由全局异常处理器统一转换适合强校验场景。返回失败结果并终止链适合接口响应型场景。记录失败日志但仍然继续向下传递适合非关键节点比如日志增强、埋点上报。三种方式要提前约定不要在一个项目里混用。否则会出现“上一个节点抛异常下一个节点还在执行”的隐性 bug。8.4 超时和上下文清理如果责任链中有远程调用节点一定要给远程调用设置超时时间。链式调用层层叠加时每个节点都慢一点整体耗时就会呈线性增长排查性能问题时很难定位是哪个节点出了问题。在链路结束时如果上下文缓存了重要的业务数据要在 finally 块里做清理避免内存占用过高。尤其是使用线程池异步执行责任链时ThreadLocal 信息必须在调用后清理否则会串数据。9. 责任链模式的常见误区和错误写法9.1 把责任链写成所有节点都强依赖前一个节点的结果责任链模式最大的优势之一是节点解耦。如果每个节点都要读取前一个节点写出的特定字段那么链就退化成“隐式依赖的 if-else”。顺序一旦调整后续节点全部失效。这种情况下应当把依赖数据放入上下文并且节点实现时要尽量容忍缺失数据。9.2 链上节点过多导致调试困难一个责任链动辄十几个节点请求进来后究竟走完了哪些节点日志里看不到排查问题只能靠猜。这是常见的落地痛点。解决方案是为每个节点增加执行日志记录节点名称、耗时、处理结果或者在上下文里记录执行路径出错时随异常一并输出。生产环境的日志可以简化成这样long start System.currentTimeMillis(); try { boolean next doHandle(context); log.info(node{}, result{}, cost{}ms, getClass().getSimpleName(), next, System.currentTimeMillis() - start); return next; } catch (Exception e) { log.error(node{} execute error, getClass().getSimpleName(), e); throw e; }9.3 把异步和同步混在一条链里有些节点需要异步处理比如异步通知、异步埋点有些节点必须同步返回结果比如用户校验。如果直接把这些节点混在一条同步链中异步节点可能会阻塞后续节点也可能因为未等异步结束就返回结果造成数据不一致。比较好的做法是同步责任链只放必须按顺序处理的核心节点异步动作放到链结束后的回调或事件发送阶段不要让两个模型互相干扰。9.4 责任链无限递归或空指针如果setNext拼成了环请求会在链上无限循环最终栈溢出。组装责任链时要做防重判断同一个处理器实例不能出现两次链尾的next必须为null。在较长的链路中可以给handle方法增加最大执行次数限制作为兜底。10. 责任链模式快速排查清单实际项目中遇到责任链相关问题建议按以下顺序排查确认请求是否真的进入了责任链入口有没有调用到第一个节点的handle方法。确认链是否组装完整每个节点的next是否指向正确的下一个节点。确认节点执行顺序是否符合预期先通过日志查看实际走的节点顺序。确认是否在某个节点被return false或异常提前终止检查该节点的判断条件和返回值。确认节点内部是否读取到了正确的上下文数据很多问题出在上下文 key 约定不一致。确认节点内部的服务调用是否异常优先检查下游服务日志。确认责任链是否有循环引用如果出现栈溢出或请求无限循环优先检查setNext。确认超时和异步节点是否拖慢了整体链路查看节点级耗时段日志。这张清单可以作为团队内部 review 责任链代码时的检查项也可以直接复制到排查文档中。11. 责任链模式的最佳实践11.1 设计阶段先画一遍请求流转图组装责任链之前先画出请求从入口到出口会经过哪些节点每个节点分别处理什么哪些节点可能终止链哪些节点只是透传。如果这张图画不清楚说明对责任链职责划分还不够明确直接写代码很容易返工。11.2 每个节点一个类命名体现职责处理器类的命名不要用Handler1、Handler2尽量用能表达职责的名字比如ParamValidHandler、UserStatusHandler、StockDeductHandler。这样责任链的顺序通过类名就能看明白日志也能直接输出节点名。11.3 统一节点执行结果的模型如果责任链用于接口校验类场景节点返回值或上下文结果字段要统一。比如统一用code表示错误码message表示提示信息。不要有的节点用success有的节点用code否则入口封装结果时要花大量时间做转换。11.4 记录节点执行链路在上下文中维护一个ListString executedNodes每个节点开始时将自己加入列表。当链执行结束或异常时把这个列表输出到日志或响应中。这在多节点场景下能大幅提升排错效率。11.5 让链的组装对业务侧无感知如果使用 Spring更推荐将责任链组装封装在单独的 Chain 类里业务代码只需要注入 Chain 并调用一个方法不需要关心链上有哪些节点。这样可以防止业务侧为了调整链顺序到处拼setNext。11.6 兼容旧系统的责任链改造如果旧系统已经有一长串 if-else想改造成责任链不要一次性全部重构。建议先把最稳定的几个判断抽成处理器保留原有的兜底逻辑等新节点稳定运行后再逐步迁移其他判断。重构过程中新旧逻辑要做结果对比避免出现“改造后流程通过但与原逻辑行为不一致”的问题。12. 责任链模式的学习路径建议12.1 第一步写一个纯 Java 的迷你实现不需要引入任何框架按上面第 3 节的最小示例先跑通。理解next引用、handle调用和节点终止条件这是责任链的基础。12.2 第二步阅读框架源码中的责任链实现Servlet Filter 的FilterChain、Spring MVC 的HandlerExecutionChain、Netty 的ChannelPipeline都可以作为阅读对象。这些框架中的责任链不仅有节点传递还包含异常处理、异步回调、生命周期管理能够帮你理解责任链在真实框架中是如何演化的。12.3 第三步结合业务改造一个小模块在真实项目中找一个有二三十行连续 if-else 的方法尝试拆成责任链。重点观察改造前后的可测试性、可扩展性和调试成本判断哪些地方收益明显哪些地方反而变复杂了。12.4 第四步补充其他可替代的设计模式学责任链时顺带对比模板方法、策略模式、观察者模式。当你能说清楚某个业务场景为什么选择责任链而不是其他三种时就说明你已经掌握了它的边界。13. 总结什么时候该真正使用责任链模式回到开头的订单校验场景。如果校验规则只有两三条用 if-else 完全没有问题可读性和性能都更好。但一旦出现以下三个特征责任链模式就会体现出明显价值校验或处理规则数量会持续增长。规则顺序可能变化且希望调整时尽量少改业务代码。每个规则都需要独立测试、独立复用或者可能被其他业务流程复用。责任链模式真正解决的问题不是“减少 if-else”本身而是“如何让多个处理者在不互相感知细节的情况下完成协作”。它的代价是增加了类数量、调用链长度和一定的理解成本。只要事先把节点职责、上下文模型、失败语义约定好并在日志中保留节点执行轨迹这个模式在项目中是可以长期稳定演进的。对于刚开始接触设计模式的开发者建议不要追逐“用新模式替代旧代码”的感觉而是先用小示例跑通结构再找一个真正需要顺序化扩展的业务模块落地。当你能判断出某个场景“为什么不适合责任链”的时候才是真正理解了这个模式。
返回列表