通过 Netty 的 Pipeline 学习责任链设计模式

通过 Netty 的 Pipeline 学习责任链设计模式

在日常开发中,我们经常会使用或听说"设计模式"。其中,责任链模式(Chain of Responsibility) 是一种非常实用、且容易被忽略的行为型设计模式。今天我想借助我们熟悉的 Netty 框架,带大家深入理解责任链模式,并看看它是如何优雅地在 Netty 的 ChannelPipeline 中落地实现的。文章会先介绍责任链模式的完整结构,然后通过Netty的实践,加深对该模式的理解。


什么是责任链设计模式?

责任链模式的核心思想是:多个处理对象形成一个链条,每个处理对象持有对下一个处理对象的引用,沿着链条传递请求,直到有对象处理它为止。

特点:

  • 请求发送者和接收者解耦。
  • 动态添加、删除责任处理者。
  • 增强可扩展性和灵活性。

举个例子:

想象你去请假,公司中请假流程是这样的:

  1. 小于 1 天:直属主管审批;
  2. 1~3 天:部门经理审批;
  3. 超过 3 天:总经理审批。

这个审批流程本身就构成了一个责任链结构。每一级判断是否自己能处理,如果不能,则将请求传给下一级。

通过实际问题理解设计模式

问题

  • 假如你正在开发一个在线订购系统。 你希望对系统访问进行限制, 只允许认证用户创建订单。 此外, 拥有管理权限的用户也拥有所有订单的完全访问权限。

  • 简单规划后, 你会意识到这些检查必须依次进行。 只要接收到包含用户凭据的请求, 应用程序就可尝试对进入系统的用户进行认证。 但如果由于用户凭据不正确而导致认证失败, 那就没有必要进行后续检查了。

在接下来的几个月里, 你实现了后续的几个检查步骤。

  • 一位同事认为直接将原始数据传递给订购系统存在安全隐患。 因此你新增了额外的验证步骤来清理请求中的数据。
  • 过了一段时间, 有人注意到系统无法抵御暴力密码破解方式的攻击。 为了防范这种情况, 你立刻添加了一个检查步骤来过滤来自同一 IP 地址的重复错误请求。
  • 又有人提议你可以对包含同样数据的重复请求返回缓存中的结果, 从而提高系统响应速度。 因此, 你新增了一个检查步骤, 确保只有没有满足条件的缓存结果时请求才能通过并被发送给系统。
  • 检查代码本来就已经混乱不堪, 而每次新增功能都会使其更加臃肿。 修改某个检查步骤有时会影响其他的检查步骤。 最糟糕的是, 当你希望复用这些检查步骤来保护其他系统组件时, 你只能复制部分代码, 因为这些组件只需部分而非全部的检查步骤。

  • 系统会变得让人非常费解, 而且其维护成本也会激增。 你在艰难地和这些代码共处一段时间后, 有一天终于决定对整个系统进行重构。

解决方案

  • 与许多其他行为设计模式一样, 责任链会将特定行为转换为被称作处理者的独立对象。 在上述示例中, 每个检查步骤都可被抽取为仅有单个方法的类, 并执行检查操作。 请求及其数据则会被作为参数传递给该方法。
  • 模式建议你将这些处理者连成一条链。 链上的每个处理者都有一个成员变量来保存对于下一处理者的引用。 除了处理请求外, 处理者还负责沿着链传递请求。 请求会在链上移动, 直至所有处理者都有机会对其进行处理。
  • 最重要的是: 处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤。
  • 在我们的订购系统示例中, 处理者会在进行请求处理工作后决定是否继续沿着链传递请求。 如果请求中包含正确的数据, 所有处理者都将执行自己的主要行为, 无论该行为是身份验证还是数据缓存。
  • 不过还有一种稍微不同的方式 (也是更经典一种), 那就是处理者接收到请求后自行决定是否能够对其进行处理。 如果自己能够处理, 处理者就不再继续传递请求。 因此在这种情况下, 每个请求要么最多有一个处理者对其进行处理, 要么没有任何处理者对其进行处理。 在处理图形用户界面元素栈中的事件时, 这种方式非常常见。

  • 例如, 当用户点击按钮时, 按钮产生的事件将沿着 GUI 元素链进行传递, 最开始是按钮的容器 (如窗体或面板), 直至应用程序主窗口。 链上第一个能处理该事件的元素会对其进行处理。 此外, 该例还有另一个值得我们关注的地方: 它表明我们总能从对象树中抽取出链来。

所有处理者类均实现同一接口是关键所在。 每个具体处理者仅关心下一个包含 execute执行方法的处理者。 这样一来, 你就可以在运行时使用不同的处理者来创建链, 而无需将相关代码与处理者的具体类进行耦合。

