责任链模式可能是行为型模式里"应用密度最高"的一个 —— Express / Koa / Spring Security / Servlet Filter / Netty Pipeline / Redux 中间件,几乎所有需要"一串处理器依次处理同一个请求"的地方,你都能找到它。这篇文章把责任链的本质讲透,从最朴素的 if/else 重构开始,走到 HTTP 中间件、审批流、异常处理链,讲清楚它和管道、过滤器、装饰器的边界。
问题:if/else 长得像意大利面
看一段处理用户请求的代码,常见而典型:
public Response handle(Request req) {
// 1. 校验登录
if (req.getToken() == null) return Response.unauthorized();
User user = validateToken(req.getToken());
// 2. 校验权限
if (!user.hasRole("admin")) return Response.forbidden();
// 3. 限流
if (rateLimiter.isExceeded(user.getId())) return Response.tooManyRequests();
// 4. 参数校验
if (req.getBody() == null || !isValid(req.getBody())) return Response.badRequest();
// 5. 缓存查询
Response cached = cache.get(req.cacheKey());
if (cached != null) return cached;
// 6. 真正的业务逻辑
Response resp = businessLogic(req);
// 7. 写日志
auditLog.write(req, resp);
return resp;
}
所有横切关注点都堆在一个方法里。问题:
- 这个方法做太多事,违反单一职责。
- 每个 if 之间互相纠缠 —— 想换顺序、想加一步、想跳过某步都得动这个大方法。
- 很难复用 —— 别的 endpoint 也要登录 + 权限,但流程里某些步骤不一样。
- 测试要构造一堆假数据走完整链路。
责任链模式说:把每个步骤拆成一个独立的"处理器",串成一条链。请求依次经过每个处理器,任一处理器可以处理它或传给下一个。
责任链的标准结构
// 1. 抽象处理器
abstract class Handler {
protected Handler next;
public Handler setNext(Handler next) {
this.next = next;
return next; // 返回 next 是为了链式 setNext
}
public Response handle(Request req) {
if (next != null) return next.handle(req);
return null; // 链尾
}
}
// 2. 具体处理器:每个只关心一件事
class AuthHandler extends Handler {
public Response handle(Request req) {
if (req.getToken() == null) return Response.unauthorized();
req.setUser(validateToken(req.getToken()));
return super.handle(req); // 传给下一个
}
}
class RoleHandler extends Handler {
public Response handle(Request req) {
if (!req.getUser().hasRole("admin")) return Response.forbidden();
return super.handle(req);
}
}
class RateLimitHandler extends Handler {
public Response handle(Request req) {
if (rateLimiter.isExceeded(req.getUser().getId())) return Response.tooManyRequests();
return super.handle(req);
}
}
class CacheHandler extends Handler {
public Response handle(Request req) {
Response cached = cache.get(req.cacheKey());
if (cached != null) return cached; // 短路返回
Response resp = super.handle(req);
if (resp.isCacheable()) cache.put(req.cacheKey(), resp);
return resp;
}
}
class BusinessHandler extends Handler {
public Response handle(Request req) {
return businessLogic(req); // 链尾,真正干活
}
}
// 3. 组装链
Handler chain = new AuthHandler();
chain.setNext(new RoleHandler())
.setNext(new RateLimitHandler())
.setNext(new CacheHandler())
.setNext(new BusinessHandler());
// 4. 触发
Response resp = chain.handle(req);
对比改造前后:每个 Handler 短而聚焦,顺序由组装决定;新加一步 = 新加一个 Handler;某 endpoint 不需要权限校验,组装时跳过 RoleHandler 即可。
责任链的两种语义
不同实现下,责任链表现出两种不同的语义,要分清:
纯责任链:某一个处理者负责到底
请求沿链流动,直到某个处理者认为"我能处理"就吃掉它,后续不再经过。例:异常处理(每层 catch 不到就向上传),日志级别过滤(找到第一个能处理这个 level 的 logger)。
// Logger 责任链
abstract class Logger {
int level;
Logger next;
public Logger setNext(Logger n) { next = n; return n; }
public void log(int level, String msg) {
if (this.level <= level) write(msg);
if (next != null) next.log(level, msg); // 全链流动
}
abstract void write(String msg);
}
过滤链 / 管道:所有处理者都参与
请求依次经过每个处理者,每个都可以读写、装饰、短路。Servlet Filter、Express 中间件就是这种。上面 AuthHandler / CacheHandler 的例子也是。
区别在于:纯责任链强调"找到第一个能处理的",过滤链强调"按顺序全部经过"。前者是"或",后者是"与"。工程里后者更常见,因为它对横切关注点的支持更天然。
实战 1:Express / Koa 中间件
Node.js 后端开发的"日常责任链":
// Express
app.use(express.json()); // 解析 JSON body
app.use(cors()); // CORS
app.use(morgan('combined')); // 日志
app.use(authMiddleware); // 鉴权
app.use('/api', rateLimitMiddleware); // 限流(部分路径)
app.use(errorHandler); // 错误处理(末端)
app.get('/api/users', (req, res) => { ... }); // 业务
// 自定义中间件
function authMiddleware(req, res, next) {
if (!req.headers.authorization) return res.status(401).send();
req.user = decode(req.headers.authorization);
next(); // 传递给下一个
}
// Koa 用 async/await,语义更清晰
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // 等下游处理完
console.log(`${ctx.method} ${ctx.url} ${Date.now() - start}ms`);
});
Koa 的"洋葱模型"特别值得记:next() 是异步等待,所以可以在调用前后都执行代码 —— 一个中间件像"洋葱皮"包裹着下游,请求一层层进、响应一层层出。这种结构让"前置 + 后置"逻辑非常自然。
实战 2:Spring Security 过滤链
Spring Security 是一条由 20+ 个 Filter 组成的责任链:
// 部分核心 Filter 顺序(简化)
SecurityContextPersistenceFilter // 从 session 加载 SecurityContext
HeaderWriterFilter // 设置 X-Frame-Options 等头
CsrfFilter // CSRF 校验
LogoutFilter // /logout 端点处理
UsernamePasswordAuthenticationFilter // 表单登录处理
BasicAuthenticationFilter // HTTP Basic
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
RememberMeAuthenticationFilter // RememberMe cookie
AnonymousAuthenticationFilter // 匿名用户处理
SessionManagementFilter // 会话管理
ExceptionTranslationFilter // 安全异常 -> HTTP 响应
FilterSecurityInterceptor // 最终的访问控制决策
每个 Filter 只做一件事。你想加自定义 Token 校验?写一个 Filter 插进合适位置就行,不改其他 Filter。这种"高度可插拔"就是责任链的核心价值。
实战 3:审批流
请假审批的经典需求:小额组长批,中额部门经理批,大额 HR + CEO 批。
abstract class Approver {
protected Approver next;
public Approver setNext(Approver n) { this.next = n; return n; }
public final void process(LeaveRequest req) {
if (canApprove(req)) approve(req);
else if (next != null) next.process(req);
else reject(req);
}
abstract boolean canApprove(LeaveRequest req);
abstract void approve(LeaveRequest req);
void reject(LeaveRequest req) { System.out.println("无人可批"); }
}
class TeamLead extends Approver {
boolean canApprove(LeaveRequest r) { return r.getDays() <= 3; }
void approve(LeaveRequest r) { System.out.println("组长批准"); }
}
class Manager extends Approver {
boolean canApprove(LeaveRequest r) { return r.getDays() <= 7; }
void approve(LeaveRequest r) { System.out.println("部门经理批准"); }
}
class HRDirector extends Approver {
boolean canApprove(LeaveRequest r) { return r.getDays() <= 30; }
void approve(LeaveRequest r) { System.out.println("HR 总监批准"); }
}
Approver chain = new TeamLead();
chain.setNext(new Manager()).setNext(new HRDirector());
chain.process(new LeaveRequest(2)); // 组长批准
chain.process(new LeaveRequest(5)); // 部门经理批准
chain.process(new LeaveRequest(15)); // HR 总监批准
chain.process(new LeaveRequest(60)); // 无人可批
这是"纯责任链"的典型 —— 每个 Approver 决定要不要处理,处理了就不再往下。
实战 4:异常处理 / 全局错误兜底
app.use((err, req, res, next) => {
if (err instanceof ValidationError) {
return res.status(400).json({ code: 'VALIDATION', message: err.message });
}
next(err); // 不处理就传下去
});
app.use((err, req, res, next) => {
if (err instanceof AuthError) {
return res.status(401).json({ code: 'AUTH', message: err.message });
}
next(err);
});
app.use((err, req, res, next) => {
console.error('未捕获错误', err);
res.status(500).json({ code: 'INTERNAL', message: 'something went wrong' });
});
错误处理链让你能按异常类型分别处理,且新加异常类型时不影响其他处理器。这是责任链在 Web 框架里最常见的应用之一。
实战 5:Netty Pipeline
Netty 的 ChannelPipeline 是责任链的另一种工业级实现 —— 每个 ChannelHandler 处理 inbound 或 outbound 事件,可以解码、加密、路由、限流等:
ServerBootstrap b = new ServerBootstrap();
b.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new LineBasedFrameDecoder(1024)) // 拆帧
.addLast(new StringDecoder()) // 字节 -> 字符串
.addLast(new StringEncoder()) // 字符串 -> 字节
.addLast(new BusinessHandler()); // 业务
}
});
同一条连接的所有读写都顺着 pipeline 走,inbound 从前往后,outbound 从后往前。这种"双向责任链"是高性能网络编程的标配设计。
责任链 vs 装饰器 vs 命令
这三个容易混:
- 责任链:多个处理器串联,每个决定要不要处理 / 短路。重点是"谁来处理"。
- 装饰器:多层包装同一个对象,每层加点行为。重点是"增强了什么"。
- 命令:把"操作"封装成对象,可以排队、撤销、记录。重点是"把操作做成数据"。
装饰器和责任链在结构上有很大相似性(都是嵌套包装),但意图不同 —— 装饰器一定会调内层,责任链可能短路。
动态构建责任链
实际项目里链常常需要按配置或权限动态构建:
List<Handler> chain = new ArrayList<>();
chain.add(new AuthHandler());
if (config.rateLimitEnabled()) chain.add(new RateLimitHandler());
if (user.isVip()) chain.add(new VipPreprocessHandler());
chain.add(new BusinessHandler());
// 用 List 风格执行,而不是链表
public Response execute(List<Handler> chain, Request req, int idx) {
if (idx >= chain.size()) return null;
return chain.get(idx).handle(req, () -> execute(chain, req, idx + 1));
}
这种"List + 索引"实现比"链表 + next 指针"更灵活 —— 可以反向迭代、可以跳到任意位置、可以动态插拔。Express、Koa 的中间件就是 List 实现,不是链表。
函数式责任链:Lambda 即处理器
// JS 风格
type Handler = (req: Request, next: () => Response) => Response;
function compose(handlers: Handler[]): Handler {
return (req, terminal) => {
function next(i: number): Response {
if (i >= handlers.length) return terminal();
return handlers[i](req, () => next(i + 1));
}
return next(0);
};
}
const middleware = compose([
(req, next) => { if (!req.token) return unauthorized(); return next(); },
(req, next) => { console.log('handling'); return next(); },
(req, next) => processBusinessLogic(req),
]);
middleware(request, () => defaultResponse());
这种实现没有类、没有继承,纯靠高阶函数。Express / Koa / Redux 中间件本质就是这种结构。
常见坑
坑 1:忘了 next。 Express 中间件最常见的 bug:req.user = ...; 但没调 next(),请求挂在那里直到超时。每个中间件要么调 next,要么显式响应(不调 next),不能两个都不做。
坑 2:next 调多次。 同样的中间件如果不小心调了两次 next,可能导致下游异步处理重复运行。规范:next 调一次就立刻 return。
坑 3:错误在异步链路里被吞。 中间件里 throw 同步异常框架能 catch,但 await 出来的异常如果没 try/catch,默默丢失。Koa 用 async/await,可以让框架统一捕获;Express 要么显式 .catch(next),要么用 express-async-errors。
坑 4:链太长难调试。 一个请求穿过 20 个中间件,出错时栈跟踪深得没法看。建议加 request-id,每个中间件入口打印 "[reqId] handler-name",出错能立刻定位是哪一步。
坑 5:中间件之间隐式依赖。 "RateLimitHandler 必须在 AuthHandler 之后"是个约定,但代码里看不出来。如果有人换了顺序,bug 就出现了。可以让 Handler 声明 requires() 让框架检查依赖,或者写文档明示。
测试责任链
// 测试一个中间件
@Test
public void authMiddleware_rejects_unauthorized() {
AuthHandler h = new AuthHandler();
Handler next = mock(Handler.class);
h.setNext(next);
Request req = new Request();
req.setToken(null);
Response resp = h.handle(req);
assertEquals(401, resp.getStatus());
verify(next, never()).handle(any()); // 不应调用下游
}
每个 Handler 独立测试,verify 它"调或不调"下游 —— 这是责任链可测性的标志。
写在最后
责任链模式不只是设计模式,它是"一切横切关注点的标准答案"。鉴权、限流、缓存、日志、监控、错误处理、Tracing —— 这些通用能力如果都写在业务方法里,业务代码就被淹没;放进责任链 / 中间件,业务方法瞬间变干净。
给一个工程信号:当一段代码在多个 endpoint / Service / 方法里反复出现"先做 X 检查再做实际工作",而 X 又是和业务无关的横切逻辑 —— 就是责任链在喊你。把这些 X 抽到中间件 / Handler 链里,业务代码就只剩业务逻辑。这种"关注点分离"是现代后端框架最重要的设计哲学,而它的具体实现,几乎全靠责任链模式。
—— 别看了 · 2026