责任链模式有哪些角色?请求流程是怎样的?
简化版
责任链模式通常包含请求对象、抽象处理器、具体处理器和链路组装者。请求从链头进入,每个处理器判断自己是否处理,并决定是否传给下一个处理器,直到请求被处理、被拦截或到达链尾。
详细版
责任链模式的典型角色包括:
- Request:请求对象,携带待处理的数据;
- Handler:抽象处理器,定义处理接口,也可能保存下一个处理器;
- ConcreteHandler:具体处理器,实现某个独立职责;
- HandlerChain:处理器链,负责保存处理器顺序并驱动执行;
- 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);
如果调用方直接知道所有处理器,责任链的解耦价值就会下降。责任链希望调用方知道“我要走一条处理链”,而不是知道“我要调用哪几个具体处理器”。
六、流程中最容易被问到的是“谁决定继续传递”
责任链有两种常见控制方式:
- 处理器主动调用下一个节点;
- 链对象统一驱动下一个节点。
前者结构简单,适合小规模链路;后者更灵活,适合框架和复杂业务。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 指针,列表式过滤器链同样是常见实现。