← 返回题目列表

责任链模式有哪些角色?请求流程是怎样的?

高频 简单 第 2 / 27 题 更新于 2026/07/28
责任链模式HandlerHandlerChain请求流程设计模式

简化版

责任链模式通常包含请求对象、抽象处理器、具体处理器和链路组装者。请求从链头进入,每个处理器判断自己是否处理,并决定是否传给下一个处理器,直到请求被处理、被拦截或到达链尾。

详细版

责任链模式的典型角色包括:

  1. Request:请求对象,携带待处理的数据;
  2. Handler:抽象处理器,定义处理接口,也可能保存下一个处理器;
  3. ConcreteHandler:具体处理器,实现某个独立职责;
  4. HandlerChain:处理器链,负责保存处理器顺序并驱动执行;
  5. Client:调用方,只把请求交给链入口,不直接依赖具体处理器。

常见流程是:

Client -> Handler A -> Handler B -> Handler C -> End

每个 Handler 都可以选择:

  • 当前处理并继续;
  • 当前处理后停止;
  • 不处理,直接传递;
  • 抛出异常或返回失败结果。

面试中要强调:责任链模式不是必须使用 next 指针实现,也可以用数组、列表、递归、迭代器或框架回调实现。只要请求沿着一组处理器按顺序传递,并且处理器之间相对解耦,就体现了责任链思想。

完整版教学

一、Request:请求对象承载上下文

责任链处理的不是抽象空气,而是一个明确的请求对象。这个请求对象可以是:

  • HTTP 请求;
  • 订单审批单;
  • 登录请求;
  • 网关上下文;
  • 日志事件;
  • 风控上下文。

请求对象通常会携带输入数据,也可能携带处理中间状态:

class OrderContext {
    String userId;
    String skuId;
    int count;
    boolean loginChecked;
    boolean riskPassed;
}

如果链上节点需要共享结果,可以把这些结果放在上下文里。但要注意不要把上下文设计成“万能大对象”,否则链路会变得隐式而难以维护。

二、Handler:抽象处理器定义统一入口

抽象处理器负责定义链上节点共同遵守的接口。

一种常见写法是:

interface Handler {
    void handle(Request request, HandlerChain chain);
}

另一种写法是每个 Handler 保存下一个节点:

abstract class Handler {
    protected Handler next;

    public void setNext(Handler next) {
        this.next = next;
    }

    public abstract void handle(Request request);
}

第一种更像 Servlet Filter 或 Spring Interceptor 的风格,由 HandlerChain 控制继续执行;第二种更像经典 GoF 结构,由处理器自己调用 next

三、ConcreteHandler:具体处理器只做一件事

具体处理器是责任链真正执行逻辑的地方。

比如登录校验处理器:

class LoginHandler implements Handler {
    public void handle(Request request, HandlerChain chain) {
        if (request.getUserId() == null) {
            throw new RuntimeException("未登录");
        }
        chain.doHandle(request);
    }
}

参数校验处理器:

class ParamHandler implements Handler {
    public void handle(Request request, HandlerChain chain) {
        if (request.getSkuId() == null) {
            throw new RuntimeException("商品为空");
        }
        chain.doHandle(request);
    }
}

每个处理器都应该边界清晰,这样新增、删除、复用和测试都比较容易。

四、HandlerChain:链路组装和执行控制

很多真实项目中不会让每个处理器都直接持有 next,而是用一个链对象统一管理顺序。

伪代码如下:

class HandlerChain {
    private List<Handler> handlers;
    private int index = 0;

    public void doHandle(Request request) {
        if (index == handlers.size()) {
            return;
        }
        Handler handler = handlers.get(index++);
        handler.handle(request, this);
    }
}

这种写法的好处是:

  • 链路顺序集中可控;
  • 可以统一处理异常;
  • 可以动态增加处理器;
  • 处理器不需要知道下一个处理器是谁。

五、Client:调用方只面向链入口

调用方只需要这样写:

chain.doHandle(request);

调用方不应该写成:

loginHandler.handle(request);
paramHandler.handle(request);
riskHandler.handle(request);

如果调用方直接知道所有处理器,责任链的解耦价值就会下降。责任链希望调用方知道“我要走一条处理链”,而不是知道“我要调用哪几个具体处理器”。

六、流程中最容易被问到的是“谁决定继续传递”

责任链有两种常见控制方式:

  1. 处理器主动调用下一个节点;
  2. 链对象统一驱动下一个节点。

前者结构简单,适合小规模链路;后者更灵活,适合框架和复杂业务。Servlet Filter、网关过滤器这类场景通常会使用链对象驱动,因为框架需要统一管理顺序、生命周期和异常。

七、请求、处理器与链容器的协作

一条链含鉴权、限流、校验 3 个处理器;请求对象只提交 1 次,Chain 负责按 1→2→3 顺序推进。

Client -> HandlerChain(index=0) -> Handler -> next(index+1)

责任链是否正确,不能只看节点都被注册,还要看请求到底经过哪些节点、在哪里停止以及返回阶段如何展开。应把“继续、已处理、拒绝、系统失败”建成可区分的结果,并让日志能够还原一次请求的完整路径。

八、链路顺序、短路与可观测性

检查维度应确认的内容
机制正确性链容器不是 GoF 必需角色,却能集中处理顺序、异常、指标和不可变装配。
适用边界Request 应包含处理所需的稳定上下文,Chain 管顺序和推进,Handler 保持单一职责;不要让三者边界混乱。
测试证据用记录型处理器覆盖全通过、主动短路、业务拒绝、节点异常和链尾无人处理
工程代价重点测量链长、各节点耗时、最坏路径延迟和短路率,并确认动态装配不会产生重复节点或排序漂移

易错点:链容器不是 GoF 必需角色,却能集中处理顺序、异常、指标和不可变装配。

九、常见误区与追问

  • 误区:每个 Handler 都必须保存 next 指针。 List 加索引也能实现责任链,而且更便于动态组装、插入和统一观测。
  • 误区:责任链一定会让每个处理器都执行。 纯链可能命中一个节点就结束,不纯链也可能因拒绝、异常或短路提前终止,必须由链契约定义。
  • 追问:请求对象可以在链上随意修改吗? 应定义可变字段和所有权;共享可变上下文会形成顺序耦合,必要时用明确结果对象。
  • 追问:责任链如何避免请求到链尾无人处理? 提供明确的默认处理器或链尾失败结果,并对未处理请求记录指标,不能静默成功。
  • 追问:责任链上线后最少要记录哪些信息? 至少记录链版本、节点顺序、逐节点耗时与决策、短路位置、异常和最终处理结果。

十、加强记忆

责任链模式的角色可以记成“请求、处理器、具体处理器、处理器链、调用方”。请求从链头进入,处理器负责局部判断和处理,链负责顺序和传递,调用方只依赖链入口;面试时要补充一句:责任链不限定必须用 next 指针,列表式过滤器链同样是常见实现。