Java Web 框架
Spring vs Quarkus 概览。
Java Web 框架
这一课你将能做到
- 用 JDK 自带的 HttpServer 手写一个最小 HTTP 服务,理解 Servlet 容器到底帮你做了什么
- 说出 Spring MVC 一个请求的完整流转路径(DispatcherServlet → HandlerMapping → Controller → 消息转换)
- 给 Spring Boot、Quarkus、Micronaut、Vert.x 画出选型判断树,并解释 Nginx 在整个链路中的位置
为什么要学
前 9 课的语言与工程能力,最终都要落到一件事上:对外提供 HTTP 接口。Java 后端的主流答案是 Spring Boot(国内招聘与生态绝对主流),云原生场景 Quarkus 等在增长。但框架再厚,底层都是同一件事——接收 HTTP 请求、路由到代码、返回响应。先亲手摸一遍无框架的最小实现,Spring Boot 的每个注解在你眼里才有对应的实体。
面试角度:「Spring MVC 请求处理流程」「Servlet 生命周期」「过滤器和拦截器的区别」是 Web 方向必考三连;结合本站技术栈答「一个请求从 Nginx 到 MySQL 的完整链路」是高级加分题。
先建立图像
一个请求的完整旅程(本站技术栈视角):
浏览器/App
│ HTTP
▼
Nginx(反向代理:TLS 终止、负载均衡、静态资源、限流)
│ 转发
▼
Tomcat(Servlet 容器:监听端口、解析 HTTP、线程池调度)
│
▼
DispatcherServlet(Spring MVC 前端控制器:统一入口)
│ HandlerMapping 按 URL+方法 找到 Handler
▼
@RestController ← @Service ← @Repository/JPA
│ HttpMessageConverter 把对象变 JSON
▼
响应原路返回 → Nginx → 客户端
框架分层角色:
| 角色 | 职责 | 谁来做 |
|---|
| Servlet 容器 | 监听端口、解析 HTTP、线程模型 | Tomcat / Jetty |
| Web 框架 | 路由、参数绑定、消息转换、拦截 | Spring MVC / Quarkus REST |
| 应用框架 | DI、配置、生命周期 | Spring Boot / Quarkus |
跟着做
步骤 1:零依赖手写一个 HTTP 服务
JDK 自带 com.sun.net.httpserver.HttpServer(第 7 课看到的 jdk.httpserver 模块)。新建 MiniWebDemo.java,完整可运行:
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
public class MiniWebDemo {
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
// 「路由」+「处理」+「序列化」,全是你手写的
server.createContext("/api/orders", MiniWebDemo::handleOrders);
server.setExecutor(java.util.concurrent.Executors.newFixedThreadPool(4)); // 请求线程池
server.start();
System.out.println("试一下: http://localhost:8080/api/orders/1");
}
static void handleOrders(HttpExchange exchange) throws IOException {
String method = exchange.getRequestMethod();
String path = exchange.getRequestURI().getPath();
String body;
int code;
if ("GET".equals(method) && path.matches("/api/orders/\\d+")) {
long id = Long.parseLong(path.substring(path.lastIndexOf('/') + 1));
body = "{\"id\":" + id + ",\"status\":\"PAID\"}";
code = 200;
} else {
body = "{\"error\":\"not found\"}";
code = 404;
}
respond(exchange, code, body);
}
static void respond(HttpExchange exchange, int code, String json) throws IOException {
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(code, bytes.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
}
}
运行:java MiniWebDemo.java,浏览器或 curl 访问 http://localhost:8080/api/orders/1 与一个不存在的路径看 404。你手写的「路由匹配、状态码、Content-Type、线程池」,正是 Spring MVC 帮你包掉的部分。
步骤 2:同一个接口的 Spring Boot 写法(对照阅读)
在 Spring Boot 项目里,上面 40 行变成 10 行——但每一行都对应步骤 1 里的一个手工环节:
// Spring Boot 项目内代码,依赖 spring-boot-starter-web
package com.example.demo;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping("/{id}")
public OrderView get(@PathVariable long id) { // ← 对应手写的路径解析
return new OrderView(id, "PAID"); // ← 对应手写的 JSON 拼接
}
record OrderView(long id, String status) {} // ← 第 6 课的 record 直接做响应体
}
路由、参数绑定、JSON 序列化、异常映射、线程模型全部由框架接管,你只写业务语义。
步骤 3:把 Nginx 放进链路
server {
listen 443 ssl;
location /api/ {
proxy_pass http://127.0.0.1:8080; # 转发给内网 Tomcat
proxy_set_header X-Real-IP $remote_addr;
}
location / { # 静态资源 Nginx 直接扛
root /var/www/dist;
}
}
理解分工:Nginx 管 TLS、负载均衡、静态资源与限流;Tomcat + Spring 只处理动态 API。面试聊部署链路时先画这张图。
讲清楚要点
- Servlet 容器与 Web 框架分工:容器管「连接与线程」(监听端口、解析字节流、一请求一线程),框架管「请求语义」(路由、绑定、序列化)。Spring Boot 内嵌 Tomcat,
java -jar 即起服务。
- DispatcherServlet 是前端控制器:所有请求先进它,它调 HandlerMapping 定位 Handler、HandlerAdapter 执行、返回值经 HttpMessageConverter 写回响应。一个入口 + 一套可插拔组件,这是理解 Spring MVC 的总纲。
- Servlet 生命周期:init(容器启动或首次请求时初始化一次)→ service(每个请求一个线程调用,单实例多线程并发)→ destroy(容器关闭时释放)。因此 Servlet/Controller 必须无状态。
- 过滤器 vs 拦截器:Filter 是 Servlet 规范组件,在 DispatcherServlet 之前生效,管编码、跨域、安全链(Spring Security 就是 Filter 链);Interceptor 是 Spring 机制,能拿到 Handler 信息,在控制器前后做鉴权、日志、耗时统计。先 Filter 后 Interceptor。
- 生态选型:Spring Boot(生态与就业首选)、Quarkus(Native/冷启动)、Micronaut(编译期 DI)、Vert.x(事件驱动非阻塞)。概念互通(路由、DI、配置),学透一个再横向迁移成本很低。
- starter 的本质:一组「依赖 + 自动配置」的打包。
spring-boot-starter-web = 内嵌 Tomcat + Jackson + MVC 自动配置;理解 starter 背后装了什么,才能解释「为什么加了依赖就有功能」。
你可能会懵的地方
- Tomcat 是服务器还是容器?
两者都是:作为 HTTP 服务器监听端口,作为 Servlet 容器管理 Servlet 生命周期与请求线程。Spring Boot 把它内嵌进 jar,部署形态从「放 war 进 Tomcat」变成「jar 自带 Tomcat」。
- 一请求一线程,并发 1 万怎么办?
传统方案是 Nginx 限流 + Tomcat 线程池调优 + 快速释放线程(把慢操作丢异步);JDK 21 的虚拟线程让「一请求一线程」模型在阻塞 IO 下也能扛高并发,Spring Boot 3.2+ 一行配置可开启。
- REST 里「资源」到底指什么?
URL 是名词(/api/orders/1),HTTP 方法是动词(GET 查、POST 建、PUT 全量改、DELETE 删),状态码表达结果(200/201/400/401/404/500)。框架无关,任何 Web 框架都遵循。
- 为什么 Controller 返回 record 就能变 JSON?
HttpMessageConverter(Jackson)在响应阶段把对象序列化;Jackson 2.12+ 支持 record 的构造器绑定。第 6 课说过 record 与 JavaBean 不兼容的坑,主流版本已解决。
- 跨域是后端问题还是前端问题?
浏览器同源策略的产物:服务端通过 CORS 响应头(或 Nginx 层)声明「允许哪些来源访问」。生产常见做法是 Nginx 统一加 CORS 头,应用层不再重复处理。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 在 Controller 里写业务与 SQL | 层职责崩坏,无法复用与测试 | Controller 只做参数校验与编排 |
| 在 Controller/Service 存请求状态到字段 | 单例多线程串数据 | 请求级数据走参数或 ThreadLocal |
| 把鉴权逻辑散落在每个接口 | 重复且易漏 | Filter/Interceptor 统一处理 |
| 返回 200 + body 里塞 error 标志 | 客户端无法按状态码处理 | 正确使用 HTTP 状态码 |
| 生产直接暴露 Tomcat 端口 | 绕过 TLS/限流/日志 | 应用只监听内网,Nginx 对外 |
卡点急救
- 端口被占用(Address already in use):8080 被旧进程占着。Windows 下
netstat -ano | findstr :8080 找 PID 再 taskkill /PID xxx /F,或换端口。
- 浏览器访问中文乱码:响应头没带 charset。如本课示例,
Content-Type: application/json; charset=utf-8 必须显式设置。
- Spring Boot 加了依赖却没有对应功能:先确认 starter 是否正确(web vs webflux),再看自动配置日志
--debug 输出里该配置类是否生效;多数问题出在依赖范围或配置项拼写。
面试常问
- Spring MVC 处理一个请求的完整流程?
可背版:请求到 DispatcherServlet → HandlerMapping 按 URL/方法找到 Handler(Controller 方法)→ HandlerAdapter 执行(期间完成参数绑定/校验)→ 返回值由 HttpMessageConverter 序列化(或走 ViewResolver)→ 响应写回。全程穿插 Interceptor 前后回调与全局异常处理。
- Servlet 的生命周期?为什么要求无状态?
init 仅一次(可配置启动即初始化),service 每请求一次且多线程并发执行同一实例,destroy 仅一次。因为单实例被并发复用,实例字段存请求级数据就会串请求,所以容器与框架组件都要求无状态。
- 过滤器与拦截器的区别?
可背版:Filter 属于 Servlet 规范,由容器调度,包住 DispatcherServlet 之外的全部请求(含静态资源),典型如编码、CORS、安全链;Interceptor 属于 Spring MVC,能拿到 HandlerMethod 元信息,适合鉴权、日志、耗时监控。执行顺序:Filter → DispatcherServlet → Interceptor → Controller,返回时逆序。
- 说说一个请求从 Nginx 到数据库的完整链路?
Nginx 终止 TLS/负载均衡 → 内网转发到 Tomcat → 请求线程进入 DispatcherServlet → Controller(校验)→ Service(业务、事务)→ Repository/JPA → MySQL(连接池);Redis 在 Service 层做缓存拦截,热点数据不过库。答出「每一跳的职责与线程」即可。
自测三问
- MiniWebDemo 里你手写的哪四件事,分别被 Spring MVC 的哪个组件接管?
- 为什么 Spring Security 的核心是一串 Filter 而不是 Interceptor?
- Nginx 在本站链路里承担哪四类职责?
小结一句话
Web 框架是「路由 + 绑定 + 序列化 + 线程模型」的工厂化封装:先用零依赖代码摸清底层,再看 Spring Boot 就全是熟人。
延伸