为什么 Java 没有自带 HTTP 服务器?
Go 写一个
http.ListenAndServe就能起 Web 服务器,go run main.go直接跑;PHP 配个 Nginx + PHP-FPM。Java 呢?语言本身没有 HTTP,Web 应用要跑在「Servlet 容器」里,最有名的就是 Tomcat。这对从 Go/PHP 转来的人非常反直觉,这篇讲清楚来龙去脉。
三种语言的 Web 服务器形态
| 语言 | 服务器从哪来 | 部署形态 |
|---|---|---|
| Go | 标准库自带 net/http | 编译一个二进制直接跑 |
| PHP | 外部 PHP-FPM + Nginx | 请求先进 Nginx,再转 PHP |
| Java(传统) | Servlet 容器(Tomcat / Jetty / Undertow) | 应用打成 war 放进容器跑 |
| Java(Spring Boot) | 内置容器(默认 Tomcat) | java -jar 直接跑,容器打进 jar |
为什么 Java 语言不内置 HTTP
1. Java 是「通用语言」,不是「服务端专用」
- Go 从诞生就是「服务端语言」,
net/http是核心标准库,开发者开箱即用 - Java 1995 年诞生时面向的是桌面和企业应用(早期还有 Applet、Swing),HTTP 从来不是语言的职责
- 标准库 JDK 只提供最底层的
java.net(TCP/UDP socket),连 HTTP 客户端都是后来补的(Java 11 才有HttpClient)
2. Java 的 Web 走的是「规范 + 容器」路线
Java 的 Web 生态不是「语言内置」,而是:
Servlet 规范(一份接口标准) ← 定义 Web 应用长什么样
↓ 由谁实现
Servlet 容器(Tomcat / Jetty / Undertow)← 实现规范、托管应用Tomcat 就是「Servlet 规范的实现」。你的应用打成一个 war 包,扔进 Tomcat 的 webapps/ 目录,Tomcat 负责:接收 HTTP 请求 → 解析 → 调度你的代码 → 返回响应。同时它管理线程、会话、生命周期。
这不是缺陷,是 Java 早期(1999 年,Java EE)形成的服务端软件架构:容器统一管理,应用只管业务。到今天这套设计依然成立,只是被 Spring Boot 藏起来了。
3. 其实 JDK 有个「山寨版」HTTP 服务器
com.sun.net.httpserver.HttpServer(JDK 自带)能起一个 HTTP 服务,但功能极其简陋:没有 Servlet 规范、路由、过滤器、会话管理,生产没人用。就像 PHP 的 php -S localhost:8000 内置服务器 —— 只够本地调试。
为什么这让其他语言开发者很困扰
困扰 1:我的代码到底跑在哪?
- Go:
main()里自己ListenAndServe,一切尽在掌握 - Java:请求进 Tomcat → Servlet 容器 → Spring MVC → 你的 Controller,中间隔了好几层
困扰 2:为什么要「装」一个 Tomcat?
因为 Servlet 容器负责的脏活累活(线程管理、请求解析、会话、部署)你不想自己写,所以需要一个容器托管你的应用。PHP 那边是 Nginx + PHP-FPM 干这事,Tomcat 就是 Java 版的「那个东西」,只是它既是 Web 服务器又是应用服务器。
困扰 3:Tomcat / Jetty / Undertow 是什么关系?
它们都是 Servlet 容器的不同实现,功能等价、可以互换。Spring Boot 让你换一行依赖就切换:
<!-- 默认 Tomcat,想换 Jetty 就排除 Tomcat 引入 Jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>Spring Boot 是怎么「藏起」Tomcat 的
Spring Boot 的杀手锏是嵌入式容器:把 Tomcat 当普通依赖打进可执行 jar,main() 启动时自动起一个 Tomcat 实例。
java -jar app.jar # 内嵌 Tomcat 随之启动,监听 8080对比传统 Java Web:
| 传统 Java Web | Spring Boot |
|---|---|
| 装 Tomcat → 打 war → 扔进 webapps | 嵌入式 Tomcat 打进 jar,java -jar 就跑 |
| 一个 Tomcat 托管多个应用 | 每个应用自带容器,独立运行 |
| 部署依赖环境里有 Tomcat | 环境只需 JDK |
所以用 Spring Boot 时,你几乎感觉不到 Tomcat 的存在 —— 它的角色和 Go 的 net/http、PHP 的 PHP-FPM 殊途同归:让你的代码能响应 HTTP 请求。
一句话理解 Tomcat
把 Tomcat 想成「托管 Java Web 应用的那个运行时」:类似 Go 标准库 net/http 干的事,也类似 PHP 的 PHP-FPM + Nginx。只是它不是语言自带的,而是一个独立软件 —— 现在被 Spring Boot 内嵌进 jar 里了。
和 Go 的直观对照
| 需求 | Go | Java(Spring Boot) |
|---|---|---|
| 起服务器 | http.ListenAndServe() | 引入 starter-web,main() 自动起 |
| 处理请求 | http.HandleFunc("/", handler) | @GetMapping("/") |
| 运行 | go run main.go | ./mvnw spring-boot:run / java -jar |
| 换服务器实现 | 用第三方库 | 换 starter-tomcat → starter-jetty |
小结
| 记住四句话 |
|---|
| Java 语言层面没有 HTTP —— 它是通用语言,Web 靠「Servlet 规范 + 容器」实现 |
| Tomcat 是 Servlet 规范的实现,负责托管 Web 应用,类似 PHP-FPM + Nginx 的角色 |
JDK 自带的 HttpServer 极其简陋,生产没人用 |
Spring Boot 把 Tomcat 内嵌进 jar,java -jar 和 go run 一样简单,你日常感知不到容器存在 |
这篇文章想聊的是「为什么 Java 这样设计」;它和 请求生命周期(请求在容器/过滤器/拦截器间的流转)、构建与部署(如何打包成可执行 jar)配合阅读更完整。