在 “Hello Java Web” 章节使用 HttpServer创建了一个简单的 HTTP server,日常 Java Web 开发有成熟的框架支持,无需从零开始,为了理解 Spring Boot 底层工作原理,我们首先来来接一下 Java Web 最重要的两个工具—— Servlet 和 Tomcat
Servlet 类:网络请求处理程序
Servlet 是 Java 处理网络请求的规范,实现了 Servlet API 接口的 Java 类的类被称之为 Servlet 类,这些代码运行现在 Servlet 容器(Tomcat、Jetty )上,用于处理 Web 应用程序中的 HTTP 请求和响应
Servlet 的工作流程可以分为以下几个阶段:
- 加载和实例化: 当客户端某个 URL 时,Servlet 容器(如 Tomcat)会根据 URL 映射找到相应的 Servlet 类,使用类加载器加载 Servlet 类文件,创建 Servlet 类的一个实例
- 初始化: 实例化后,容器会调用 Servlet 的 init() 方法进行初始化。这个方法在 Servlet 生命周期中只执行一次,用于执行一次性的初始化任务
- 请求处理: 每当有请求到达服务器时,Servlet 容器会为该请求创建一个新的线程,并调用 Servlet 的 service() 方法。service() 方法根据请求类型(GET、POST 等)分发到对应的处理方法(如 doGet、doPost)
- 销毁: 当 Servlet 容器决定销毁某个 Servlet(服务器关闭或应用卸载),会调用 Servlet 的 destroy() 方法。这个方法用于释放资源,如关闭数据库连接、清理缓存等
客户端请求 → Servlet 容器接收请求
↓
判断 Servlet 是否已加载
/ \
是 否
↓ ↓
调用 doXXX() 方法 加载 Servlet 类
|
实例化
|
调用 init()
|
调用 doXXX()
Tomcat:运行 Servlet 的容器
在 Java 中容器通常指的是一个环境,它负责管理组件的生命周期、资源分配、安全性、并提供各种服务支持。容器通过遵循特定的规范和协议,确保组件能够以一致、可扩展和可维护的方式运行。
Tomcat 是一个 Servlet 容器,也称为 Servlet 引擎,专门用于管理和执行 Servlet 组件的环境。它负责处理来自客户端的 HTTP 请求,将其转发给相应的 Servlet 进行处理,并将 Servlet 生成的响应返回给客户端。
作为一个 Servlet 容器,Tomcat 实现了 Java Servlet 规范以及 JavaServer Pages(JSP)规范,使其能够运行和管理基于 Servlet 和 JSP 的 Web 应用程序,主要有几个工作职责:
-
Servlet 生命周期管理:
- 根据配置或注解识别并加载 Servlet 类,创建 Servlet 实例并进行初始化
- 接收 HTTP 请求,调用 Servlet 的
service()方法,将请求分发给doGet()、doPost()等具体方法处理。 - 销毁:在容器关闭或重新部署时,调用 Servlet 的
destroy()方法,释放资源。
-
请求与响应处理:
- URL映射:根据请求的URL路径,将其映射到对应的 Servlet。
- 会话管理:通过 Cookie 或 URL 重写机制管理用户会话。
- 多线程处理:支持并发处理多个请求,每个请求由独立的线程处理,提高吞吐量和响应速度。
Servlet 版本的 Hello World
接下来使用 Servlet 和 Tomcat 创建一个简单的 java Web 动态服务
1. 准备项目结构
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── org
│ │ │ └── example
│ │ │ ├── App.java
│ │ │ └── HelloServlet.java
│ │ └── webapp
│ │ └── WEB-INF
│ │ └── web.xml
│ └── test
│ └── java
│ └── org
│ └── example
2. 添加依赖
传统 Java 应用程序需要部署到一个独立的 Tomcat 服务器上运行,而使用嵌入式 Tomcat,开发者可以将 Tomcat 服务器作为应用程序的一部分打包,从而允许应用程序自己启动一个 Tomcat 服务器来处理 HTTP 请求
在 pom.xml 添加 Tomcat 和 Servlet 依赖,Java 8 需要使用 9.x(Which Version Do I Want?),Java 21 可以使用 11.x
<dependencies>
<!-- Tomcat 11 核心依赖 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>11.0.2</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
</dependency>
</dependencies>
3. 编写 HelloServlet.java
开发者使用 Servlet API 编写 HTTP 请求处理程序,响应用户 Web 请求。可以重写doGet()方法实现对 GET 请求的处理
package org.example;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
try (PrintWriter out = resp.getWriter()) {
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head><title>Hello World</title></head>");
out.println("<body>");
out.println("<h1>Hello, World!</h1>");
out.println("</body>");
out.println("</html>");
// 强制把缓冲区数据立即输出到客户端
out.flush();
}
}
}
PrintWriter 是 Java 里用于字符输出流的一个类,为了提升 I/O 操作的效率,它采用了缓冲区机制。也就是说当调用 PrintWriter 的 write 或者 println 等方法输出数据时,这些数据并不会马上被发送到目标输出流(在 Servlet 应用场景下,目标输出流一般就是客户端的响应流),而是先被存储在 PrintWriter 的内部缓冲区里。
如果不调用 flush() 方法,那么当缓冲区满了或者 PrintWriter 对象被关闭(调用 close() 方法)时,缓冲区中的数据也会被自动输出到目标输出流。不过,要是缓冲区一直没满,并且也没有关闭 PrintWriter 对象,那么数据就会一直停留在缓冲区里,客户端也就无法接收到这些数据。
4. 配置 Servlet 与路由映射
正常情况需要通过 WEB-INF/web.xml 配置路由与 Servlet 处理程序之间的映射关系,Tomcat 会扫描识别。为了方便演示,让任意 URL 都由 HelloServlet 处理
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<!-- Servlet 定义 -->
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>org.example.HelloServlet</servlet-class>
</servlet>
<!-- 将 Servlet与路由映射 -->
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
5. 使用注解简化配置
为了避免繁琐的路由配置,可以使用注解 @WebServlet(urlPatterns = "/*") 在 HelloServlet 类上快速标识
@WebServlet(urlPatterns = "/*")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
try (PrintWriter out = resp.getWriter()) {
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head><title>Hello World</title></head>");
out.println("<body>");
out.println("<h1>Hello, World!</h1>");
out.println("</body>");
out.println("</html>");
// 强制把缓冲区数据立即输出到客户端
out.flush();
}
}
}
但 Tomcat 默认只扫描 web.xml 内容,为了让注解生效需要在 WEB-INF/web.xml 配置中添加 metadata-complete="false"
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0"
metadata-complete="false">
</web-app>
这个属性用于控制 Servlet 容器对注解的处理方式。当设置为 false 时,表示 web.xml 不是元数据的唯一来源,Servlet 容器除了读取 web.xml 文件中的配置信息外,还会扫描应用程序中的注解(如 @WebServlet、@WebFilter、@WebListener 等),并根据注解的定义来配置 Servlet、过滤器和监听器等组件
6. 新建 Tomcat 实例
创建一个主类 App.java 来启动嵌入式 Tomcat 并部署我们的 Servlet
package org.example;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.webresources.DirResourceSet;
import org.apache.catalina.webresources.StandardRoot;
import java.io.File;
public class App {
public static void main( String[] args ) throws LifecycleException {
// 创建 Tomcat 实例
Tomcat tomcat = new Tomcat();
// 设置端口号
tomcat.setPort(8080);
tomcat.getConnector(); // 必须调用才会监听端口
String docBase = new File("src/main/webapp").getAbsolutePath();
// 添加静态文件夹(含 web.xml 配置)到 tomcat 文本根目录
Context ctx = tomcat.addWebapp("/", docBase);
WebResourceRoot resources = new StandardRoot(ctx);
String resourceBase = new File("target/classes").getAbsolutePath();
DirResourceSet drs = new DirResourceSet(resources, "/WEB-INF/classes", resourceBase, "/");
// 添加 .class 动态文件到服务器目录
resources.addPreResources(drs);
ctx.setResources(resources);
tomcat.start();
System.out.printf("Server running at http://localhost:%d/\n", 8080);
tomcat.getServer().await();
}
}
7. 预览效果
做完这些工作,执行 App.java 的 main 方法,浏览器访问 http://localhost:3000/ANYPATH就可以看到响应内容了
上述配置略显繁琐,后面章节介绍的 SpringBoot 会把开发者从各种各样的配置文件中解放出来,SpringBoot 版本的 Hello Java Web 会简单很多
Tomcat 与 Nginx 分工
Nginx 和 Tomcat 都是广泛使用的服务器软件,但它们设计的初衷和擅长的领域有所不同
- Nginx 从一开始就被设计为高性能的 HTTP 服务器,使用事件驱动架构使其能够高效地处理大量并发请求,并且内置强大的反向代理功能,可以高效地分发请求到后端服务器,支持负载均衡、SSL 终端、缓存等;同时 Nginx 对静态资源的处理进行了高度优化,能够以极低的资源消耗快速响应静态内容
- Tomcat 作为 Servlet 容器,专注于运行基于 Servlet 和 JSP 的动态 Web 应用,通过为每个请求分配独立线程,并行处理多个请求,确保高并发场景下的响应速度和稳定性。与 Java EE(现为Jakarta EE)规范紧密集成,支持企业级 Java 应用的开发和部署
在实际应用中,Nginx 和 Tomcat 常常结合使用:
- Nginx 作为前端服务器,负责处理所有客户端请求,提供静态资源服务,进行反向代理和负载均衡。
- Tomcat 作为后端应用服务器,专注于运行Java Web应用,处理动态内容和业务逻辑。
这种分工合作的架构能充分发挥两者的优势,提升整体系统的性能、可扩展性和可靠性。