写一个返回 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 与连接关闭。
五、自己沿源码读一遍
建议先打开三个位置:
HttpTrafficHandler.channelRead:请求在哪里变成上下文任务;DispatcherHandler:找处理器、执行、返回值处理怎么组织;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 可以依赖哪些能力、服务器实现必须兑现哪些行为。检查抽象是否合适,要回到请求生命周期,而不是只看接口签名。
如果是你来划分边界,哪些能力放进统一请求上下文,哪些责任留给服务器适配层? 欢迎在评论里聊聊你的实现或使用经历。