写了很多年 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:HostContent-TypeCookie……

  • 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 细节:

  • EventLoopGroupChannelPipelineChannelHandler……

支持:

  • 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 自己也是一个 ServletDispatcherServlet

  • 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 的出现就是为了两个问题:

  1. Pod IP 会变(重启、扩容、缩容),怎么找它?

  2. 多个 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 是“应用层的一块砖”。

你的服务只需要做好几件事:

  1. 暴露清晰的 HTTP/gRPC 接口;

  2. 正确处理请求和响应(包括错误码、幂等、重试策略);

  3. 提供健康检查 / 指标暴露(Actuator / Prometheus endpoint);

  4. 合理配置超时 / 限流 / 重试(应用内或配合网关)。

至于:

  • 怎么让全世界都能访问到你;

  • 流量怎么路由到对应服务;

  • 灰度发布、蓝绿发布;

  • 全链路跟踪、熔断、限流;

  • 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_passupstream、基本超时)

  • [ ] 理解 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_timeoutkeepalive_timeout 是多少;

  • 理解背后对应的是哪个层的超时。

2)看一眼 Ingress YAML

  • 读懂 host / path / backend.service.name / port

  • 知道你的服务在这张路由图里的位置。

3)学会看 Tomcat / Reactor Netty 的线程池指标

  • 活动线程数、队列长度;

  • 对应 maxConnectionsmaxThreads 这些配置。

技巧 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 工程师”

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