Netty 是怎么把 HTTP 请求交给 MVC 的?一个请求的完整链路

写一个返回 Hello 的接口很简单:

java 复制代码
import infra.web.annotation.GET;
import infra.web.annotation.RestController;

@RestController
class DemoController {

  @GET("/index")
  public String index() {
    return "Hello";
  }
}

这个字符串从控制器返回值变成浏览器里的 HTTP 响应,中间发生了什么?本文沿 TODAY Infrastructure 的一个普通同步 HTTP 请求,拆解 Netty 与 MVC 的衔接过程,以及统一请求上下文需要承担哪些责任。

说明:上面是框架示例里控制器的局部摘录,不是独立可启动项目。完整代码见 DemoController。本文源码链接固定在该提交。

一、先看整条链路

从应用代码看,我们声明了映射并返回字符串,框架还要完成五件事:接收请求、找到处理器、执行处理器、解释返回值、写回响应。先展开:

text 复制代码
Netty 已解码的 HttpRequest
  → HttpTrafficHandler 创建请求上下文
  → HttpRequestExecutor 执行上下文任务
  → HttpContext.run
  → DispatcherHandler 分发请求
  → 查找并执行处理器
  → ReturnValueHandler 处理返回值
  → 同步请求完成,Netty 实现写回响应

这是帮助阅读的简化链路。过滤器、异常、异步请求、请求体的后续到达都会改变实际路径,不能当作所有请求都逐行经过的固定轨迹。

二、Netty 在哪里把请求交给 MVC

衔接入口在 HttpTrafficHandler.channelRead。

这里收到的是 Netty 已解码的 HttpRequest。处理器创建 Netty 包内部的请求上下文,然后:

java 复制代码
executor.execute(httpContext, httpContext);

把执行委托出去;后续到达的 HttpContent 也会交给这个上下文处理。

有两个容易混淆的地方。

1. 请求头到达 ≠ 请求体完整

收到一个 HttpRequest,不代表已经拿到完整请求体。框架的上下文要衔接后续数据,不能把两者等同。

2. 委托执行 ≠ 固定线程模型

委托给 HttpRequestExecutor 不等于必然切换到线程池,也不等于默认用虚拟线程。要回答"控制器在哪个线程执行",还得继续看执行器的具体实现与配置。这篇先讲职责交接。

内部上下文实现了 Runnable,它的 run 方法 处理解码错误等情况,正常请求进入 DispatcherHandler.handleRequest,并在 finally 中清理上下文持有者。

到这里,连接侧与 MVC 侧接上了。

三、分发器把三个问题交给不同策略

进入 DispatcherHandler 不代表它独自完成全部工作。拆成三个问题看更清楚。

第 1 问:谁来处理这个请求?

HandlerMapping 负责查找处理器。委托位置见 DispatcherHandler。

第 2 问:怎样执行找到的处理器?

处理器可能直接实现 HttpRequestHandler,也可能需要由 HandlerAdapter 包装执行。这样分发器只组织流程,不必把每种处理器的执行细节都塞进同一个类。

第 3 问:怎样处理执行结果?

处理器返回后由 ReturnValueHandler 处理。应用里的 return "Hello" 只是这一阶段的输入,并不等于底层响应已经写入。

可以顺着 处理器执行与结果处理 和 异常、返回值处理 读下去。

四、统一 HttpContext,统一的是什么

MVC 面向 infra.web.HttpContext,不直接面向 Netty 的连接与请求对象。

注意源码里有两个同名类型:

  • infra.web.HttpContext:MVC 使用的统一接口;
  • infra.web.server.netty.HttpContext:Netty 包内部的具体实现,继承 NettyHttpContext,同时是上下文任务的执行入口。

统一抽象减少了 MVC 对 Netty 细节的直接依赖。但接口统一了,底层责任不会消失:请求体怎么读、响应何时提交、连接状态如何处理、资源什么时候清理,仍要服务器适配层认真完成。同步请求完成后的写回看 NettyHttpContext 的 flush 与写入处理。这里衔接底层写入并处理 keep-alive 与连接关闭。

五、自己沿源码读一遍

建议先打开三个位置:

  1. HttpTrafficHandler.channelRead:请求在哪里变成上下文任务;
  2. DispatcherHandler:找处理器、执行、返回值处理怎么组织;
  3. NettyHttpContext.flush:结果怎么交回底层连接。

先连起来,再深入线程执行、控制器匹配和返回值转换,方向不容易丢。

六、运行示例

如果想跑起来,克隆框架,在 infra-samples 目录执行:

bash 复制代码
git clone https://github.com/TAKETODAY/today-infrastructure.git
cd today-infrastructure/infra-samples
./gradlew infraRun

另开终端:

bash 复制代码
curl -i http://localhost:8080/index

预期 HTTP 200,正文 Hello。端口被占用可用 --server.port=8081 换端口。

验证记录:

  • 2026-10-11:框架提交 88f5240e85859d64eb3c9543a528afb41b53519f、macOS ARM64、JDK 25.0.1、Gradle 9.8.0,全新 Gradle 用户目录与空 Maven Local,默认端口与 8081 均返回 HTTP 200 / Hello。
  • 2026-10-13:用提交 a880d8b5ba6bd73e65333fd73ec0679bd9b130df 的干净导出副本,JDK 25.0.3 / Gradle 9.8.0 构建 netty-app 发行包,同一份发行包在 Liberica JDK 17.0.16+12-LTS 下启动,GET /index 返回 HTTP 200 / Hello,日志确认应用使用 Java 17.0.16;验证后进程停止、端口关闭。

范围说明:

  • 示例依赖从 Maven 仓库解析的 5.0-Draft.8-SNAPSHOT,不是从上述提交编译的框架 JAR,快照内容可能变化。
  • JDK 17.0.16 通过只证明默认示例这条路径;虚拟线程、WebSocket、SSE、HTTP/2、数据库、JNI 等功能的最低 JDK 仍需分别验证。
  • 直接 git clone 拿的是仓库当前默认分支,不等于固定到验证提交。按自己本地的提交、框架版本和 JDK 为准。
  • 这篇不讨论"这个框架有多少功能",只拆一条具体的请求链路。

结语

统一请求上下文的价值不只是"换一个接口名",而是明确 MVC 可以依赖哪些能力、服务器实现必须兑现哪些行为。检查抽象是否合适,要回到请求生命周期,而不是只看接口签名。

如果是你来划分边界,哪些能力放进统一请求上下文,哪些责任留给服务器适配层? 欢迎在评论里聊聊你的实现或使用经历。

相关推荐
蚰蜒螟1 小时前
从内核启动到 Java NIO:一次 socket accept 的源码串联之旅
java·python·nio
会编程的吕洞宾2 小时前
Spring AI 2.0 自校正结构化输出实战:让大模型稳定吐出合格 JSON
java·人工智能·后端·spring
bmxy小明同学2 小时前
2026-10-09-token与成本
java·前端·数据库
微笑的曙光3 小时前
第八期 · 情绪闯关挑战盒:一个「无惩罚」成就系统的数据建模
java·微信小程序
java资料站3 小时前
七、Spring AI Alibaba · Skills
java·后端·spring
code2cat3 小时前
Java进阶篇之AtomicReferenceFieldUpdater:用不可变快照替换整份状态
java·开发语言·cas
geats人山人海3 小时前
现代c++2.4 虚继承
java·开发语言·c++
灯澜忆梦4 小时前
【面向对象编程c++】| 数组与函数
java·数据结构·c++
我命由我123454 小时前
Android 广播 - 组件通信
android·java·开发语言·android studio·android jetpack·android-studio·android runtime