责任链模式中如何动态装配处理器?顺序怎么控制?
简化版
责任链可以通过配置、注册表或 Spring Bean 列表动态装配,关键是处理器要有明确的启用条件、顺序值和适用场景。顺序不能靠容器偶然返回顺序,应该用 order、优先级枚举或配置文件显式控制,并在启动时校验重复顺序、缺失兜底和非法组合。
详细版
动态装配责任链常见做法有三种:第一,把处理器作为 Spring Bean 注入 List<Handler>,再按 @Order 或 Ordered 排序;第二,维护一个 HandlerRegistry,按业务场景从注册表中选出处理器;第三,把链路配置放到数据库或配置中心,实现不同业务线不同链路。
高频面试点不在“能不能动态”,而在“动态后怎么可控”。处理器顺序要显式,链路启停要可审计,新增处理器要能被测试覆盖,链尾要有兜底处理器。否则动态装配会变成线上行为不可预测的来源。
回答时可以按这个结构说:先定义处理器契约,再注册所有处理器,再按场景选择可用处理器,再排序并校验,最后生成不可变链路实例。这样既保留扩展性,也避免每次请求都重新排序和装配。
完整版教学
一、为什么责任链经常需要动态装配
责任链常用于鉴权、风控、审批、参数校验、消息消费前置处理等场景,这些场景的共同特点是“处理步骤会变”。比如普通下单只需要 登录校验 -> 参数校验 -> 库存校验,大额下单还要追加 风控校验 -> 人工审核校验。如果把链写死在代码里,新增业务线时就会不停改组装代码,责任链的扩展收益会被抵消。
动态装配的目标不是让链越灵活越好,而是让“哪些处理器参与、按什么顺序执行、失败后怎么处理”变成显式规则。假设系统有 12 个处理器,一个场景启用 5 个,另一个场景启用 8 个,如果每个 Controller 自己组装,3 个场景就会出现 3 份顺序逻辑。把装配集中起来,才能统一做排序、校验和观测。
场景 A: Auth -> Param -> Stock -> Submit
场景 B: Auth -> Param -> Risk -> Quota -> Stock -> Submit
场景 C: Auth -> Param -> Coupon -> Risk -> ManualReview -> Submit
记忆钩子:动态装配不是“运行时随便拼”,而是把链路规则从业务调用方挪到统一装配层。
二、处理器契约要包含顺序和适用条件
如果处理器接口只有 handle(context, chain),装配层就不知道它应该排在哪里,也不知道它适用于哪些业务。真实项目里通常会让处理器额外暴露 order()、supports(scene)、name() 这类元信息。这样装配层不需要理解每个处理器内部逻辑,只根据契约生成链。
下面是一个常见写法,order 控制顺序,supports 控制场景,name 用于日志和排错。order 数字越小越先执行,比如 100 做认证,200 做参数校验,300 做风控,留下间隔是为了将来插入 250 这种中间步骤。
interface OrderHandler {
String name();
int order();
boolean supports(OrderScene scene);
ChainResult handle(OrderContext context, Chain chain);
}
| 元信息 | 作用 | 常见错误 |
|---|---|---|
name | 日志、指标、排错 | 只打印类名,重构后难追踪 |
order | 控制执行顺序 | 多个处理器顺序重复 |
supports | 控制是否参与链路 | 把场景判断散落在处理器内部 |
enabled | 灰度或开关 | 禁用后没有兜底校验 |
三、Spring 中常见的 Bean 列表装配方式
在 Spring 项目里,最常见的是把所有处理器定义成 Bean,然后通过构造器注入 List<OrderHandler>。Spring 可以根据 @Order、Ordered 或 AnnotationAwareOrderComparator 进行排序,但面试时要强调:不要依赖“注入列表天然有序”这种隐式行为,最好在装配代码里显式排序一次。
@Component
class OrderChainFactory {
private final List<OrderHandler> allHandlers;
OrderChainFactory(List<OrderHandler> allHandlers) {
this.allHandlers = allHandlers.stream()
.sorted(Comparator.comparingInt(OrderHandler::order))
.toList();
}
HandlerChain build(OrderScene scene) {
List<OrderHandler> selected = allHandlers.stream()
.filter(handler -> handler.supports(scene))
.toList();
validate(scene, selected);
return new HandlerChain(selected);
}
}
这个写法的好处是新增处理器只需要新增 Bean,不需要修改调用方。假设新增 BlackListHandler(order=250),它会自动进入排序后的链路。但这也带来风险:新增 Bean 可能影响多个场景,所以 supports 必须精准,启动校验和链路快照日志也必须补上。
四、配置化链路要做启动校验
如果链路来自数据库或配置中心,风险会比代码装配更高。配置写错一个处理器名称,线上链路就可能缺少关键校验;顺序写反,可能导致风控在参数校验前读取空字段;重复配置处理器,可能造成重复扣减、重复发券或重复记录日志。
一个可控的配置化链路至少要校验 4 件事:处理器名称必须存在,顺序不能冲突,必选处理器不能缺失,链尾必须有处理结果。比如支付链要求 AuthHandler、ParamHandler、SubmitHandler 必须出现,风控处理器可以按场景启用。
orderSubmit:
- name: AuthHandler
order: 100
- name: ParamHandler
order: 200
- name: RiskHandler
order: 300
- name: SubmitHandler
order: 900
启动校验:
配置项 4 个 -> 名称全部命中 -> order 无重复 -> 必选节点齐全 -> 生成链路快照
五、链路实例最好不可变并可缓存
动态装配不代表每个请求都重新创建链。对于“按场景固定”的链路,可以在启动时或配置变更时生成不可变链路并缓存。这样请求进来后只做一次 Map 查询,比如 scene=ORDER_SUBMIT 查到对应的 HandlerChain,避免每次请求都过滤、排序、校验。
假设单次排序 12 个处理器只花 0.1ms,看起来不多,但 1000 QPS 下就是每秒 100ms CPU 时间浪费;更重要的是每次动态排序容易引入请求间行为差异。把装配放到启动阶段或配置刷新阶段,能让运行期更稳定。
class ChainRegistry {
private final Map<OrderScene, HandlerChain> chains;
HandlerChain get(OrderScene scene) {
HandlerChain chain = chains.get(scene);
if (chain == null) {
throw new IllegalArgumentException("missing chain: " + scene);
}
return chain;
}
}
六、顺序控制要能被人读懂和测试
顺序值本身只是数字,数字背后的业务含义要清楚。常见做法是划分区间:100-199 做准入校验,200-299 做参数规范化,300-499 做风控,800-899 做副作用前检查,900 以后做最终提交。这样新处理器插入时不需要猜。
100 Auth
200 ParamNormalize
300 Risk
400 StockCheck
900 Submit
链路顺序还要有测试。测试不要只测“返回成功”,还要断言实际执行顺序。比如输入一个合法订单,记录执行节点,期望顺序是 Auth, Param, Risk, Submit;输入未登录订单,期望只执行 Auth 并短路。顺序一旦是业务规则,就必须进入测试。
七、动态装配的观测信息要足够
责任链动态化后,线上排查时最怕“不知道这次请求走了哪条链”。因此每次请求最好带一个 traceId,链路入口记录场景、链路版本、处理器列表,每个处理器记录结果和耗时。这样当用户反馈“为什么订单被拒绝”,可以定位到是 RiskHandler 在 12ms 内返回了 REJECT。
| 观测字段 | 示例 | 用途 |
|---|---|---|
chainName | orderSubmit | 区分业务链 |
chainVersion | v18 | 对应配置版本 |
handler | RiskHandler | 定位节点 |
decision | PASS/STOP/ERROR | 判断短路原因 |
costMs | 12 | 排查慢节点 |
trace=abc scene=ORDER_SUBMIT version=v18
Auth PASS 2ms -> Param PASS 1ms -> Risk STOP 12ms
八、常见误区与追问
- 误区:动态装配就是把所有 Handler 注入成 List。 注入列表只是收集处理器,真正的动态装配还包括场景筛选、排序、校验、缓存和观测。
- 误区:顺序靠类名或 Bean 加载顺序就行。 类名顺序和加载顺序都不是稳定业务契约,必须用显式
order或配置控制。 - 误区:配置化后就不用发版。 配置化降低发版频率,但处理器代码、契约变更、必选节点校验仍然需要严格发布流程。
- 追问:新增处理器会不会影响已有链路? 会,所以处理器要有明确
supports条件,并在启动时输出每条链的处理器快照用于审查。 - 追问:每次请求现装链可以吗? 低 QPS 可以,但高频请求更推荐按场景缓存不可变链,配置变更时再重建。
- 追问:怎么防止重复 order? 启动校验时按
order分组,发现同一场景内多个处理器使用相同顺序就直接失败或要求二级排序规则。
九、加强记忆
记住“收集、筛选、排序、校验、固化、观测”这 6 步:Spring 或注册表负责收集处理器,supports 负责筛选,order 负责排序,启动校验负责挡住错误配置,不可变链负责稳定运行期行为,日志指标负责解释每次请求走过的节点。面试回答动态装配时,把这 6 步说完整,基本就能覆盖工程落地的高频追问。