← 返回题目列表

如何手写一个责任链模式?核心代码怎么设计?

高频 中等 第 4 / 27 题 更新于 2026/07/28
责任链模式代码实现HandlerChainJava设计模式

简化版

手写责任链通常先定义统一的 Handler 接口,再定义 HandlerChain 保存处理器列表和当前下标,最后由每个 Handler 在需要放行时调用 chain.doHandle(context)。这种列表式实现比每个节点保存 next 更适合工程扩展。

详细版

一个常见 Java 实现包含三部分:

  1. Context:请求上下文;
  2. Handler:处理器接口;
  3. 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”决定放行或短路。工程里还要注意链对象状态不能被多线程共享,链尾必须有明确语义。