上一篇观察者是"一个事件广播给所有人、各自独立处理"。这一篇的**责任链模式(Chain of Responsibility)**换一种协作方式:把多个处理者串成一条链,让请求沿着链一个一个往下传,由链上的节点依次处理------谁能处理就处理、谁想拦截就拦截。
它的核心场景一句话:一个请求要经过一串"关卡",每个关卡负责一项检查或处理,通过了才进入下一关,任何一关不通过就拦下。 这就像你过机场安检:先查证件、再过安检门、再查行李、再核对登机牌,一道道关卡串成一条链,请求(你)顺着链往前走,每道关卡各管一段,任何一道拦下你就走不了。关键在于:每道关卡只管自己那一项,不知道整条链有多长、下一道是谁,它只负责"处理完了把请求交给下一个"。
我们的订单场景里有个绝佳例子:下单前的风控校验链 。一次下单请求,要依次经过一连串检查------库存是否充足、是否超过限购、用户是不是黑名单、是否触发风控规则......这些检查有先后、会增减(明天可能又要加个"实名认证校验")。如果把它们全塞进一个方法里用 if 层层嵌套,那个方法会变成一个又深又乱、加一个检查就要动一次的泥潭。责任链模式,就是把每项检查做成一个独立的"处理节点",串成一条链,请求顺着链走,清爽又可扩展。
这篇文章按这条线索展开:先看"一堆校验用嵌套 if 堆在一起"的困境;再引出责任链如何把每项处理拆成链上的节点;然后讲清它的角色、以及"链怎么组装、请求怎么流转"这个关键;接着看它在 Servlet 过滤器、网关、拦截器里无处不在的身影;最后辨析它与其他模式、给出适用边界。贯穿例是下单风控链。
目录
- [一堆校验用嵌套 if 堆在一起的困境](#一堆校验用嵌套 if 堆在一起的困境)
- 责任链模式:把处理者串成一条链
- 角色,与链的组装和流转
- 现实身影:过滤器、网关、拦截器
- 责任链的两种形态,以及什么时候用
一、一堆校验用嵌套 if 堆在一起的困境
看下单前的风控校验。最直觉的写法,是在下单方法里把所有检查一项项 if 下来:
java
public class OrderService {
public void placeOrder(OrderRequest req) {
// 1. 库存校验
if (!inventory.enough(req)) {
throw new RuntimeException("库存不足");
}
// 2. 限购校验
if (purchase.exceedLimit(req)) {
throw new RuntimeException("超过限购数量");
}
// 3. 黑名单校验
if (blacklist.contains(req.getUserId())) {
throw new RuntimeException("用户在黑名单");
}
// 4. 风控规则校验
if (riskControl.hit(req)) {
throw new RuntimeException("触发风控");
}
// ... 明天要加"实名认证校验"?继续往这堆
doPlaceOrder(req); // 全过了才真正下单
}
}
这段代码能跑,但毛病很典型:
- 所有校验挤在一个方法里 :
placeOrder越来越长,库存、限购、黑名单、风控全糊在一起,违反单一职责。这个方法会长到没人愿意读。 - 违反开闭原则 :每加一项校验(实名认证、地址校验),都得回来撬开这个方法加一段
if。改一次,就要重新测试整个下单流程。 - 顺序和组合写死:校验的顺序被硬编码了。如果某个场景(比如内部测试下单)想跳过黑名单校验、或者调整校验顺序,你没法灵活配置,只能改代码。
- 每项校验难以复用 :库存校验这段逻辑,别的地方(比如加购物车)也想用,但它被埋在
placeOrder里,拎不出来。
问题的根源是:多个"平行的处理步骤",被硬编码成了一长串固定的 if。 每一项校验本是独立的、可增减、可重排的,却被焊死在一个方法里。我们真正想要的是:把每一项校验做成一个独立的"节点",像串珠子一样串成一条链;请求顺着链往下走,每个节点各管一项,通过就传给下一个、不通过就拦下。加校验就往链上加一个节点,调顺序就调节点的串接顺序。 这就是责任链。
二、责任链模式:把处理者串成一条链
责任链的做法:定义一个抽象处理者,它持有"下一个处理者"的引用;每个具体处理者只做自己那一项处理,做完后把请求传给下一个;把这些处理者一个个串起来,就形成一条链。
第一步,定义抽象处理者(关键是它持有 next------指向下一个节点):
java
public abstract class OrderHandler {
protected OrderHandler next; // 指向链上的下一个处理者
public OrderHandler setNext(OrderHandler next) {
this.next = next;
return next; // 返回 next,方便链式组装
}
// 处理请求。做完自己的事,决定要不要传给下一个
public abstract void handle(OrderRequest req);
// 一个小工具:把请求传给下一个节点(如果有)
protected void handleNext(OrderRequest req) {
if (next != null) {
next.handle(req);
}
// next 为 null,说明到链尾了,校验全部通过
}
}
第二步,每项校验是一个具体处理者:
java
public class InventoryHandler extends OrderHandler {
public void handle(OrderRequest req) {
if (!inventory.enough(req)) {
throw new RuntimeException("库存不足"); // 不通过,拦下,链中断
}
handleNext(req); // 通过,交给下一个节点
}
}
public class LimitHandler extends OrderHandler {
public void handle(OrderRequest req) {
if (purchase.exceedLimit(req)) throw new RuntimeException("超过限购");
handleNext(req);
}
}
public class BlacklistHandler extends OrderHandler {
public void handle(OrderRequest req) {
if (blacklist.contains(req.getUserId())) throw new RuntimeException("黑名单");
handleNext(req);
}
}
第三步,组装链,然后把请求丢给链头:
java
// 组装:库存 → 限购 → 黑名单
OrderHandler chain = new InventoryHandler();
chain.setNext(new LimitHandler())
.setNext(new BlacklistHandler());
// 请求从链头进入,自动顺着链走
chain.handle(orderRequest);
对比第一节,升级点非常清晰:每项校验被拆成一个独立的、可复用、可单独测试的节点;OrderService 不再关心"有哪些校验、什么顺序",它只管把请求丢给链头;要加"实名认证校验",写个 RealNameHandler 串进链里就行;要调顺序,改组装顺序就行------都不用碰已有的校验节点。 这就同时满足了单一职责和开闭原则。用一张图看这个"请求沿链流转"的结构最清楚:

图里最该记住的,是那条请求顺着节点一路往下传的"链" ,以及每个节点手里那个指向"下一个"的引用。每个节点只知道"我的下一个是谁",不知道整条链的全貌------正是这种"局部只关心下一步"的设计,让链可以任意加长、任意重排,而节点本身不用改。这和链表的思想如出一辙。
三、角色,与链的组装和流转
责任链的角色很简洁,两个:
| 角色 | 本例中是谁 | 职责 |
|---|---|---|
| 抽象处理者(Handler) | OrderHandler |
定义处理接口,持有 next 引用 |
| 具体处理者(ConcreteHandler) | InventoryHandler 等 |
处理自己负责的部分,决定是否传给下一个 |
(注意:发起请求的"客户端"通常也被算作一个隐含角色------它负责组装链、并把请求丢给链头。)
理解责任链,关键在于每个节点手里那个"传不传给下一个"的决定权。节点处理完自己的逻辑后,有两种选择:
- 传下去 :调用
handleNext(req),把请求交给下一个节点继续处理(比如校验通过); - 不传 :直接返回、或抛异常、或返回一个结果,中断整条链(比如校验不通过,后面的节点就不用走了)。
这个"传或不传"的决定权在每个节点自己手里,正是"责任链"名字的由来------每个节点决定"这个请求的责任,是我担下来(处理并中断),还是往后传"。 而这又引出两种不同的链的"语义":
- "一票否决"式(如我们的风控校验):每个节点都必须通过,任何一个拦下就中断。请求要走完整条链才算成功。这是最常见的形态。
- "找到能处理的就停"式:请求沿链传递,直到遇到第一个"有能力处理它"的节点,处理完就结束,后面的不再走。比如审批流程------小额报销组长就批了、大额才往上传到总监,一旦有人批了就结束。
两种语义结构一样,区别只在节点内部"什么时候中断、什么时候传下去"的逻辑。理解了这一点,你就能用同一套结构应对不同的业务。
四、现实身影:过滤器、网关、拦截器
责任链是框架里应用极广的模式,尤其在"请求要经过一串处理"的场景:
- Servlet 的 Filter(过滤器链) :这是责任链最经典的实现。一个 HTTP 请求进来,要依次经过编码过滤器、鉴权过滤器、日志过滤器......每个
Filter处理完调chain.doFilter()把请求传给下一个。你写一个自定义 Filter 加进链里,就能拦截所有请求做统一处理------加过滤器不动别的,正是责任链的开闭优势。 - Spring MVC 的拦截器(HandlerInterceptor):请求到达 Controller 前,依次经过一串拦截器(登录检查、权限检查等),也是责任链。
- 网关的过滤器链(Spring Cloud Gateway、Zuul):API 网关里,请求要经过一长串过滤器(限流、鉴权、改写、路由......),是责任链在微服务层面的应用。
- Netty 的 ChannelPipeline :网络数据的编解码、业务处理,被组织成一条
ChannelHandler的流水线,数据顺着流水线一站站处理,是责任链的高性能实现。 - OkHttp 的拦截器链 :HTTP 请求/响应经过一串
Interceptor,也是责任链。
一个识别信号:凡是"一个请求要依次经过一串可插拔的处理环节",尤其是名字里带 Filter、Interceptor、Pipeline、Chain 的,基本都是责任链。 它是构建"处理流水线"的标准模式。
五、责任链的两种形态,以及什么时候用
补充一个实现上的细节:责任链有两种常见的组织形态:
- 链表式 (我们上面用的):每个节点自己持有
next引用,自己负责调用下一个。结构纯粹,但组装稍繁琐。 - 列表式 (框架常用):不用每个节点持有
next,而是把所有处理者放进一个List,由一个"链的执行器"按顺序遍历调用。Servlet 的FilterChain、很多框架的实现都是这种------更好管理、更容易动态增删和配置。两者思想一致,只是"谁来驱动流转"不同(节点自驱 vs 执行器驱动)。
适合用责任链的信号:
- 一个请求需要经过多个处理环节 ,这些环节会增减、顺序可能调整;
- 你想让每个处理环节独立、可复用、可单独测试,并能灵活组装;
- 发送者不需要知道"到底谁会处理这个请求",只管丢给链头。
不必用的信号:
- 就固定两三个检查,永远不变------那直接顺序调用/几个
if更直白,套责任链是过度设计; - 处理环节之间有复杂的相互依赖、需要来回交互------责任链是"单向流水",不擅长这种,那可能是别的场景。
几个注意点:
- 链一定要有终点 :要么某个节点处理并中断,要么请求走到链尾被妥善处理。别让请求走到链尾却没人处理还悄无声息------那会变成一个难查的 bug(请求"消失"了)。
- 别把链搞得太长太隐蔽:链太长时,一个请求到底经过了哪些节点、在哪被拦下,排查起来会比较绕。保持链清晰、每个节点职责单一。
判断的核心还是那句话:先确认真的存在"一串会增减的、可独立处理的环节",责任链才值得上。 给固定不变的两三步硬套责任链,凭空多出一堆 Handler 类和组装代码,是典型的过度设计。
小结 。责任链模式把多个处理者串成一条链,请求沿链传递,每个节点各管一段、决定"处理并中断"还是"传给下一个",从而把一长串硬编码的 if 校验,拆成独立、可复用、可灵活组装的节点------新增或重排处理环节,不动已有节点。它有"一票否决"和"找到能处理的就停"两种语义,以及链表式和列表式两种组织形态。Servlet Filter、网关过滤器、Spring 拦截器、Netty Pipeline,都是它的身影,是构建"处理流水线"的标准模式。用它要注意"链一定要有终点"。下一篇我们讲状态模式------它和策略结构几乎一样,但意图相反:策略是"平行地选一个算法",状态是"对象随自身状态在不同行为间流转",典型如订单从"待付款"一步步走到"已完成"的状态机。