主题
责任链与命令:把流程拆成可编排的单元
本文是设计模式系统学习系列的 L2 核心篇。前置:6. 策略与模板方法。 学完可以配合面试题食用:拦截器 vs Filter vs AOP
责任链:每个环节有各自的决定权
业务逻辑里常有这样的场景:一个请求进来,要经过多个前置检查(鉴权、限流、风控、参数校验),每个检查都可能"拒绝通过"或"放行"。
最直接的写法是 if-else 串起来:
java
if (!auth(req)) return;
if (!rateLimit(req)) return;
if (!riskCheck(req)) return;
if (!validate(req)) return;
// 处理业务...这样写的问题是:所有检查写死在同一个方法里。新增一个检查要改代码,想调整检查顺序也要改代码。更糟的是,一个检查失败后返回的时机和方式不同,容易漏掉某个分支。
责任链模式的解决思路很简单:把每个检查节点做成一个 Handler,每个 Handler 持有下一个 Handler 的引用,请求沿链传递,各节点决定处理还是下传。
java
public abstract class Handler {
protected Handler next;
public void setNext(Handler next) { this.next = next; }
public abstract void handle(Request req);
}定一个抽象父类,next 指针指向下一个节点。每个子类在 handle 里做自己的逻辑,然后决定是否调用 next.handle()。
两种责任链:纯链 vs 过滤器链
GoF 定义的"纯责任链"是 找到即止:链上有一个 Handler 能处理这个请求,剩下的就不跑了。比如 Java 异常处理——catch 到 Exception 就不会继续 catch 上层。
但在工程框架里,更常见的是 过滤器链(Filter Chain):每个节点都执行,只是顺序可控。一个典型的例子是 Servlet Filter:
java
@WebFilter("/*")
public class AuthFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
// 前置处理
if (!checkAuth(req)) { resp.sendError(401); return; }
// 放行到下一个 Filter 或 Servlet
chain.doFilter(req, resp);
// 后置处理
log("auth done");
}
}纯责任链和过滤器链的区别:
| 特性 | 纯责任链(GoF 经典) | 过滤器链(工程常见) |
|---|---|---|
| 执行策略 | 找到处理者即终止 | 所有节点依次执行 |
| 代表场景 | 异常处理、日志级别 | Servlet Filter、Netty Pipeline |
| 控制权 | 节点决定是否继续传递 | 每个节点做前置+后置,链保证遍历 |
Netty 的 ChannelPipeline 是过滤器链最强的实现之一:每个 ChannelHandler 可以决定是否调用 ctx.fireChannelRead(msg),在调用前做解码、调用后做编码,形成完整的入站/出站双向链。
链的装配:可插拔才是关键
责任链真正的价值不在写法,在装配方式。写死 HandlerA.setNext(HandlerB).setNext(HandlerC) 换汤不换药,真正的可插拔让链的组成在运行时决定。
工程落地常见做法:
- 注解 + 排序:给每个 Handler 打
@Order(1)、@Order(2)注解,框架自动收集并排序 - 配置文件:外部 YAML 配置链的节点列表和顺序,不改代码就能调
- Spring 自动注入:
@Autowired List<Handler>收集所有实现,按@Order排序后组装
java
@Component @Order(1)
public class AuthHandler extends AbstractHandler { ... }
@Component @Order(2)
public class RateLimitHandler extends AbstractHandler { ... }
@Component
public class HandlerChainBuilder {
@Autowired
private List<AbstractHandler> handlers;
public AbstractHandler buildChain() {
handlers.sort(Comparator.comparingInt(h -> h.getClass().getAnnotation(Order.class).value()));
for (int i = 0; i < handlers.size() - 1; i++) {
handlers.get(i).setNext(handlers.get(i + 1));
}
return handlers.isEmpty() ? null : handlers.get(0);
}
}这样,新增一个风控节点,只要写一个类加 @Component @Order(3),链自动组装,零侵入。
命令模式:把请求打包成对象
命令模式的出发点不同:把"你要做什么"封装成一个对象,而不是直接在调用方写调用逻辑。
java
public interface Command {
void execute();
}看上去和 Runnable 一模一样。没错,Runnable 就是最简的命令模式实现。
为什么需要把一个方法调用包成对象?因为对象可以存储、排队、重放、撤销:
- 存储:把用户的每一步操作记录下来,方便审计回放
- 排队:命令对象可以放进队列,按序执行或延迟执行
- 撤销:保存上一个命令的逆操作,execute 对应 undo
- 组合:MacroCommand 可以组合多个 Command 一起执行
java
// 接收者
class Light {
void on() { System.out.println("灯开"); }
void off() { System.out.println("灯关"); }
}
// 命令
class LightOnCommand implements Command {
private Light light;
LightOnCommand(Light light) { this.light = light; }
public void execute() { light.on(); }
public void undo() { light.off(); }
}
// 调用者
class RemoteControl {
private Command slot;
private Command lastCommand;
void setCommand(Command cmd) { this.slot = cmd; }
void press() { slot.execute(); lastCommand = slot; }
void undo() { lastCommand.undo(); }
}这种"遥控器按键"场景在工程里不那么常见,但命令模式的核心思想——将请求的发起者和执行者解耦——到处在用。Java 的 ThreadPoolExecutor 内部就是放 Runnable 进工作队列,Worker 线程取出执行,这是命令模式在并发框架里的实证。
责任链 + 命令组合实战:审批流
两种模式经常一起出现。一个典型的审批流:每个审批节点是一个命令,它们按顺序组成责任链。
java
// 审批命令
class ApprovalCommand implements Command {
private String approver;
private boolean approved;
ApprovalCommand(String approver) { this.approver = approver; }
public void execute() {
// 模拟审批:同意则继续,拒绝则终止
approved = Math.random() > 0.3;
System.out.println(approver + " 审批 " + (approved ? "通过" : "拒绝"));
}
boolean isApproved() { return approved; }
}
// 责任链中的 Handler
class ApprovalHandler extends Handler {
private String approver;
ApprovalHandler(String approver) { this.approver = approver; }
public void handle(Request req) {
ApprovalCommand cmd = new ApprovalCommand(approver);
cmd.execute();
if (cmd.isApproved() && next != null) {
next.handle(req);
}
}
}每个审批节点既是一个命令(封装了"某人审批这个请求"的操作),又是责任链的一环(决定是否继续传递)。这样组合,每一步都可重放、可审计,链的组成可配置。
动手实操:手写风控责任链
下面实现一个完整的风控责任链,支持可插拔节点和短路机制(某个节点拒绝后不再继续检查):
java
import java.util.*;
// 请求对象
class RiskRequest {
String userId;
double amount;
String ip;
RiskRequest(String userId, double amount, String ip) {
this.userId = userId; this.amount = amount; this.ip = ip;
}
}
// 结果
class RiskResult {
boolean passed;
String reason;
RiskResult(boolean passed, String reason) {
this.passed = passed; this.reason = reason;
}
}
// 责任链节点
abstract class RiskHandler {
protected RiskHandler next;
public void setNext(RiskHandler next) { this.next = next; }
public RiskResult handle(RiskRequest req) {
RiskResult r = doCheck(req);
if (!r.passed) return r; // 短路:拒绝就不再继续
if (next != null) return next.handle(req);
return new RiskResult(true, "全部通过");
}
protected abstract RiskResult doCheck(RiskRequest req);
}
// 具体节点
class BlacklistHandler extends RiskHandler {
private Set<String> blacklist = Set.of("u_black01", "u_black02");
protected RiskResult doCheck(RiskRequest req) {
if (blacklist.contains(req.userId))
return new RiskResult(false, "黑名单用户");
return new RiskResult(true, "黑名单检查通过");
}
}
class AmountLimitHandler extends RiskHandler {
private static final double MAX_AMOUNT = 50000;
protected RiskResult doCheck(RiskRequest req) {
if (req.amount > MAX_AMOUNT)
return new RiskResult(false, "单笔超限");
return new RiskResult(true, "金额检查通过");
}
}
class IpRiskHandler extends RiskHandler {
private Set<String> highRiskIps = Set.of("1.2.3.4");
protected RiskResult doCheck(RiskRequest req) {
if (highRiskIps.contains(req.ip))
return new RiskResult(false, "高风险IP");
return new RiskResult(true, "IP检查通过");
}
}
// 使用
public class RiskChainDemo {
public static void main(String[] args) {
RiskHandler h1 = new BlacklistHandler();
RiskHandler h2 = new AmountLimitHandler();
RiskHandler h3 = new IpRiskHandler();
h1.setNext(h2); h2.setNext(h3);
RiskRequest req = new RiskRequest("u_normal", 30000, "192.168.1.1");
System.out.println(h1.handle(req).reason); // 输出:金额检查通过 -> IP检查通过 -> 全部通过
RiskRequest badReq = new RiskRequest("u_black01", 100, "0.0.0.0");
System.out.println(h1.handle(badReq).reason); // 输出:黑名单用户(短路)
}
}这个实现体现了核心思想:每个节点只关注自己的检查逻辑,不关心上下游是谁。要新增一个"同设备多账号"检查,加一个类,链上插一下就行,已有代码一行不改。
常见误区与小结
- 误区:责任链就是 Filter。Filter 是责任链的变体(过滤器链),纯责任链找到即止,过滤器链每个都执行,Netty Pipeline 和 Servlet Filter 属于后者。
- 误区:命令模式就是多一层包装。区别在于 Runnable 是命令,但直接
new Thread(() -> { work(); }).start()不是命令模式,因为命令模式的调用者(Invoker)和执行者(Receiver)是分开的,而 Runnable 把两者合并在匿名函数里了。 - 误区:责任链装好之后不能改链的结构。工程上可以用动态代理 + 配置中心做到运行时热更新链的节点顺序。
- 误区:所有 if-else 换责任链都更好。节点少于 3 个、变化频率低的情况下,if-else 更清晰,上责任链是过度设计。
- 误区:命令模式在生产很少用。事务日志的回滚记录、操作审计的 replay、工作流引擎的节点编排,都是命令模式或其变体的工程化应用。
小结:责任链解决"多个检查/处理阶段如何编排"的问题,核心是让每个节点可插拔、可排序;命令模式解决"如何把调用封装成对象"的问题,核心是让请求可存储、可排队。两者组合起来,可以构建出可配置、可审计、可热更新的业务处理流。
参考
- 《设计模式:可复用面向对象软件的基础》第 5 章 - 行为型模式(责任链、命令)
- Netty 源码:
io.netty.channel.DefaultChannelPipeline— 双向链表的过滤器链实现 - Spring Security 源码:
org.springframework.security.web.FilterChainProxy— 安全过滤器链的装配与遍历