如何手写一个责任链模式?核心代码怎么设计?
简化版
手写责任链通常先定义统一的 Handler 接口,再定义 HandlerChain 保存处理器列表和当前下标,最后由每个 Handler 在需要放行时调用 chain.doHandle(context)。这种列表式实现比每个节点保存 next 更适合工程扩展。
详细版
一个常见 Java 实现包含三部分:
- Context:请求上下文;
- Handler:处理器接口;
- HandlerChain:保存处理器并驱动执行。
示例结构:
interface Handler {
void handle(Context context, HandlerChain chain);
}
class HandlerChain {
private final List<Handler> handlers;
private int index = 0;
void doHandle(Context context) {
if (index >= handlers.size()) {
return;
}
Handler handler = handlers.get(index++);
handler.handle(context, this);
}
}
处理器中如果调用 chain.doHandle(context) 就表示继续传递;如果不调用,就表示短路。
工程实现中还要考虑处理器顺序、链对象是否可复用、异常处理、线程安全和链尾兜底。尤其要注意:如果 HandlerChain 内部有 index 状态,通常每次请求都应该创建新的 Chain 实例,避免多线程共享下标。
完整版教学
一、先定义上下文对象
上下文用于承载请求数据和链路处理中产生的必要状态。
class OrderContext {
private String userId;
private String skuId;
private int count;
// getter / setter
}
上下文不要设计得过度复杂。它应该表达本条链真正需要的数据,而不是把所有业务对象都塞进去。
二、定义处理器接口
处理器接口要让每个节点有机会决定是否继续。
interface OrderHandler {
void handle(OrderContext context, OrderHandlerChain chain);
}
为什么要把 chain 传进去?因为处理器要能控制是否继续执行。
如果处理器调用:
chain.doHandle(context);
表示放行。
如果处理器不调用,链路就在这里停止。
三、实现处理器链
列表式责任链示例:
class OrderHandlerChain {
private final List<OrderHandler> handlers;
private int index = 0;
OrderHandlerChain(List<OrderHandler> handlers) {
this.handlers = handlers;
}
public void doHandle(OrderContext context) {
if (index >= handlers.size()) {
return;
}
OrderHandler handler = handlers.get(index++);
handler.handle(context, this);
}
}
这个实现的关键点是 index++。每次调用 doHandle,都会取出下一个处理器。
四、编写具体处理器
登录校验:
class LoginHandler implements OrderHandler {
public void handle(OrderContext context, OrderHandlerChain chain) {
if (context.getUserId() == null) {
throw new RuntimeException("用户未登录");
}
chain.doHandle(context);
}
}
参数校验:
class ParamHandler implements OrderHandler {
public void handle(OrderContext context, OrderHandlerChain chain) {
if (context.getSkuId() == null || context.getCount() <= 0) {
throw new RuntimeException("参数错误");
}
chain.doHandle(context);
}
}
业务处理:
class CreateOrderHandler implements OrderHandler {
public void handle(OrderContext context, OrderHandlerChain chain) {
// 真正创建订单
// 通常作为链尾业务节点,不再继续传递
}
}
五、组装并执行
List<OrderHandler> handlers = List.of(
new LoginHandler(),
new ParamHandler(),
new CreateOrderHandler()
);
OrderHandlerChain chain = new OrderHandlerChain(handlers);
chain.doHandle(context);
这样调用方只依赖链,不依赖具体处理器顺序。
六、注意线程安全问题
上面的 OrderHandlerChain 内部有可变的 index。
这意味着它不适合被多个请求共享。如果多个线程共享同一个 Chain 实例,index 会互相干扰,导致请求跳过处理器或执行顺序错乱。
更安全的做法是:
- 每次请求创建新的 Chain 实例;
- 或者把
index作为方法参数传递; - 或者让链对象无状态。
比如无状态写法:
void doHandle(Context context, int index) {
if (index >= handlers.size()) {
return;
}
handlers.get(index).handle(context, nextIndex -> doHandle(context, index + 1));
}
实际项目中,框架通常会帮你处理这些细节。
七、链尾处理要明确
如果链尾没有业务处理器,index >= handlers.size() 时直接返回可能会掩盖问题。
可以根据场景选择:
- 正常结束;
- 抛出异常;
- 返回默认响应;
- 执行最终业务处理。
比如校验链可以正常结束,表示校验通过;命令处理链则可能需要抛出“不支持的命令类型”。
八、可重入、线程安全的链游标
若单例 Chain 把 index 存为成员变量,两个并发请求可能共享同一游标;把 index 放进每次调用的上下文后,两条链各自从 0 开始。
request A(index 0..N) || request B(index 0..N), no shared mutable cursor
责任链是否正确,不能只看节点都被注册,还要看请求到底经过哪些节点、在哪里停止以及返回阶段如何展开。应把“继续、已处理、拒绝、系统失败”建成可区分的结果,并让日志能够还原一次请求的完整路径。
九、链路顺序、短路与可观测性
| 检查维度 | 应确认的内容 |
|---|---|
| 机制正确性 | 能串起来只是第一步,线程安全、异常策略和链尾语义才决定实现是否可上线。 |
| 适用边界 | 处理器和链常被容器作为单例复用,应避免请求级可变成员;动态更新链时可用不可变快照。 |
| 测试证据 | 用记录型处理器覆盖全通过、主动短路、业务拒绝、节点异常和链尾无人处理 |
| 工程代价 | 重点测量链长、各节点耗时、最坏路径延迟和短路率,并确认动态装配不会产生重复节点或排序漂移 |
易错点:能串起来只是第一步,线程安全、异常策略和链尾语义才决定实现是否可上线。
十、常见误区与追问
- 误区:把当前索引存在 Chain 单例字段里最省事。 并发请求会竞争游标并跳过节点,应使用调用栈参数、请求上下文或每次创建执行游标。
- 误区:责任链一定会让每个处理器都执行。 纯链可能命中一个节点就结束,不纯链也可能因拒绝、异常或短路提前终止,必须由链契约定义。
- 追问:处理器抛异常后还要继续吗? 由契约区分业务拒绝和系统异常;系统异常通常停止并统一转换,不能任意吞掉继续。
- 追问:责任链如何避免请求到链尾无人处理? 提供明确的默认处理器或链尾失败结果,并对未处理请求记录指标,不能静默成功。
- 追问:责任链上线后最少要记录哪些信息? 至少记录链版本、节点顺序、逐节点耗时与决策、短路位置、异常和最终处理结果。
十一、加强记忆
手写责任链记住三件事:Handler 定义统一处理入口,HandlerChain 保存顺序并驱动下一个节点,处理器通过“调不调用 chain”决定放行或短路。工程里还要注意链对象状态不能被多线程共享,链尾必须有明确语义。