← 返回题目列表

责任链模式中的上下文对象应该怎么设计?

高频 困难 第 15 / 27 题 更新于 2026/08/02
责任链模式上下文对象DTO线程安全字段契约

简化版

责任链上下文对象要承载链路需要共享的数据、处理结果和执行状态,但不能变成所有处理器随便读写的“大杂烩”。设计时要区分只读输入、可变中间态、最终结果和观测信息,并为每个字段定义谁能写、什么时候写、是否允许为空。

详细版

上下文对象通常包含请求输入、用户信息、校验结果、风险结果、链路状态和错误信息。它让多个处理器不用互相直接依赖,但如果没有边界,处理器之间会通过字段暗中耦合:A 处理器写了 riskLevel,B 处理器默认它一定存在,C 处理器又覆盖它,问题会越来越隐蔽。

比较稳的做法是把上下文分层:request 放原始输入,facts 放已经计算出的事实,result 放链路输出,attributes 只放少量扩展字段,并限制使用范围。高价值字段不要全部塞进 Map<String,Object>,应该有明确类型和注释。

面试回答可以强调:上下文是责任链的共享契约,不是临时变量袋。字段越多,越要有命名、生命周期、读写权限和测试约束。

完整版教学

一、上下文对象解决的是处理器之间的数据传递

责任链里每个处理器只面向链入口和上下文,不应该直接依赖其他处理器。比如 AuthHandler 解析用户,RiskHandler 使用用户等级和 IP,StockHandler 使用商品和数量。如果没有上下文,就会出现处理器之间传参数、调用彼此方法,链式解耦就失效了。

Request -> Context
AuthHandler 写入 userId
RiskHandler 读取 userId,写入 riskLevel
StockHandler 读取 skuId/count,写入 stockPassed
SubmitHandler 读取前面结果,生成 orderId

上下文的价值在于把“共享数据”集中在一个对象里,但代价是它可能变成隐式耦合点。一个 5 节点链里,如果每个节点写 3 个字段,很快就有 15 个字段;字段生命周期不清楚时,排查成本会比直接调用还高。

心法:上下文是链路契约,不是所有处理器都能随手塞东西的临时 Map。

二、把字段分成输入、事实、结果和观测四类

一个可维护的上下文通常可以分成 4 类字段。第一类是只读输入,比如请求参数、场景、traceId;第二类是事实数据,比如用户身份、商品信息、风险等级;第三类是链路结果,比如通过、拒绝、错误码;第四类是观测信息,比如执行节点、耗时、短路原因。

class OrderContext {
    private final OrderRequest request;
    private final String traceId;
    private UserInfo userInfo;
    private RiskDecision riskDecision;
    private ChainResult result = ChainResult.pass();
    private final List<String> executedHandlers = new ArrayList<>();
}
字段类别示例写入规则
只读输入request, scene, traceId构造时写入,后续不改
事实数据userInfo, riskDecision指定处理器写入
链路结果result, errorCode短路或结束时写入
观测信息executedHandlers, costMs链容器统一写入

三、不要把所有字段都塞进 Map

Map<String,Object> 很诱人,因为新增字段不用改类。但它牺牲了类型安全、可读性和重构能力。比如 context.put("risk", "HIGH")context.put("riskLevel", 3) 同时存在时,哪个是准的?IDE 也无法帮你发现拼写错误。

Map 不是不能用,而是适合放低频扩展字段。高频字段、关键业务字段、会影响短路决策的字段,应该建成明确属性。假设订单链有 8 个处理器,其中 5 个都读取 userInfo,这个字段就应该是强类型;只有某个灰度处理器临时写入 experimentBucket,才适合放扩展属性。

// 不推荐:关键字段弱类型化
Object user = context.getAttributes().get("user");

// 推荐:关键字段强类型化
UserInfo user = context.getUserInfo();

四、字段读写权限要靠约定和封装控制

如果上下文所有字段都有公开 setter,任何处理器都能改任何数据,链路就会变得不可预测。更好的做法是为关键字段提供语义化方法,比如 markRiskRejected()attachUser()recordHandler(),而不是暴露通用 setResult()

