前端视角 Java Web 入门手册 4.1:Web 开发基础—— Servlet & Tomcat 版本 Hello World

243 阅读7分钟

在 “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 的工作流程可以分为以下几个阶段:

  1. 加载和实例化: 当客户端某个 URL 时,Servlet 容器(如 Tomcat)会根据 URL 映射找到相应的 Servlet 类,使用类加载器加载 Servlet 类文件,创建 Servlet 类的一个实例
  2. 初始化: 实例化后,容器会调用 Servlet 的 init() 方法进行初始化。这个方法在 Servlet 生命周期中只执行一次,用于执行一次性的初始化任务
  3. 请求处理: 每当有请求到达服务器时,Servlet 容器会为该请求创建一个新的线程,并调用 Servlet 的 service() 方法。service() 方法根据请求类型(GET、POST 等)分发到对应的处理方法(如 doGet、doPost)
  4. 销毁: 当 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 应用程序,主要有几个工作职责:

  1. Servlet 生命周期管理:

    • 根据配置或注解识别并加载 Servlet 类,创建 Servlet 实例并进行初始化
    • 接收 HTTP 请求,调用 Servlet 的service()方法,将请求分发给doGet()doPost()等具体方法处理。
    • 销毁:在容器关闭或重新部署时,调用 Servlet 的destroy()方法,释放资源。
  2. 请求与响应处理:

    • 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应用,处理动态内容和业务逻辑。

这种分工合作的架构能充分发挥两者的优势,提升整体系统的性能、可扩展性和可靠性。