Java Web 不只是 Spring Boot:从七层网络模型到 Servlet / Netty / WebFlux 全家桶
写了很多年 Controller,还是搞不清“请求从哪儿来的”?
老实说,你可能经历过这样的场景:
写 Java Web 一上来就是 spring-boot-starter-web,然后:
-
@RestController -
@GetMapping("/hello") -
return "world";
服务跑起来,浏览器里敲 http://localhost:8080/hello,页面乖乖返回 "world"。
久而久之,我们会默认一个“非常危险”的前提:
Java Web = Spring Boot = 写 Controller。
直到有一天:
面试问你:“一个 HTTP 请求是怎么走到你的 Controller 的?”
排查线上问题时,发现:
-
前面有 Nginx、有 网关、有 Ingress、还有各种限流、超时、重试……
-
你在日志里看到的,只是整个链路最末端的一小截。
这时候你会发现:
我会写 Controller,但我对“这个请求从哪儿来、经过了什么”,其实并不清楚。
这篇文章想做的,就是帮你搭一张 “从网络到 Java Web 的全景图”:
第一,用 七层网络模型 搭一个坐标系;
第二,把 HTTP / TCP / Socket / NIO / Netty 放回各自的位置;
第三,再看 Servlet / Spring MVC / WebFlux 各自站在哪一层;
最后,你就能回答:
-
为什么我们说“Spring MVC 是建立在 Servlet 之上的”?
-
Netty 和 Servlet 的“段位”到底差在哪里?
-
云原生架构图里 Nginx / Gateway / Ingress 和你的 Java Web 之间,是怎样的关系?
先从大地图开始:七层网络模型。
先用七层网络模型,给 Java Web 找个“坐标系”
OSI 七层模型 & 工程里常用的简化版
课本上那幅经典图,你大概见过很多次了:
OSI 七层模型:
1)物理层(Physical)
2)数据链路层(Data Link)
3)网络层(Network)
4)传输层(Transport)
5)会话层(Session)
6)表示层(Presentation)
7)应用层(Application)
如果只背名字,很快就忘;我们用一种更“工程”的方式来记,每层一句话:
物理层:负责“0 和 1”怎么在线上飞
-
电压、电流、光信号、无线电波。
数据链路层:在“一个局域网”里,负责“谁是谁 + 帧传输”
-
MAC 地址、交换机、ARP、VLAN。
网络层:负责“跨网络转发,到达目标 IP”
-
IP、路由、
traceroute。
传输层:负责“端到端的连接与传输”
-
TCP(可靠、有序、面向连接)、UDP(简单、无连接)。
会话层 / 表示层 / 应用层:
-
会话层:对话管理(会话建立、恢复)
-
表示层:编码、加密、压缩(数据“长什么样”)
-
应用层:HTTP、FTP、SMTP、DNS、MQTT、你自定义的协议……
现实世界里,大家为了方便,往往用一个简化的“TCP/IP 四层模型”:
应用层 HTTP、gRPC、WebSocket、MQTT、DNS、你的自定义协议……
传输层 TCP、UDP
网络层 IP
链路层 以太网、Wi-Fi……
(再往下是物理层的电信号/光信号)
只要抓住这件事就够了:
Java Web 所有东西,都在“传输层之上、应用层之内”。
再具体一点:
-
HTTP 是应用层协议;
-
TCP/UDP 是传输层协议;
-
Socket 是操作系统提供给应用程序、用来操作 TCP/UDP 的“门把手”;
-
NIO/Netty/Servlet/Spring MVC/WebFlux,都是站在这几层之上,提供各种“编程模型”。
HTTP / TCP / Socket / NIO / Netty 在哪一层?
我们把几个关键名词放到刚才的坐标系里:
HTTP:标准的应用层协议
规范里写的是:
-
请求行:
GET /path HTTP/1.1 -
各种 Header:
Host、Content-Type、Cookie…… -
Body 怎么组织,状态码是什么意思(200 / 404 / 500 …)。
它默认下面有一个“可靠、有序的传输层”:TCP。
你写的
@RestController,本质就是在处理 HTTP 请求/响应。
TCP:传输层协议
负责:
-
三次握手、四次挥手;
-
保证数据不丢、不乱序;
-
做流量控制、拥塞控制。
在 Java 程序里,你不直接操作 TCP 报文,而是通过 Socket 来使用它。
Socket:应用程序操作传输层的“门把手”
操作系统提供的一组 API 模型(socket() / bind() / listen() / accept() / read() / write())。
你可以在一个 TCP Socket 上:
-
跑 HTTP;
-
跑 gRPC;
-
跑游戏服务器自己的二进制协议;
-
等等。
Socket 自己不是协议,它只是让你“拿到一条 TCP/UDP 管道”的编程接口。
NIO:用少量线程玩很多 Socket 的方式
传统阻塞 I/O:一连接一线程,read() / write() 会阻塞。
NIO 提供:
-
SocketChannel(可配置为非阻塞) -
Selector(一个线程监听成千上万连接的就绪事件)
好处是:
-
几个 I/O 线程就能顶住大量连接;
-
代价是编程模型更复杂(事件驱动 + 状态机)。
Netty:站在 NIO 之上的“网络编程框架”
帮你封装这些 NIO 细节:
-
EventLoopGroup、ChannelPipeline、ChannelHandler……
支持:
-
HTTP、WebSocket、TCP/UDP、自定义二进制协议、各种 RPC 协议……
所以 Netty 更贴近“传输层 + 应用层之间”的位置,是很多中间件 / 网关 / RPC 框架的底座。
一句总结:
HTTP 在应用层;TCP 在传输层;Socket 是操作 TCP 的把手;NIO 是高效玩 Socket 的手法;Netty 是站在 NIO 上的网络框架。
一张图:从浏览器到 Java 进程的协议堆栈
用一张“横切图”把刚才这些拼在一起:
[浏览器 / 客户端]
↓ HTTP 请求报文 (GET /api/hello HTTP/1.1 ...)
[HTTP 协议 - 应用层]
↓ 作为 payload
[TCP - 传输层]
↓ 封装成 IP 包
[IP - 网络层]
↓ 封装成以太网帧
[以太网 / Wi-Fi - 链路层/物理层]
↓ 网线/光纤/无线 信号
================== 网络传输 ==================
[服务器网卡收到帧]
↑
[内核网络栈:以太网 → IP → TCP]
↑ 找到对应端口
[内核:该端口对应的进程(Java)]
↑
[Java 进程:Socket / NIO / Netty / Tomcat]
↑ 解析 HTTP
[Servlet / Spring MVC / WebFlux 等框架]
↑
[你的 Controller / Handler 方法]
后面我们讲 Servlet 时,会把上半部分“服务器侧”的细节放大。
Servlet 技术栈:Java Web 的默认“世界观”
理解完大坐标系,接下来我们把视角切到 Java 这边:
你最熟悉的 Spring MVC,其实是“骑在 Servlet 之上”的。
要搞懂这句话,先看 Servlet 自己到底规定了什么。
Servlet 规范到底规定了什么?
Servlet 不是一个“某家公司写的框架”,而是 Jakarta(原 Java EE)里的一个规范。
这份规范主要规定了几件事:
1)容器应该怎么做?
帮你监听某个端口(比如 8080);
接收 TCP 连接;
把收到的字节按 HTTP 协议解析成请求;
然后构造出几个 Java 对象交给你:
-
ServletRequest/ServletResponse -
对 HTTP 来说:
HttpServletRequest/HttpServletResponse
2)你的应用可以实现什么接口?
实现 javax.servlet.Servlet(或者继承 HttpServlet);
覆盖 doGet() / doPost() 等方法;
在里面读取参数、写回响应。
3)请求处理的“拦截链”:Filter
Filter` 可以在 Servlet 之前/之后执行:
-
日志、鉴权、跨域、参数包装、统一异常处理等;
使用 FilterChain 形成责任链。
用一句更“抽象”的话总结:
Servlet 规范把“HTTP 请求到达 Java 代码”的最后一段流程标准化了。
这也是为什么:
-
你可以从 Tomcat 换成 Jetty / Undertow / 应用服务器;
-
只要它们都实现了 Servlet 规范,你的 Servlet / Spring MVC 代码基本不用改。
从网卡到 doGet():一次 HTTP 请求在 Servlet 栈里的旅程
我们把“服务器这一侧”的路径放大,看得再细一点(典型 Servlet 栈 + Tomcat):
1)网卡收到数据帧
底层:以太网帧 → IP 包 → TCP 段;
内核网络栈根据目标端口(例如 8080)找到对应进程(Tomcat)。
2)JVM 里的 Servlet 容器接管
Tomcat 的 Acceptor 线程在监听端口,接受连接;
每当某个连接有数据可读,Tomcat 从 Socket 中读出字节流。
3)HTTP 协议解析
Tomcat 内部的 HTTP 解析器负责:
-
解析请求行(方法、路径、版本);
-
解析各个 Header;
-
解析 Body(包括表单、JSON、文件上传等)。
4)封装为 HttpServletRequest / HttpServletResponse
容器创建这两个对象;
把解析过的请求信息挂在 HttpServletRequest 上:
-
URL、Query 参数、Header、Cookie、Body 输入流;
把要写回去的东西通过 HttpServletResponse 提供给你:
-
状态码、Header、输出流 / Writer。
5)走 Filter 链
容器会构造一个 FilterChain;
顺序调用你配置好的每个 Filter:
-
日志记录 Filter;
-
鉴权 Filter;
-
编码 / 跨域处理 Filter;
最后,一个 Filter 调用 chain.doFilter(request, response),把请求交给下一个;
直到走到最后,把请求交给目标 Servlet。
6)调用 Servlet 的 service() / doGet() / doPost()
容器选择合适的方法(例如 GET → doGet());
进入你的业务逻辑代码:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().write("hello servlet");
}
}
这个过程里,你不需要关心 TCP、Socket,只是在操作 Java 对象。
7)写回响应 → 经 TCP → IP → 网卡 → 浏览器
resp.getWriter().write(...) 写入响应体;
容器往输出流里写完整的 HTTP 响应报文:
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: ...
hello servlet
通过 TCP 发送回客户端。
用一句话缩成一条线:
网卡 → 内核网络栈 → Tomcat Socket → HTTP 解码 →
HttpServletRequest/Response → Filter 链 →
Servlet.doGet()/doPost() → 写响应 → HTTP 编码 → TCP → 网卡
你在 Servlet 或 Spring MVC 里写的 Controller,其实都站在这条链的 最上面。
Spring MVC 是怎么“骑在” Servlet 上的?
理解这一点,你就能把“Servlet 世界观”和“Spring 世界观”拼起来了。
在 Spring MVC 眼里,世界大致是这样的:
1)自己只做“应用层的调度 & 参数绑定”
路由:URL 到某个处理方法(Handler Method)
参数解析:Query / PathVariable / Body → Java 对象
视图解析:返回 String / ModelAndView / 对象(转 JSON)
2)HTTP 细节交给 Servlet 容器
如何监听端口、解析 HTTP、构造 Request/Response,全交给 Tomcat/Jetty 等;
Spring MVC 只看:
-
HttpServletRequest里面的参数; -
HttpServletResponse怎么写。
因此,在一个典型的 Spring Boot Web 项目里,请求链大致是:
浏览器
↓ HTTP
Tomcat (Servlet 容器)
↓ 创建 HttpServletRequest / HttpServletResponse
Filter 链(包括 Spring 自己的过滤器)
↓
DispatcherServlet(Spring MVC 核心 Servlet)
↓ HandlerMapping 找到哪个 Controller 方法
↓ HandlerAdapter 调用该方法
↓ 返回值交给 HttpMessageConverter 转成 JSON / 文本 / HTML
↓ 写入 HttpServletResponse
↓ 容器通过 TCP 发回浏览器
注意关键点:
-
Spring MVC 自己也是一个 Servlet:
DispatcherServlet; -
Spring Boot 启动时,会帮你自动注册这个 Servlet 到容器里;
-
所以说:
“Spring MVC 是建立在 Servlet 技术栈之上的”
本质上就是:它把所有 HTTP → Controller 的细节,统一塞进了一个核心 Servlet(DispatcherServlet)里。
你可以换 Tomcat → Jetty → Undertow,只要它们都是 Servlet 容器,
Spring MVC 这一层几乎不用动。
Netty 技术栈:当 Servlet 还不够用的时候
前面说了这么多 Servlet / Spring MVC,看起来已经能搞定绝大多数 Web 项目了,那为什么还会有 Netty 这一挂?
一句话先压个标题:
当你不再满足于“只写 HTTP 业务接口”,
而是想自己「玩连接 / 玩协议 / 玩高并发网络」,你就会遇到 Netty。
为什么会有 Netty:阻塞 I/O vs NIO 模型
先从最原始的问题说起:
如果我想写一个“接受很多客户端连接”的服务器,用最传统的
ServerSocket会发生什么?
传统阻塞 I/O:一连接一线程
最原始、最朴素的写法,大概长这样(简化示意):
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept(); // 阻塞等待连接
new Thread(() -> {
try (InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream()) {
// 阻塞读写,处理请求
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
特点是:
-
accept()阻塞等待新连接; -
每来一个连接,就
new Thread专门处理; -
线程里用
read()/write()来阻塞读写这个 Socket。
问题也很明显:连接少的时候,这样很好写;当同时在线的连接数量很多时:
-
线程数量会暴涨;
-
每个线程都有栈内存开销;
-
线程上下文切换频繁;
-
CPU 花在“调度线程”上的时间开始超过“真正干活”的时间。
这就是“一连接一线程”模型的天然瓶颈。
NIO 模型:用少量线程管理大量连接
Java 后来搞出了一套 NIO(New I/O):
-
ServerSocketChannel/SocketChannel:可设置为 非阻塞; -
Selector:用一个线程轮询一堆 Channel 的“就绪事件”(可读/可写/有连接)。
伪代码长这样(仅示意结构):
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(8080));
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞等待“有事件发生”
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 有新连接
} else if (key.isReadable()) {
// 某个连接有数据可读
} else if (key.isWritable()) {
// 某个连接可以写数据
}
}
}
特点变成:
-
只要一个 I/O 线程(或者少量几个)就能管理大批量连接;
-
每个连接不再占用一个固定线程;
-
以“事件”驱动:有连接、有数据可读、有数据可写 → 触发对应处理逻辑。
这就是当下高并发网络编程的标准姿势,Netty 做的事,就是把这些底层 NIO 细节封装成一个“程序员友好很多”的框架。
Netty 解决的核心问题
Netty 的官方一句话通常是类似:
一个基于 NIO 的高性能网络应用框架。
你可以理解为,它帮你解决了这些麻烦事:
1)I/O 线程模型怎么设计?
-
EventLoopGroup:管理 I/O 线程; -
Boss Group:负责接收连接;
-
Worker Group:负责处理读写事件。
2)连接生命周期 & 事件流转怎么组织?
-
Channel:代表一条连接; -
ChannelPipeline:连接上的“处理流水线”; -
ChannelHandler:你写的处理逻辑。
3)协议怎么编解码?
-
字节流 → 消息对象:Decoder;
-
消息对象 → 字节流:Encoder;
-
内置了 HTTP、WebSocket、Protobuf 等各种编解码器;
-
你也可以自定义二进制协议。
4)零拷贝、内存管理、性能优化怎么做?
ByteBuf:
-
读写指针;
-
池化;
-
规避频繁分配/回收。
5)异常、超时、心跳检测、空闲连接清理……
-
这些都可以通过
ChannelHandler插在 Pipeline 里统一处理。
它适合什么场景?
-
大量长连接:IM 聊天、推送服务器、在线游戏网关;
-
自定义协议:RPC 框架、物联网设备协议;
-
高吞吐网关:某些 API Gateway、反向代理、代理层;
-
需要极致性能、精细控制网络行为的系统。
也可以一句话简单粗暴概括:
Servlet 栈更像“给你一个 HTTP 世界”;
Netty 则更像“给你一整套玩 Socket / 协议 / 连接的积木箱”。
Servlet 栈 vs Netty 栈:用一个对比表说明
为了把差异说清楚,我们用一张对比表来整理:
| 维度 | Servlet 技术栈(Tomcat + Spring MVC) | Netty 技术栈 |
|---|---|---|
| 抽象层级 | 主要面向 HTTP 应用层 | 更贴近 TCP/UDP + 应用层协议的交界处 |
| 协议 | 天然就是 HTTP(Servlet 规范就是 HTTP) | HTTP / WebSocket / 自定义二进制协议 / MQTT / RPC 等 |
| I/O 模型 | 传统:一请求一个线程(线程池),阻塞 I/O 现代容器会做一些优化,但本质思路类似 |
事件驱动 + NIO,少量 I/O 线程管理大量连接 |
| 开发门槛 | 非常低:写 doGet() 或 @RestController 即可 |
相对高:需要理解事件驱动、Channel、Pipeline、Handler 等 |
| 生态 | Spring MVC / Spring Boot / Filter / JSP / 各种成熟组件 | 更多用于中间件 / 网关 / RPC 框架 / 游戏 / IM / 物联网等 |
| 典型使用者 | 业务开发同学 | 中间件/基础设施/网络服务开发同学 |
| 典型场景 | 后台管理系统、REST API、企业内部系统、常规微服务 | 网关、IM 聊天、游戏服务器、物联网、行情推送、自研 RPC |
所以,当你写业务系统时:
你可能没直接碰过 Netty;
但你已经间接在用了:
-
Spring WebFlux 默认使用 Reactor Netty;
-
某些 RPC 框架(如 gRPC Java 的某些实现)、消息组件、网关底层也依赖 Netty。
Spring MVC vs WebFlux:两个时代的 Java Web 编程模型
现在我们有了两条“底层基因谱系”:
-
Servlet 栈:HTTP + 阻塞 I/O + 请求/线程模型;
-
Netty 栈:NIO + 事件驱动 + 多协议。
Spring 在两边都插了一脚:
-
Spring MVC:站在 Servlet 栈上;
-
Spring WebFlux:站在 Reactor Netty(或其它 reactive server)上。
这一节就来重点讲一下:
Spring MVC vs WebFlux,到底差在哪里?用什么场景?代码长什么样?
Spring MVC:阻塞 + 每请求一个线程
Spring MVC 是我们最熟悉的 Web 编程模型:
-
@Controller/@RestController -
@GetMapping/@PostMapping -
方法返回值是对象 / 字符串,框架帮你自动序列化成 JSON/HTML。
底层模型:基于 Servlet + 线程池
典型 Spring Boot Web 项目(spring-boot-starter-web):
第一,嵌入一个 Tomcat/Jetty/Undertow;
第二,容器维护一个工作线程池(比如 maxThreads=200);
第三,每当有 HTTP 请求进来:
-
从线程池里取一个线程处理;
-
线程调用
DispatcherServlet→ Controller; -
在整个处理周期中,这个线程都“被占用”。
如果你的 Controller 做这些事情:
-
调用数据库(阻塞);
-
调用下游 HTTP 接口(阻塞);
-
调用 Redis、MQ(阻塞)……
那这个线程就一直等在那里,直到全部返回,然后写回响应。
对开发者来说,这是非常友好的:代码写起来就像“同步直觉式”。
适用场景
-
标准企业业务系统;
-
大部分增删改查类后台;
-
常规 REST API 微服务;
-
不要求极致连接数 / 极致 IO 吞吐的时候。
WebFlux:响应式 + Reactor Netty
WebFlux 出场时,带了两个关键词:
-
Reactive(响应式):背压、非阻塞、流;
-
Reactor Netty:基于 Netty 的响应式 HTTP/TCP 客户端 & 服务器。
底层模型:事件驱动 + 非阻塞
在 WebFlux 世界里:
底层用的是 Reactor Netty(默认配置):
-
少量 I/O 线程(EventLoop)管理大量连接;
-
整个调用链要尽量保持 非阻塞。
框架引入了响应式类型:
-
Mono<T>:0 或 1 个元素的异步流(类似单值的 Promise); -
Flux<T>:0 到 N 个元素的异步流(类似可观察数据流)。
Controller 写法也从“直接返回对象”,变成“返回一个流(Mono/Flux)”。
典型方式:
每一步 IO 操作(读 DB、调用下游服务)都要用非阻塞客户端:
-
Reactive Repository(R2DBC / Reactive MongoDB);
-
WebClient(Reactive HTTP Client);
把这些异步结果拼在一个 Reactor 流里,一直到写回响应。
整个链路不再是“一请求占一个线程”,而是“线程在事件之间来回流动”。
适用场景
高并发、IO 密集型服务:
-
大量 HTTP 调用、数据库调用、下游服务调用;
需要良好“背压”语义的流式处理:
-
SSE / WebSocket 流 / 长轮询;
对资源利用率要求极高,想用少量线程撑住很多请求。
一眼看懂:同一个接口用 Spring MVC vs WebFlux 各写一遍
下面用一段简单的“查询用户”的接口,对比两边的写法。
实际项目中会更复杂,这里只略微展示风格差异。
用 Spring MVC(阻塞式)的写法
假设你用的是经典的 JPA / MyBatis:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService; // 内部用阻塞式 Repository
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDTO getUserById(@PathVariable Long id) {
// 阻塞调用:线程在这儿等数据库返回
User user = userService.findById(id);
return UserDTO.from(user);
}
}
特点:
-
方法返回的是
UserDTO,不是异步类型; -
Service/Repository 通常用的是阻塞式 JDBC;
-
从 Controller 视角来看,就是“写完就 return”。
用 WebFlux(响应式)的写法
假设你用的是 R2DBC / Reactive Repository:
@RestController
@RequestMapping("/users")
public class UserHandler {
private final ReactiveUserService userService; // 内部用 R2DBC 等响应式组件
public UserHandler(ReactiveUserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public Mono<UserDTO> getUserById(@PathVariable Long id) {
// 返回一个 Mono:代表“未来某个时间点会给你一个 UserDTO”
return userService.findById(id)
.map(UserDTO::from);
}
}
特点:
1)返回的是 Mono<UserDTO>;
2)findById 本身是非阻塞调用;
3)线程不会在 findById 上傻等,而是“注册回调”:
-
查到数据 → 往流里吐一个元素 → 框架负责写入响应;
-
出错 → 流里发 error → 框架统一异常处理。
你可以这样理解两边心智模式的差异:
-
Spring MVC:
“我现在就拿到这个结果,然后返回给你。”
-
WebFlux:
“我返回给你一个‘结果的承诺’(流),
等我真正拿到数据的时候,会通过这条流把结果推过去。”
实战选型建议:什么时候选谁?
这可能是你在项目/面试中最常被问到的一件事。
什么时候优先选 Spring MVC(Servlet 栈)?
建议直接记住:
如果你没有强烈理由用 WebFlux,那就用 Spring MVC。
原因很现实:
1)团队学习成本
-
大多数 Java 开发对同步阻塞 + Servlet 模型更熟悉;
-
Debug、排错、日志追踪都更直觉。
2)生态成熟 & 兼容性更好
-
各种中间件客户端、ORM 框架(MyBatis / JPA)默认都是阻塞模型;
-
和 Spring MVC 天然配合,无需额外适配。
3)大部分业务场景没到“必须响应式”的程度
-
普通 B/S 系统、后台管理、常规微服务;
-
即使用 WebFlux,也很可能是“伪异步”:外面是 WebFlux,里面还是阻塞式调用。
用一句“接地气”的话就是:
绝大多数公司、绝大多数项目、绝大多数时候,Spring MVC 已经足够好。
什么时候可以认真考虑 WebFlux?
下面这些“硬指标”中,只要有 2~3 条满足,就可以考虑:
1)主要瓶颈是 IO,而不是 CPU
-
你的服务主要是在等待数据库 / 下游 HTTP 服务返回;
-
CPU 利用率不高,但线程数很多。
2)需要撑非常多的并发连接
-
比如:网关、API 聚合层、推送服务;
-
或者你希望用相对少量机器顶住更高并发。
3)依赖的基础设施已经有成熟的响应式方案
-
使用 R2DBC 等响应式数据库访问;
-
使用 Reactive Redis / Mongo / WebClient 等组件。
4)团队对响应式编程有兴趣 / 有经验
-
有人熟悉 Reactor;
-
可以接受
Mono/Flux带来的心智负担。
简历 & 面试怎么说?
如果你两边都了解,可以这样组织一段“高质量回答”:
Spring MVC 基于 Servlet,是阻塞 + 一请求一线程的模型,生态成熟、开发门槛低,适合 90% 的常规业务系统;
WebFlux 基于 Reactor Netty,采用事件驱动 + 响应式流,适合 IO 密集、高并发的场景,需要配套使用响应式的下游组件;
在选型上,我会优先选择 Spring MVC,只有在明确存在连接数/线程数瓶颈、或者系统本身就是以流式处理为核心时,才会推动使用 WebFlux。
Nginx / API Gateway / Ingress:你的 Java Web 服务是怎么被“看见”的?
前面咱们一直在“主机内部”打转:
从 TCP、Socket、Servlet、Netty 到 Spring MVC / WebFlux。
但现实里,你的 Java 服务很少是直接暴露在公网上的:
线上一般不会让 java -jar app.jar 直接绑定 80/443;
前面通常会有:
-
Nginx
-
各种 API 网关(Spring Cloud Gateway、Kong、APISIX…)
-
在 K8s 里,还有 Ingress Controller / Service / LB。
这些东西,决定了“外部世界到底是怎么访问到你这个 Java 进程的”。
这一节就讲清楚三个问题:
1)L4 vs L7 是什么?为什么大家老这么叫?
2)Nginx 在 Java Web 架构里一般干啥?
3)API Gateway 相比 Nginx,又多做了哪些事?Ingress 又是什么关系?
L4 vs L7:两种思维方式
先把两个常见名词说清楚:
L4(Layer 4):传输层
-
关心的是:
源 IP + 源端口 + 目标 IP + 目标端口 -
不解析 HTTP、gRPC、MQTT 内容;
-
就像一个快递“中转枢纽”,只看 收件地址,不拆箱看里面是什么。
L7(Layer 7):应用层
-
关心的是:HTTP 的 URL、Header、Method、Body 等;
-
可以根据
/api/user和/api/order做不同路由; -
就像一个“前台 + 分诊台”,会问你“来干什么的”,然后给你分配窗口。
在流量转发时:
L4 代理 / 负载均衡:
-
按 IP / 端口转发:比如
:80 → 多台 :8080 -
看不到 HTTP 的路径、Header;
-
优点:性能高、简单;
-
缺点:太“粗”,无法做细粒度的路由/鉴权。
L7 代理 / 负载均衡:
-
会解析 HTTP 请求;
-
可以按 Host、Path、Header、Method 等做各种决策;
-
适合微服务、API 网关、灰度发布等“业务相关”的流量治理。
记一句就够:
“只看 IP:Port 的是 L4;能看 URL/Header 的是 L7。”
Nginx 在 Java Web 架构里的典型角色
现实里的 Nginx,基本有两种典型角色:
角色 1:HTTP L7 反向代理(最常见)
这是你在 Java 项目里 几乎一定会碰到的 用法:
-
前端访问
https://example.com; -
Nginx 监听 80/443;
-
再
proxy_pass到后面的http://your-java-app:8080。
最常见的配置类似这样(简化):
http {
upstream java_backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://java_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
这里 Nginx 做了什么?
1)监听 80 端口:对外暴露 HTTP 服务;
2)解析 HTTP 请求头、URL;
3)把请求按策略(轮询 / 权重等)转发给后端多个 Java 实例;
4)可以顺便做:
-
SSL 终结(HTTPS → HTTP);
-
基于 Header 的路由;
-
基础的限流、熔断;
-
静态资源缓存。
这就是典型的 L7 代理:会读 HTTP 头、知道 URL。
角色 2:TCP/UDP L4 代理(stream {})
Nginx 还有一个不那么常用、但有时候会用到的模式:stream 块。
比如,你希望把外部的 :443 TCP 流量,转发到内网某个服务:
stream {
upstream tcp_backend {
server 10.0.0.1:9000;
server 10.0.0.2:9000;
}
server {
listen 443;
proxy_pass tcp_backend;
}
}
这里 Nginx:
-
并不解析 HTTP(甚至可能不是 HTTP);
-
只做基于 TCP 的 L4 负载均衡。
对 Java Web 来说,多数情况下你只用 HTTP 反向代理那一面(L7)。
但知道它也可以做 L4,有助于你理解一些复杂部署图。
API Gateway:不只是“反向代理”
在微服务架构里,我们常会听到另一个词:API Gateway(网关)。
和 Nginx 相比,它的特点是:
以“微服务/API 管理”为目标的 L7 网关,功能更业务化。
常见的网关包括:
-
Spring Cloud Gateway
-
Netflix Zuul(老牌)
-
Kong / APISIX / Traefik
-
各大云厂商的 API Gateway 产品
Nginx vs API Gateway:差异在哪?
做个简单对比(“硬核一点”的理解是):
| 能力 | Nginx(HTTP 反向代理) | API Gateway(以 Spring Cloud Gateway 为例) |
|---|---|---|
| 路由粒度 | Host / Path / Header 等 | Host / Path / Header / Method / Body 等 |
| 规则配置 | 配置文件(Nginx 配置语法) | 配置中心 / 动态规则 / 管理后台 |
| 认证授权 | 可自写 Lua / 第三方模块 | 通常有内置的认证插件(JWT / OAuth2 / Key) |
| 限流熔断 | 可以用 Lua / 第三方模块 | 内置限流、熔断、降级、重试等 Filter |
| 身份 & 租户 | 支持但需要自己设计 | 直接提供多租户 / API 级配额等能力 |
| 观测 | Access Log / 自定义日志 | 内建请求统计、Tracing 集成、更细粒度的监控维度 |
可以把 API Gateway 看成:
“以 API 为中心的业务网关 + 反向代理 + 插件系统”。
它不仅关心“流量从 A 转到 B”,还关心:
1)谁在调用?
2)调用多少次?
3)要不要限流?
4)要不要鉴权?
5)要不要灰度到新版本?
在 Kubernetes 里,我们还会看到一个概念:Ingress + Ingress Controller:
-
Ingress:一种 K8s 对“L7 路由规则”的抽象(用 YAML 定义 Host、Path 到 Service 的映射);
-
Ingress Controller:真正干活的 Nginx/Envoy/Traefik/Kong 等。
很多团队的实践是这样的:
-
外层:云厂商 L7 LB + Ingress Controller(Nginx/Envoy);
-
中间:API Gateway(Kong / Spring Cloud Gateway / APISIX)做更复杂的认证/限流;
-
内层:你的 Java 微服务(Spring MVC / WebFlux)。
云原生视角下的 Java Web“全链路”:从浏览器到 Pod
现在我们终于可以把前面所有角色连在一起了:
在一个典型的“Java Web + Kubernetes + Nginx/Gateway”的架构里,
一条真实的 HTTP 请求,到底经历了哪些跳点,在哪些层做了什么?
这一节我们就画一条完整路径,从浏览器一路走到你的 Pod,再走进 Controller。
用一条真实请求路径串起来所有角色
假设你有一个典型的云原生部署:
-
前面有云厂商的负载均衡(或你自己配的 Nginx);
-
集群里用了 Ingress + Service;
-
后端是一个 Spring Boot 微服务(MVC 或 WebFlux)。
一个请求的大致路径可以画成下面这样:
用户浏览器
↓ ① HTTP 请求: https://api.example.com/user/1
云厂商 L7 LB(或外层 Nginx / WAF)
↓ ② 基于 Host/Path 做 L7 负载均衡,转到某个 K8s Node
K8s Ingress Controller(通常是 Nginx/Envoy/Kong 等)
↓ ③ 按 Ingress 规则把请求路由到对应 Service
K8s Service(ClusterIP / NodePort / LoadBalancer)
↓ ④ 在 Pod 实例之间做 L4 负载均衡(IP:Port)
后端 Pod(一个 Java 容器)
↓ ⑤ 容器内的 Spring Boot 应用监听 :8080
嵌入式 Servlet 容器 / Reactor Netty
↓ ⑥ 解析 HTTP、构造 Request、调度到框架
Spring MVC / WebFlux
↓ ⑦ DispatcherServlet / Handler 调用你的 Controller 方法
你的业务逻辑
↓ ⑧ 访问 DB / Redis / 下游服务,返回响应数据
Spring 框架序列化响应对象 → HTTP 响应
↓ ⑨ 经 Netty/Tomcat 写回 TCP → 一路反向返回到浏览器
用户浏览器收到响应,渲染页面或处理 JSON
标一下“它们在几层干活”:
1)云厂商 L7 LB:L7
2)外层 Nginx / WAF:L7
3)Ingress Controller(Nginx/Envoy 等):L7
4)Service(ClusterIP):L4
5)Pod / 容器内的 Java 应用:
-
Reactor Netty / Tomcat:L4 之上、负责 HTTP 层的解析;
-
Spring MVC / WebFlux:应用层框架;
你可以看到:
对于一个 Java Web 程序员来说,
真正“自己写的那一坨代码”,只是这条链路的最后几步而已。
前面那一大串 L4/L7 转发、路由、限流、鉴权、观测能力,
都来自 Nginx / LB / Gateway / Ingress / Service Mesh 等云原生组件。
K8s 几个和 Java 开发强相关的对象
从 Java 开发视角,不需要一口气吃下 K8s 所有概念。至少要搞清楚这几个:
1)Pod / Deployment:你的 Java 服务本体
Pod:
-
K8s 中最小调度单位;
-
Pod 内包含一个或多个容器,你的
openjdk/jre镜像就在里面跑; -
一个 Pod 通常对应一个 JVM 进程,监听某个端口(比如 8080)。
Deployment:
-
管理一组 同构 Pod;
-
指定副本数、副本模板、滚动更新策略等;
-
你的 Java Web 服务,一般就是以 Deployment 的形式存在。
对你来说:
“一个 Java 实例” ≈ “一个 Pod”(简单理解就好)。
2)Service:给一组 Pod 一个“稳定的名字”
Service 的出现就是为了两个问题:
-
Pod IP 会变(重启、扩容、缩容),怎么找它?
-
多个 Pod 副本之间怎么负载均衡?
Service 做的事:
-
给一组 Pod 打标签(Label 选择器);
-
创建一个虚拟 IP(ClusterIP);
-
所有访问 Service IP:Port 的流量,会被转发到这些 Pod 上(L4 负载均衡)。
对 Java 开发来说,可以这么理解:
Service 就是“集群内部的 DNS + 负载均衡 + 服务发现”。
你在代码/配置里写
http://user-service:8080,背后就是在访问这个 Service。
3)Ingress + Ingress Controller:对外暴露 HTTP 服务
Service 解决的是“集群内部 Pod 与 Pod 的通信”;
如果你要对外暴露一个 HTTP 接口(给前端 / 外部调用),就需要 Ingress:
Ingress 做的事:
-
用一份 YAML 定义:
Host + Path → Service的路由; -
比如:
rules:
- host: api.example.com
http:
paths:
- path: /user
backend:
service:
name: user-service
port:
number: 8080
- path: /order
backend:
service:
name: order-service
port:
number: 8080
Ingress Controller:
-
是一个真正跑在集群里的 Nginx/Envoy/Traefik 等;
-
监听 Ingress 资源变化,自动生成对应的路由配置;
-
对外暴露一个固定 IP/域名,作为整个集群的 HTTP 入口。
对 Java 开发来说,最重要的是看懂那条链:
外部域名 -> Ingress Controller -> Service -> Pod -> 你的 Java 进程
Java Web 在云原生里的“生态位”
把这一切拼完,你会发现:
在云原生世界里,Java Web 是“应用层的一块砖”。
你的服务只需要做好几件事:
-
暴露清晰的 HTTP/gRPC 接口;
-
正确处理请求和响应(包括错误码、幂等、重试策略);
-
提供健康检查 / 指标暴露(Actuator / Prometheus endpoint);
-
合理配置超时 / 限流 / 重试(应用内或配合网关)。
至于:
-
怎么让全世界都能访问到你;
-
流量怎么路由到对应服务;
-
灰度发布、蓝绿发布;
-
全链路跟踪、熔断、限流;
-
TLS、WAF、防火墙、DDoS 防护……
这些更多是由:
-
Nginx / API Gateway / Service Mesh / LB / Ingress
-
SRE / 平台团队 / 运维工程师
共同负责。
但这不代表 Java 开发可以完全“不管不问”:
你至少要能看懂那张“流量全链路图”;
出问题时,能跟 SRE 一起排查:
-
是 Ingress 规则错了?
-
是 Gateway 超时?
-
是你服务的线程池打满?
面试时,也能讲清楚:
-
“我的 Java Web 服务在整个云原生架构里的位置是什么?”
-
“从浏览器到我的 Controller 中间经过了哪些层?”
实战建议:从“只会写 Controller”升级为“懂全图的 Java Web 工程师”
前面基本搭完了“理论大图”:
七层网络模型、HTTP / TCP / Socket / NIO / Netty、Servlet / Spring MVC / WebFlux、Nginx / Gateway / Ingress / K8s 全链路。
但如果这些东西只停留在“看着挺懂,一写代码还是只会加 Controller”,那就有点可惜了。
这一节我们就聊点 可执行的升级路径:怎么从“能写业务”成长为“懂全图的 Java Web 工程师”。
学习路线:从你的 Controller 向“上下游”扩展
先把你 现在最熟悉的一点 画出来:
(你最熟) Controller / Handler → Service → Repository → DB/Redis/MQ ...
接下来,可以刻意向 两头扩展:
① 向“上游”扩展:请求怎么来到你的 Controller?
从 Controller 往前推,一路推到浏览器 / 客户端:
浏览器 / 客户端
↓ HTTP
Nginx / 网关 / Ingress
↓ HTTP
Service (L4)
↓ TCP
Pod IP:Port
↓
Tomcat / Reactor Netty
↓
Servlet / Spring MVC / WebFlux
↓
你的 Controller
你可以按这个顺序一点点啃:
1)搞懂 Servlet 请求链路
-
Filter → DispatcherServlet → HandlerMapping → HandlerAdapter → Controller -
建议亲手写一个最小 Spring MVC Demo,自己打几个日志。
2)搞懂 Nginx 反向代理基本配置
-
会看懂并改这几个东西就够用:
server/location/proxy_pass/ 常见超时参数。
3)搞懂 K8s Ingress / Service 的基本用法
-
Service:虚拟 IP + 负载均衡的概念;
-
Ingress:
Host + Path → Service的 YAML 路由逻辑。
② 向“下游”扩展:你的服务是怎么“调别人的”?
从 Controller 往后推,推到你的依赖系统:
Controller
↓
Service
↓
DB / Redis / MQ / 外部 HTTP API / gRPC Service ...
可以重点看两件事:
1)阻塞 vs 非阻塞的调用链
-
Spring MVC 下用
RestTemplate/ 阻塞式 JDBC → 所有调用都“占着线程等”; -
WebFlux 下用
WebClient/ R2DBC → 用 Mono / Flux 把调用链串起来。
2)网络超时 / 重试 / 连接池配置
-
明确谁负责超时、谁负责重试;
-
出问题时,知道是下游接口慢,还是你配置不当。
当你把 “Controller 上下游” 都吃透以后,
你再看一张系统架构图,就不只是 “哦,有个网关、有个服务”,而是能一层层拆开来说:
“这个地方是 L7;这里是 L4;这里走的是 HTTP/1.1,这里是 gRPC,底下统一都跑在 TCP 上。”
一个“必修知识” Checklist:没必要一次全啃,但要有全局感
你可以直接把下面这张清单贴到你的笔记里,当成一个“长期打卡表”。
A. 网络基础(偏“概念层”)
-
[ ] OSI 七层模型 & TCP/IP 四层简化版
-
[ ] HTTP 基础:请求行 / Header / Body / 状态码 / 长连接 / Keep-Alive
-
[ ] TCP 基础:三次握手 / 四次挥手 / 端口 / TIME_WAIT / 粘包拆包概念
-
[ ] Socket 的角色:应用通过 Socket 使用 TCP / UDP
B. Java Web 内核(Servlet 栈 & Netty 栈)
-
[ ] Servlet 规范:
HttpServletRequest/HttpServletResponse/Filter -
[ ] 一次请求在 Tomcat 里的处理流程(线程模型+请求分发)
-
[ ] Spring MVC 核心流程:
DispatcherServlet/ HandlerMapping / HandlerAdapter -
[ ] NIO 基本模型:
Selector/SocketChannel/ 非阻塞 I/O -
[ ] Netty 核心概念:
EventLoopGroup/Channel/ChannelPipeline/ChannelHandler
C. Spring MVC vs WebFlux
-
[ ] 用 Spring MVC 写一个最小的 REST API(阻塞式 DB 调用)
-
[ ] 用 WebFlux + WebClient 写一个简单“聚合下游”接口(Mono/Flux)
-
[ ] 理解“一请求一线程” vs “事件驱动 + 非阻塞”的差异
-
[ ] 知道在什么场景下优先选 Spring MVC,什么时候才会考虑 WebFlux
D. 云原生 & 流量入口
-
[ ] 会看懂并改 Nginx 最常用配置(
proxy_pass、upstream、基本超时) -
[ ] 理解 K8s Service / Deployment / Pod 的基本关系
-
[ ] 会写一个简单的 Ingress 资源,把
/api/user路由到某个 Service -
[ ] 至少知道一个 API Gateway 是怎么在架构图里出现的(Spring Cloud Gateway / Kong 等)
这张表不要求你“一周全学完”,
更像是:你每完成一个模块,就去钩一项,
过几个月回头看,你会发现自己已经从“写接口的”变成了“能看全图的”。
在日常工作中刻意练习的几个小技巧
你不一定有时间专门系统看书,但 在日常开发/排错中刻意练一下,效果会非常好。
技巧 1:任何一个线上问题,都尝试“按照链路往前/往后推”
比如:“前端说接口超时”。
你可以训练自己按照这条线思考:
浏览器 → Nginx / LB → Ingress → Service → Pod → Java → 下游服务
每一跳问自己:
-
超时发生在哪一层?
-
有没有对应的日志 / 指标 / 超时设置?
-
是 L7 的代理超时,还是你服务内部线程池被打满?
久而久之,你排错能力会明显提升。
技巧 2:多看一点“基础设施”的配置和日志
举几个具体可以实践的动作:
1)把 Nginx 配置打印出来读一读:
-
看看
proxy_read_timeout、keepalive_timeout是多少; -
理解背后对应的是哪个层的超时。
2)看一眼 Ingress YAML:
-
读懂
host/path/backend.service.name/port; -
知道你的服务在这张路由图里的位置。
3)学会看 Tomcat / Reactor Netty 的线程池指标:
-
活动线程数、队列长度;
-
对应
maxConnections、maxThreads这些配置。
技巧 3:给自己画图
每当你接手/新建一个项目,试试在笔记里画三张简单的图:
1)部署拓扑图:
-
哪些模块是独立服务?
-
各自监听什么端口?
-
有几个环境(dev/stage/prod)?
2)流量流向图:
-
用户请求从哪进来?
-
中间经过哪些网关/Nginx/Ingress?
-
在哪几层有可能被拒绝或限流?
3)下游依赖图:
-
你的服务依赖哪些 DB / Redis / MQ / 外部 API?
-
它们的超时/重试策略是谁配置的?
能画出来,就说明你真的理解了;画不出来,就知道自己缺哪块。
面试视角:把“全图理解”变成高质量回答
最后给你一个非常实用的小招:
把这一套“全图理解”,提前整理成几段“可复用回答模板”。
比如面试常问的:
问题 1:
“一个 HTTP 请求是怎么到你的 Controller 的?”
你就可以串起来答一大段(可以根据真实项目场景微调):
用户在浏览器访问我们的域名,会先到云厂商的 L7 负载均衡,再进到集群里的 Ingress Controller;
Ingress 根据 Host 和 Path 把流量路由到对应的 Service,Service 在后端 Java Pod 之间做 L4 负载均衡;
每个 Pod 里是 Spring Boot 应用,内嵌的 Tomcat 监听 8080 端口,接受 HTTP 请求并解析成
HttpServletRequest/Response;接着会走一遍 Spring 的 Filter 链,然后到
DispatcherServlet,通过HandlerMapping找到对应的 Controller 方法,执行业务逻辑;返回值通过
HttpMessageConverter序列化成 JSON,再写回响应,最后通过同一条 TCP 链路返回给客户端。
如果你再顺带加一句:
“在这个链路中,如果出现超时,我会按 L7 → L4 → 应用内部 这几个层次去排查。”
那么面试官一般就会在心里默默给你加分了。
问题 2:
“Spring MVC 和 WebFlux 的区别?什么场景下用谁?”
你也可以直接用我们前面写好的那种对比方式来回答,就不赘述了。
结语:把 Java Web 放回“网络世界”的大图里
回顾一下这篇文章,我们其实一直在做一件事:
把你每天写的 Java Web 代码,重新放回到“整个网络世界”的坐标系里。
我们走过了三条主线:
1)网络层次的主线:
-
用 OSI 七层 / TCP/IP 四层模型搭了一个大背景;
-
把 HTTP 放回应用层,把 TCP 放回传输层,把 Socket 定位成“操作系统给应用的门把手”;
-
再往上,才是 Servlet / Spring MVC / WebFlux 这些框架。
2)Java Web 技术族谱的主线:
-
Servlet 技术栈:容器负责 HTTP 解析,Servlet 规范定义请求/响应接口,Spring MVC 站在其上;
-
Netty 技术栈:基于 NIO 的事件驱动 I/O,更贴近 TCP/UDP 和自定义协议;
-
Spring MVC vs WebFlux:阻塞 vs 响应式,Servlet vs Reactor Netty。
3)云原生全链路的主线:
-
从浏览器 → Nginx / LB / WAF → Ingress Controller → Service → Pod → Spring Boot → Controller;
-
理解了 Nginx / API Gateway / Ingress / Service Mesh 在几层工作、做什么事;
-
也知道了:Java Web 在云原生世界里只是一块“应用层砖”,但这块砖要和整套地基配合好。
你完全不需要变成一个“网络工程师”:
-
不用去背 OSPF/BGP 的每个细节;
-
不用手写 iptables 规则;
-
也不用自己去写 TLS 握手过程。
但如果你能做到:
-
大致说出“一次请求从 L7 入口走到你服务”的关键跳点;
-
看得懂一份 Nginx 配置 / 一份 Ingress YAML;
-
讲清楚 Servlet 栈和 Netty 栈的差异;
-
解释什么时候用 Spring MVC,什么时候才考虑 WebFlux;
那你已经从“只会写 Controller”升级成了 “懂全图的 Java Web 工程师”。
更多推荐
所有评论(0)