真实世界问题类比

  • 最近, 你刚为自己的电脑购买并安装了一个新的硬件设备。 身为一名极客, 你显然在电脑上安装了多个操作系统, 所以你会试着启动所有操作系统来确认其是否支持新的硬件设备。 Windows 检测到了该硬件设备并对其进行了自动启用。 但是你喜爱的 Linux 系统并不支持新硬件设备。 抱着最后一点希望, 你决定拨打包装盒上的技术支持电话。

  • 首先你会听到自动回复器的机器合成语音, 它提供了针对各种问题的九个常用解决方案, 但其中没有一个与你遇到的问题相关。 过了一会儿, 机器人将你转接到人工接听人员处。

  • 这位接听人员同样无法提供任何具体的解决方案。 他不断地引用手册中冗长的内容, 并不会仔细聆听你的回应。 在第 10 次听到 "你是否关闭计算机后重新启动呢?" 这句话后, 你要求与一位真正的工程师通话。

  • 最后, 接听人员将你的电话转接给了工程师, 他或许正缩在某幢办公大楼的阴暗地下室中, 坐在他所深爱的服务器机房里, 焦躁不安地期待着同一名真人交流。 工程师告诉了你新硬件设备驱动程序的下载网址, 以及如何在 Linux 系统上进行安装。 问题终于解决了! 你挂断了电话, 满心欢喜。

责任链模式结构

责任链模式包含以下角色:

  • Handler(抽象处理者):定义处理请求的接口,并持有下一个处理者的引用。

  • ConcreteHandler(具体处理者):处理请求或将请求传递给下一个处理者。

  • Client:构建责任链,并触发请求。


Netty中责任链模式的典范:ChannelPipeline

Netty 是一个高性能的异步事件驱动网络通信框架,在它的核心组件 ChannelPipeline 中,正好使用了责任链模式。

Pipeline 是什么?

ChannelPipeline 是 Netty 中的数据处理流程抽象,主要负责事件的传递。我们可以在其中添加多个 ChannelHandler,每个 Handler 负责处理特定的业务逻辑,例如编解码、鉴权、业务逻辑处理等等。

来看一下常见的 Pipeline 架构图:

rust 复制代码
[Inbound] -> Decoder -> AuthHandler -> BusinessHandler -> ...
                                      <- ResponseEncoder <- [Outbound]

每一个 ChannelHandler 就是责任链中的一个节点,通过 ctx.fireXXX() 进行链式传递。


源码剖析:责任链模式在 Netty 中的体现

Netty 中的 ChannelPipeline 由一系列 ChannelHandlerContext 组成的双向链表构成,每一个 ChannelHandlerContext 包装了一个具体的 ChannelHandler

添加一个 Handler 时:

java 复制代码
pipeline.addLast(new MyDecoder());
pipeline.addLast(new AuthHandler());
pipeline.addLast(new BusinessHandler());

Netty 内部维护的是一组上下文节点:

text 复制代码
head <-> MyDecoderCtx <-> AuthHandlerCtx <-> BusinessHandlerCtx <-> tail

当某个事件(比如 channelRead)被触发时,Netty 会从 head 节点开始调用链上的 handler,直到某个节点消费掉事件或传递到 tail 为止。

关键方法:

java 复制代码
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    // 处理逻辑
    ctx.fireChannelRead(msg); // 继续传递
}

是不是很眼熟?这就是责任链模式的标准套路。


自定义一个 Handler 演示责任链

java 复制代码
public class LoggingHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("[LoggingHandler] 收到消息: " + msg);
        ctx.fireChannelRead(msg); // 继续传递给下一个 handler
    }
}

多个 handler 组合在 pipeline 中,便形成了一个灵活可控的责任链。


小结:责任链模式的几个优势,在 Netty 中被充分发挥

设计目标 Netty 中的实现
解耦发送者和处理者 Handler 彼此独立,通过 ctx 传递事件
支持动态插拔 pipeline.addBeforeremove 方法
顺序执行、可中断 可按需调用 ctx.fireXXX 或中断
事件驱动,自由组合 可组合 Inbound / Outbound Handler

推荐阅读:

  • 《Netty 实战》:全面掌握 Netty 的实战用法
  • 《设计模式之美》:图解常见设计模式,简明易懂
  • Netty 源码:DefaultChannelPipeline.java

最后

如果文章对你有帮助,点个免费的赞鼓励一下吧!关注公众号:加瓦点灯, 每天推送干货知识!

相关推荐
2025学习1 小时前
Spring循环依赖导致Bean无法正确初始化
后端
l0sgAi1 小时前
最新SpringAI 1.0.0正式版-实现流式对话应用
后端
parade岁月1 小时前
从浏览器存储到web项目中鉴权的简单分析
前端·后端
用户91453633083912 小时前
ThreadLocal详解:线程私有变量的正确使用姿势
后端
用户4099322502122 小时前
如何在FastAPI中实现权限隔离并让用户乖乖听话?
后端·ai编程·trae
阿星AI工作室2 小时前
n8n教程:5分钟部署+自动生AI日报并写入飞书多维表格
前端·人工智能·后端
郝同学的测开笔记2 小时前
深入理解 kubectl port-forward:快速调试 Kubernetes 服务的利器
后端·kubernetes
Ray662 小时前
store vs docValues vs index
后端
像污秽一样3 小时前
软件开发新技术复习
java·spring boot·后端·rabbitmq·cloud
Y_3_73 小时前
Netty实战:从核心组件到多协议实现(超详细注释,udp,tcp,websocket,http完整demo)
linux·运维·后端·ubuntu·netty