class OrderContext {
    private UserInfo userInfo;
    private ChainResult result = ChainResult.pass();

    void attachUser(UserInfo userInfo) {
        if (this.userInfo != null) {
            throw new IllegalStateException("userInfo already attached");
        }
        this.userInfo = userInfo;
    }

    void reject(String code, String message) {
        this.result = ChainResult.stop(code, message);
    }
}

这个设计的重点是让非法状态更早暴露。比如 userInfo 只能写一次,如果两个认证处理器都尝试覆盖它,就应该直接失败。否则后面的处理器拿到被覆盖的数据,问题会出现在更远的地方。

五、上下文生命周期要限定在单次请求内

责任链上下文通常不是线程安全对象,它应该只服务于单次请求或单次任务。不要把上下文放进单例处理器的成员变量,也不要在异步任务里继续引用会被复用的上下文。处理器本身可以是单例 Bean,但上下文必须每次新建。

@Component
class BadHandler implements Handler {
    private OrderContext lastContext; // 错误:多线程请求会互相覆盖

    public ChainResult handle(OrderContext context, Chain chain) {
        this.lastContext = context;
        return chain.next(context);
    }
}

假设 2 个请求同时进入同一个单例处理器,请求 A 写入 lastContext=A 后还没执行完,请求 B 写入 lastContext=B,A 后续读取成员变量就可能拿到 B 的数据。责任链并没有消除并发问题,只是把请求数据集中到了上下文里。

六、短路结果要放在上下文还是返回值里

有两种常见设计:一种是处理器返回 ChainResult,链容器根据返回值决定是否继续;另一种是处理器修改上下文中的 result,再调用或不调用 next。返回值方式更显式,上下文方式更适合需要后置处理和统一收集结果的链。

设计方式优点风险
返回 ChainResult短路清晰,便于测试结果和上下文字段可能重复
修改 context.result统一承载状态处理器可能忘记停止后续链
抛异常适合非预期错误容易把业务拒绝和系统异常混在一起
业务拒绝: return STOP(code=RISK_REJECT)
系统错误: throw RuntimeException
正常通过: return PASS

七、上下文要支持可观测但不要污染业务

链路观测需要记录每个处理器的执行情况,但这些字段最好由链容器统一维护,不要让业务处理器自己拼日志。比如链容器在调用处理器前记录开始时间,调用后记录结果,异常时记录错误类型。处理器只负责业务决策。

executedHandlers:
1 AuthHandler PASS 3ms
2 ParamHandler PASS 1ms
3 RiskHandler STOP 15ms code=RISK_REJECT

这样设计有两个好处:第一,所有处理器都有统一日志格式;第二,新增处理器天然被观测覆盖。否则 10 个处理器里有 7 个打日志、3 个没打,线上排查会缺关键节点。

八、常见误区与追问

  • 误区:上下文就是一个 Map,方便就行。 关键字段弱类型化会让拼写错误、类型转换错误和字段覆盖问题延迟到运行期。
  • 误区:字段越全越好。 字段越多,处理器之间越容易产生隐式依赖,应该只放链路真正需要共享的数据。
  • 误区:处理器可以随便改上下文。 关键字段要有写入时机和写入方约束,最好通过语义化方法控制。
  • 追问:上下文要不要设计成不可变? 输入部分适合不可变,中间事实和结果通常需要变化,可以做成局部可变、整体单请求隔离。
  • 追问:异步处理器能不能持有上下文? 可以传递必要快照,但不要长时间持有可变上下文,避免生命周期越界和并发修改。
  • 追问:业务拒绝应该写上下文还是抛异常? 业务拒绝更适合明确结果,系统异常才适合抛异常,两者混用会让监控和重试策略混乱。

九、加强记忆

上下文设计记住“四分法”:输入只读,事实有主,结果明确,观测统一。只读输入保证请求基础不被改;事实字段要知道由哪个处理器写;结果字段要能表达通过、停止和错误;观测字段由链容器统一补齐。这样上下文既能传递数据,又不会把责任链变成字段互相污染的黑箱。