#Servlet与Tomcat完全指南 - 从入门到精通(含面试题)

🎯 适合人群 :Java Web开发初学者、准备面试的开发者

📚 阅读时间 :约30分钟

💡 建议:边看边敲代码,效果更佳!


📖 目录

  1. 什么是Servlet?
  2. 什么是Tomcat?
  3. Tomcat工作原理详解
  4. Servlet体系结构
  5. Servlet生命周期
  6. 第一个Servlet程序
  7. HttpServletRequest详解
  8. HttpServletResponse详解
  9. Servlet容器原理
  10. 自定义Tomcat实现原理
  11. 常见面试题

1. 什么是Servlet?

1.1 Servlet的定义

Servlet(Server Applet)是运行在服务器端的Java小程序,是Sun公司提供的一种用于开发动态Web资源的技术。

🔔 通俗理解:Servlet就像是餐厅里的服务员,负责接收顾客(浏览器)的点单(请求),然后把菜(响应数据)端给顾客。

1.2 Servlet的作用

复制代码
┌─────────────┐     请求      ┌─────────────┐     处理      ┌─────────────┐
│   浏览器     │ ──────────▶ │   Servlet   │ ──────────▶ │   数据库     │
│  (客户端)    │ ◀────────── │   (服务器)   │ ◀────────── │  (数据存储)  │
└─────────────┘     响应      └─────────────┘     结果      └─────────────┘

Servlet的核心功能

  • ✅ 接收客户端(浏览器)发送的请求
  • ✅ 处理请求(调用业务逻辑、访问数据库等)
  • ✅ 生成响应结果并返回给客户端

2. 什么是Tomcat?

2.1 Tomcat的定义

Tomcat 是Apache软件基金会开发的一个开源的Servlet容器(也叫Web容器),它实现了Java Servlet和JSP规范。

🔔 通俗理解:如果把Servlet比作服务员,那Tomcat就是整个餐厅。餐厅(Tomcat)管理着所有的服务员(Servlet),负责接待顾客、分配任务等。

2.2 为什么需要Tomcat?

Servlet本身只是一个Java类,它不能独立运行,需要一个"容器"来:

  • 🏠 提供运行环境:管理Servlet的创建、初始化、销毁
  • 📡 处理网络通信:监听端口、接收请求、发送响应
  • 🔄 管理生命周期:控制Servlet什么时候创建、什么时候销毁

2.3 常见的Servlet容器

容器名称 说明 特点
Tomcat Apache开源项目 轻量级、使用最广泛
Jetty Eclipse基金会 更轻量、嵌入式场景多
Undertow Red Hat开发 高性能、Spring Boot默认可选
WebLogic Oracle商业产品 企业级、功能强大
WebSphere IBM商业产品 企业级、稳定性高

3. Tomcat工作原理详解

3.1 网络通信基础概念

在理解Tomcat之前,我们需要先了解几个重要概念:

3.1.1 什么是Socket?

Socket(套接字) 是网络通信的"管道",是应用程序与网络协议之间的接口。

复制代码
┌──────────────┐                              ┌──────────────┐
│    客户端     │                              │    服务器     │
│              │         Socket连接            │              │
│  ┌────────┐  │  ◀════════════════════════▶  │  ┌────────┐  │
│  │ Socket │  │        数据传输通道           │  │ Socket │  │
│  └────────┘  │                              │  └────────┘  │
└──────────────┘                              └──────────────┘

🔔 通俗理解:Socket就像是两个人打电话时的电话线,数据通过这条"线"来回传输。

3.1.2 什么是端口?

端口(Port) 是计算机与外界通信的"门牌号",用于区分同一台计算机上的不同服务。

复制代码
服务器(电脑)
┌─────────────────────────────────────────┐
│                                         │
│   端口3306 ──▶ MySQL数据库服务           │
│   端口8080 ──▶ Tomcat服务               │
│   端口80   ──▶ HTTP服务(网页)          │
│   端口443  ──▶ HTTPS服务(加密网页)     │
│                                         │
└─────────────────────────────────────────┘

🔔 通俗理解:如果IP地址是小区地址,那端口号就是具体的门牌号。快递员(数据)根据门牌号(端口)找到正确的住户(服务)。

3.1.3 什么是BIO、NIO、AIO?

这三种是Java中不同的I/O模型(输入/输出模型):

模型 全称 特点 适用场景
BIO Blocking I/O(阻塞式I/O) 一个连接一个线程,等待时阻塞 连接数少、固定的场景
NIO Non-blocking I/O(非阻塞式I/O) 一个线程处理多个连接 连接数多、连接时间短
AIO Asynchronous I/O(异步I/O) 异步非阻塞,操作系统完成后通知 连接数多、连接时间长
java 复制代码
// BIO示例:阻塞等待数据
// 当没有数据时,程序会一直等待,不能做其他事情
InputStream inputStream = socket.getInputStream();
int data = inputStream.read(); // 这里会阻塞,直到有数据到来

🔔 通俗理解

  • BIO:你在餐厅点餐后,站在柜台前一直等,直到餐做好(阻塞等待)
  • NIO:你点餐后拿个号,可以去做其他事,时不时看看叫到你没(轮询)
  • AIO:你点餐后去做其他事,餐好了服务员会叫你(回调通知)

3.2 Tomcat处理请求的完整流程

当浏览器访问 http://ip:8080/xxx/my 时,Tomcat的处理流程如下:

复制代码
步骤1: 浏览器发起请求
    ↓
步骤2: 请求到达服务器网卡
    ↓
步骤3: 根据端口号(8080)找到Tomcat服务
    ↓
步骤4: Tomcat通过Socket接收请求
    ↓
步骤5: 打开输入流(InputStream)读取请求数据
    ↓
步骤6: 解析请求信息(URL、请求方法、参数等)
    ↓
步骤7: 根据URL找到对应的Servlet
    ↓
步骤8: 调用Servlet处理请求
    ↓
步骤9: Servlet生成响应数据
    ↓
步骤10: 通过输出流(OutputStream)返回响应
    ↓
步骤11: 浏览器接收并显示结果

3.3 请求解析详解

当Tomcat接收到HTTP请求后,会解析出以下信息:

复制代码
HTTP请求报文示例:
┌─────────────────────────────────────────────────────┐
│ GET /my?name=zhangsan HTTP/1.1                      │  ← 请求行
│ Host: localhost:8080                                │  ← 请求头
│ Cookie: JSESSIONID=ABC123                           │
│ User-Agent: Mozilla/5.0                             │
│                                                     │
│ name=zhangsan&age=18                                │  ← 请求体(POST时)
└─────────────────────────────────────────────────────┘

解析后得到的信息

  • 📍 来源IP:客户端的IP地址
  • 🔗 URL地址 :请求的资源路径(如 /my
  • 🚪 端口 :请求的端口号(如 8080
  • 🍪 Cookie:客户端携带的Cookie信息
  • 📝 请求类型:GET、POST、PUT、DELETE等
  • 📦 请求参数:URL参数或表单数据

4. Servlet体系结构

4.1 Servlet继承体系图

复制代码
                    ┌─────────────────────┐
                    │   Servlet接口        │  ← 定义Servlet生命周期
                    │  (javax.servlet)    │
                    └──────────┬──────────┘
                               │ 实现
                               ▼
                    ┌─────────────────────┐
                    │  GenericServlet     │  ← 抽象类,实现了除service外的所有方法
                    │     (抽象类)         │
                    └──────────┬──────────┘
                               │ 继承
                               ▼
                    ┌─────────────────────┐
                    │   HttpServlet       │  ← 抽象类,针对HTTP协议优化
                    │     (抽象类)         │     根据请求方式调用不同方法
                    └──────────┬──────────┘
                               │ 继承
                               ▼
                    ┌─────────────────────┐
                    │    自定义Servlet     │  ← 我们编写的Servlet类
                    │  (MyServlet等)      │     重写doGet()、doPost()方法
                    └─────────────────────┘

4.2 Servlet接口详解

java 复制代码
/**
 * Servlet接口 - 定义了Servlet的生命周期方法
 * 所有的Servlet都必须直接或间接实现这个接口
 */
public interface Servlet {
    
    /**
     * 初始化方法 - Servlet被创建时调用,只执行一次
     * @param config Servlet配置信息对象
     */
    void init(ServletConfig config) throws ServletException;
    
    /**
     * 获取Servlet配置信息
     * @return ServletConfig对象,包含初始化参数等信息
     */
    ServletConfig getServletConfig();
    
    /**
     * 服务方法 - 每次请求都会调用此方法
     * @param request  请求对象,包含客户端发送的所有请求信息
     * @param response 响应对象,用于向客户端发送响应数据
     */
    void service(ServletRequest request, ServletResponse response) 
        throws ServletException, IOException;
    
    /**
     * 获取Servlet信息(作者、版本等)
     * @return Servlet的描述信息
     */
    String getServletInfo();
    
    /**
     * 销毁方法 - Servlet被销毁前调用,只执行一次
     * 用于释放资源(关闭数据库连接、释放文件句柄等)
     */
    void destroy();
}

4.3 GenericServlet抽象类

java 复制代码
/**
 * GenericServlet - 通用Servlet抽象类
 * 
 * 作用:实现了Servlet接口中除了service()方法以外的所有方法
 * 好处:我们继承它后,只需要重写service()方法即可
 */
public abstract class GenericServlet implements Servlet, ServletConfig {
    
    private transient ServletConfig config;
    
    // 空参的init方法,方便子类重写
    public void init() throws ServletException {
        // 子类可以重写此方法进行初始化操作
    }
    
    // 带参数的init方法,保存配置信息
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init(); // 调用空参init,方便子类重写
    }
    
    @Override
    public ServletConfig getServletConfig() {
        return config;
    }
    
    // service方法仍然是抽象的,需要子类实现
    @Override
    public abstract void service(ServletRequest req, ServletResponse res)
        throws ServletException, IOException;
    
    @Override
    public String getServletInfo() {
        return "";
    }
    
    @Override
    public void destroy() {
        // 默认空实现,子类可以重写
    }
}

4.4 HttpServlet抽象类(重点!)

java 复制代码
/**
 * HttpServlet - 针对HTTP协议的Servlet抽象类
 * 
 * 核心功能:根据HTTP请求方式(GET、POST等)调用不同的处理方法
 * 
 * 我们开发时通常继承这个类,然后重写doGet()和doPost()方法
 */
public abstract class HttpServlet extends GenericServlet {
    
    /**
     * service方法 - 请求分发的核心
     * 根据请求方式调用对应的doXxx方法
     */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        
        // 获取请求方式(GET、POST、PUT、DELETE等)
        String method = req.getMethod();
        
        // 根据不同的请求方式,调用不同的处理方法
        if (method.equals("GET")) {
            doGet(req, resp);      // GET请求 → doGet()
        } else if (method.equals("POST")) {
            doPost(req, resp);     // POST请求 → doPost()
        } else if (method.equals("PUT")) {
            doPut(req, resp);      // PUT请求 → doPut()
        } else if (method.equals("DELETE")) {
            doDelete(req, resp);   // DELETE请求 → doDelete()
        }
        // ... 还有其他请求方式
    }
    
    /**
     * 处理GET请求 - 子类需要重写此方法
     * GET请求通常用于:查询数据、获取页面
     */
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // 默认返回405错误(方法不允许)
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }
    
    /**
     * 处理POST请求 - 子类需要重写此方法
     * POST请求通常用于:提交表单、上传文件、新增数据
     */
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // 默认返回405错误(方法不允许)
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }
    
    // doPut()、doDelete()等方法类似...
}

💡 为什么要区分doGet和doPost?

  • GET请求:参数在URL中,有长度限制,适合查询操作
  • POST请求:参数在请求体中,无长度限制,适合提交数据

5. Servlet生命周期

5.1 生命周期概述

Servlet的生命周期是指:从Servlet被创建到被销毁的整个过程。

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        Servlet生命周期                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌─────────┐      ┌─────────┐      ┌─────────┐      ┌─────────┐   │
│   │  加载    │ ──▶ │  初始化  │ ──▶ │  服务   │ ──▶ │  销毁   │   │
│   │ (Load)  │      │ (Init)  │      │(Service)│      │(Destroy)│   │
│   └─────────┘      └─────────┘      └─────────┘      └─────────┘   │
│        │                │                │                │        │
│        ▼                ▼                ▼                ▼        │
│   Tomcat启动       调用init()       调用service()    调用destroy() │
│   或首次请求        只执行1次        每次请求执行      只执行1次     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

5.2 生命周期详解

阶段1:加载和实例化
java 复制代码
// Tomcat通过反射创建Servlet实例
// 相当于执行:MyServlet servlet = new MyServlet();
Class<?> clazz = Class.forName("com.example.MyServlet");
Servlet servlet = (Servlet) clazz.newInstance();

触发时机

  • 默认:第一次访问该Servlet时
  • 配置 loadOnStartup 后:Tomcat启动时
阶段2:初始化(init方法)
java 复制代码
/**
 * 初始化方法 - 只执行一次
 * 
 * 适合做的事情:
 * 1. 加载配置文件
 * 2. 建立数据库连接池
 * 3. 初始化缓存
 */
@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    System.out.println("Servlet初始化了!");
    
    // 示例:获取初始化参数
    String encoding = config.getInitParameter("encoding");
    System.out.println("编码格式:" + encoding);
}
阶段3:服务(service方法)
java 复制代码
/**
 * 服务方法 - 每次请求都会执行
 * 
 * 这是Servlet的核心方法,处理客户端请求
 */
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    System.out.println("收到一个请求!");
    // 调用父类的service方法,会自动分发到doGet或doPost
    super.service(req, resp);
}
阶段4:销毁(destroy方法)
java 复制代码
/**
 * 销毁方法 - 只执行一次
 * 
 * 触发时机:
 * 1. Tomcat正常关闭时
 * 2. Web应用被卸载时
 * 
 * 适合做的事情:
 * 1. 关闭数据库连接
 * 2. 释放资源
 * 3. 保存数据
 */
@Override
public void destroy() {
    System.out.println("Servlet被销毁了!");
    // 释放资源的代码...
}

5.3 生命周期完整示例

java 复制代码
package com.example.servlet;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;

/**
 * Servlet生命周期演示
 * 
 * 运行后观察控制台输出,理解各方法的调用时机
 */
@WebServlet(
    name = "LifeCycleServlet",
    urlPatterns = "/lifecycle",
    loadOnStartup = 1  // 设置为1表示Tomcat启动时就创建此Servlet
)
public class LifeCycleServlet extends HttpServlet {
    
    // 构造方法 - 创建实例时调用
    public LifeCycleServlet() {
        System.out.println("1. 构造方法执行 - Servlet实例被创建");
    }
    
    // 初始化方法 - 实例创建后调用,只执行一次
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        System.out.println("2. init()方法执行 - Servlet初始化完成");
    }
    
    // 服务方法 - 每次请求都会调用
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        System.out.println("3. service()方法执行 - 处理请求");
        super.service(req, resp);
    }
    
    // 处理GET请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        System.out.println("4. doGet()方法执行 - 处理GET请求");
        resp.getWriter().write("Hello, Servlet!");
    }
    
    // 销毁方法 - Tomcat关闭时调用,只执行一次
    @Override
    public void destroy() {
        System.out.println("5. destroy()方法执行 - Servlet被销毁");
        super.destroy();
    }
}

控制台输出顺序

复制代码
Tomcat启动时:
1. 构造方法执行 - Servlet实例被创建
2. init()方法执行 - Servlet初始化完成

每次访问 /lifecycle 时:
3. service()方法执行 - 处理请求
4. doGet()方法执行 - 处理GET请求

Tomcat关闭时:
5. destroy()方法执行 - Servlet被销毁

6. 第一个Servlet程序

6.1 开发环境准备

所需工具

  • ☕ JDK 8 或以上版本
  • 🐱 Apache Tomcat 9.x
  • 💻 IDE(推荐 IntelliJ IDEA 或 Eclipse)

6.2 创建Web项目结构

复制代码
MyWebProject/
├── src/
│   └── com/
│       └── example/
│           └── servlet/
│               └── HelloServlet.java    ← 我们的Servlet类
├── web/
│   ├── WEB-INF/
│   │   ├── web.xml                      ← 配置文件(可选)
│   │   └── lib/                         ← 第三方jar包
│   └── index.html                       ← 首页
└── pom.xml                              ← Maven配置(如果使用Maven)

6.3 编写第一个Servlet

java 复制代码
package com.example.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 第一个Servlet程序 - Hello World
 * 
 * @WebServlet注解说明:
 * - name: Servlet的名称(可选)
 * - urlPatterns: 访问路径,可以配置多个
 * - value: urlPatterns的简写形式
 * 
 * 访问地址:http://localhost:8080/项目名/hello
 */
@WebServlet(name = "HelloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    
    /**
     * 处理GET请求
     * 
     * @param request  请求对象 - 包含客户端发送的所有信息
     * @param response 响应对象 - 用于向客户端返回数据
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // 1. 设置响应的内容类型和编码
        //    text/html 表示返回HTML格式
        //    charset=UTF-8 表示使用UTF-8编码,防止中文乱码
        response.setContentType("text/html;charset=UTF-8");
        
        // 2. 获取输出流,用于向客户端写入数据
        PrintWriter out = response.getWriter();
        
        // 3. 输出HTML内容
        out.println("<!DOCTYPE html>");
        out.println("<html>");
        out.println("<head><title>我的第一个Servlet</title></head>");
        out.println("<body>");
        out.println("<h1>Hello, Servlet!</h1>");
        out.println("<p>恭喜你,第一个Servlet运行成功!</p>");
        out.println("<p>当前时间:" + new java.util.Date() + "</p>");
        out.println("</body>");
        out.println("</html>");
    }
    
    /**
     * 处理POST请求
     * 
     * 这里直接调用doGet方法,让GET和POST请求执行相同的逻辑
     * 实际开发中,通常会根据业务需求分别处理
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 调用doGet方法,复用代码
        doGet(request, response);
    }
}

6.4 Servlet的两种配置方式

方式一:注解配置(推荐,Servlet 3.0+)
java 复制代码
// 简单写法
@WebServlet("/hello")
public class HelloServlet extends HttpServlet { }

// 完整写法
@WebServlet(
    name = "HelloServlet",           // Servlet名称
    urlPatterns = {"/hello", "/hi"}, // 可以配置多个访问路径
    loadOnStartup = 1,               // 启动时加载,数字越小优先级越高
    initParams = {                   // 初始化参数
        @WebInitParam(name = "encoding", value = "UTF-8"),
        @WebInitParam(name = "author", value = "张三")
    }
)
public class HelloServlet extends HttpServlet { }
方式二:web.xml配置(传统方式)
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    
    <!-- 定义Servlet -->
    <servlet>
        <!-- Servlet的名称,用于关联映射 -->
        <servlet-name>HelloServlet</servlet-name>
        <!-- Servlet类的完整路径 -->
        <servlet-class>com.example.servlet.HelloServlet</servlet-class>
        <!-- 启动时加载(可选) -->
        <load-on-startup>1</load-on-startup>
        <!-- 初始化参数(可选) -->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </servlet>
    
    <!-- 配置Servlet映射(访问路径) -->
    <servlet-mapping>
        <!-- 对应上面的servlet-name -->
        <servlet-name>HelloServlet</servlet-name>
        <!-- 访问路径 -->
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
    
</web-app>

6.5 URL匹配规则

java 复制代码
// 1. 精确匹配 - 只匹配指定路径
@WebServlet("/user/login")
// 访问:/user/login ✓
// 访问:/user/login/test ✗

// 2. 路径匹配 - 匹配指定路径下的所有请求
@WebServlet("/user/*")
// 访问:/user/login ✓
// 访问:/user/register ✓
// 访问:/user/info/detail ✓

// 3. 扩展名匹配 - 匹配指定扩展名的请求
@WebServlet("*.do")
// 访问:/login.do ✓
// 访问:/user/save.do ✓
// 注意:不能加 / 前缀

// 4. 默认匹配 - 匹配所有请求(优先级最低)
@WebServlet("/")
// 当其他Servlet都不匹配时,由此Servlet处理

匹配优先级:精确匹配 > 路径匹配 > 扩展名匹配 > 默认匹配


7. HttpServletRequest详解

7.1 什么是HttpServletRequest?

HttpServletRequest 是Servlet API中的一个接口,代表客户端的HTTP请求。当浏览器发送请求到服务器时,Tomcat会将请求信息封装到这个对象中。

复制代码
浏览器请求                          HttpServletRequest对象
┌─────────────────┐                ┌─────────────────────────┐
│ GET /user?id=1  │                │ getMethod() → "GET"     │
│ Host: localhost │   封装到       │ getRequestURI() → "/user"│
│ Cookie: xxx     │ ──────────▶   │ getParameter("id") → "1"│
│ User-Agent: xxx │                │ getCookies() → Cookie[] │
└─────────────────┘                └─────────────────────────┘

7.2 获取请求行信息

java 复制代码
/**
 * 获取HTTP请求行中的信息
 * 
 * 请求行格式:GET /user/login?name=zhangsan HTTP/1.1
 */
@WebServlet("/requestDemo")
public class RequestLineServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // 设置响应编码
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        // ========== 获取请求行信息 ==========
        
        // 1. 获取请求方式(GET、POST等)
        String method = request.getMethod();
        out.println("请求方式:" + method + "<br>");
        // 输出:请求方式:GET
        
        // 2. 获取请求URI(不包含协议、主机、端口)
        String uri = request.getRequestURI();
        out.println("请求URI:" + uri + "<br>");
        // 输出:请求URI:/项目名/requestDemo
        
        // 3. 获取请求URL(完整的请求地址)
        StringBuffer url = request.getRequestURL();
        out.println("请求URL:" + url + "<br>");
        // 输出:请求URL:http://localhost:8080/项目名/requestDemo
        
        // 4. 获取协议版本
        String protocol = request.getProtocol();
        out.println("协议版本:" + protocol + "<br>");
        // 输出:协议版本:HTTP/1.1
        
        // 5. 获取项目的虚拟目录(上下文路径)
        String contextPath = request.getContextPath();
        out.println("项目路径:" + contextPath + "<br>");
        // 输出:项目路径:/项目名
        
        // 6. 获取Servlet路径
        String servletPath = request.getServletPath();
        out.println("Servlet路径:" + servletPath + "<br>");
        // 输出:Servlet路径:/requestDemo
        
        // 7. 获取查询字符串(URL中?后面的部分)
        String queryString = request.getQueryString();
        out.println("查询字符串:" + queryString + "<br>");
        // 输出:查询字符串:name=zhangsan&age=18
    }
}

7.3 获取请求头信息

java 复制代码
/**
 * 获取HTTP请求头信息
 * 
 * 请求头包含了客户端环境信息、Cookie、认证信息等
 */
@WebServlet("/headerDemo")
public class RequestHeaderServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        // ========== 获取请求头信息 ==========
        
        // 1. 获取指定名称的请求头
        String userAgent = request.getHeader("User-Agent");
        out.println("浏览器信息:" + userAgent + "<br>");
        // 可以用来判断是手机还是电脑访问
        
        String referer = request.getHeader("Referer");
        out.println("来源页面:" + referer + "<br>");
        // 可以用来防盗链
        
        String host = request.getHeader("Host");
        out.println("主机信息:" + host + "<br>");
        
        // 2. 获取所有请求头名称
        out.println("<hr><h3>所有请求头:</h3>");
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            String value = request.getHeader(name);
            out.println(name + " : " + value + "<br>");
        }
    }
}

7.4 获取请求参数(重点!)

java 复制代码
/**
 * 获取请求参数 - 最常用的功能
 * 
 * 无论是GET请求的URL参数,还是POST请求的表单数据,
 * 都可以通过以下方法获取
 */
@WebServlet("/paramDemo")
public class RequestParamServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // 【重要】设置请求体的编码,解决POST请求中文乱码
        // 注意:必须在获取参数之前设置!
        request.setCharacterEncoding("UTF-8");
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        // ========== 获取请求参数 ==========
        
        // 1. 获取单个参数值
        // 适用于:单选框、文本框等只有一个值的情况
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        out.println("用户名:" + username + "<br>");
        out.println("密码:" + password + "<br>");
        
        // 2. 获取同名参数的多个值
        // 适用于:复选框等有多个值的情况
        // 例如:hobby=reading&hobby=gaming&hobby=music
        String[] hobbies = request.getParameterValues("hobby");
        if (hobbies != null) {
            out.println("爱好:");
            for (String hobby : hobbies) {
                out.println(hobby + " ");
            }
            out.println("<br>");
        }
        
        // 3. 获取所有参数名称
        out.println("<hr><h3>所有参数:</h3>");
        Enumeration<String> paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String name = paramNames.nextElement();
            String value = request.getParameter(name);
            out.println(name + " = " + value + "<br>");
        }
        
        // 4. 获取所有参数的Map集合
        // Map的key是参数名,value是参数值数组
        Map<String, String[]> paramMap = request.getParameterMap();
        out.println("<hr><h3>参数Map:</h3>");
        for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
            String name = entry.getKey();
            String[] values = entry.getValue();
            out.println(name + " = " + Arrays.toString(values) + "<br>");
        }
    }
    
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // POST请求也用同样的方法获取参数
        doGet(request, response);
    }
}

7.5 获取客户端信息

java 复制代码
/**
 * 获取客户端相关信息
 */
@WebServlet("/clientInfo")
public class ClientInfoServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        // 1. 获取客户端IP地址
        String clientIP = request.getRemoteAddr();
        out.println("客户端IP:" + clientIP + "<br>");
        
        // 2. 获取客户端主机名
        String clientHost = request.getRemoteHost();
        out.println("客户端主机:" + clientHost + "<br>");
        
        // 3. 获取客户端端口
        int clientPort = request.getRemotePort();
        out.println("客户端端口:" + clientPort + "<br>");
        
        // 4. 获取服务器IP
        String serverIP = request.getLocalAddr();
        out.println("服务器IP:" + serverIP + "<br>");
        
        // 5. 获取服务器端口
        int serverPort = request.getServerPort();
        out.println("服务器端口:" + serverPort + "<br>");
        
        // 6. 获取服务器名称
        String serverName = request.getServerName();
        out.println("服务器名称:" + serverName + "<br>");
    }
}

7.6 请求转发

java 复制代码
/**
 * 请求转发 - 服务器内部的资源跳转
 * 
 * 特点:
 * 1. 浏览器地址栏不变
 * 2. 只能转发到当前服务器内部资源
 * 3. 是一次请求,可以共享request域中的数据
 */
@WebServlet("/forwardDemo")
public class ForwardServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        System.out.println("ForwardServlet被访问了...");
        
        // 1. 在request域中存储数据(可以传递给转发的目标)
        request.setAttribute("message", "这是从ForwardServlet传递的数据");
        request.setAttribute("user", new User("张三", 18));
        
        // 2. 获取请求转发器,指定转发目标
        RequestDispatcher dispatcher = request.getRequestDispatcher("/targetServlet");
        
        // 3. 执行转发
        dispatcher.forward(request, response);
        
        // 注意:forward之后的代码不会执行
        System.out.println("这行代码不会执行");
    }
}

/**
 * 转发的目标Servlet
 */
@WebServlet("/targetServlet")
public class TargetServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        System.out.println("TargetServlet被访问了...");
        
        // 从request域中获取数据
        String message = (String) request.getAttribute("message");
        User user = (User) request.getAttribute("user");
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("收到的消息:" + message + "<br>");
        out.println("收到的用户:" + user + "<br>");
    }
}

7.7 Request域对象

java 复制代码
/**
 * Request域 - 在一次请求范围内共享数据
 * 
 * 作用范围:一次请求(包括请求转发)
 */
@WebServlet("/scopeDemo")
public class RequestScopeServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // ========== Request域的常用方法 ==========
        
        // 1. 存储数据
        request.setAttribute("name", "张三");
        request.setAttribute("age", 18);
        request.setAttribute("list", Arrays.asList("Java", "Python", "Go"));
        
        // 2. 获取数据
        String name = (String) request.getAttribute("name");
        Integer age = (Integer) request.getAttribute("age");
        
        // 3. 删除数据
        request.removeAttribute("age");
        
        // 4. 获取所有属性名
        Enumeration<String> names = request.getAttributeNames();
        while (names.hasMoreElements()) {
            String attrName = names.nextElement();
            Object attrValue = request.getAttribute(attrName);
            System.out.println(attrName + " = " + attrValue);
        }
        
        // 转发到另一个Servlet,数据可以共享
        request.getRequestDispatcher("/anotherServlet").forward(request, response);
    }
}

8. HttpServletResponse详解

8.1 什么是HttpServletResponse?

HttpServletResponse 代表服务器的HTTP响应。我们通过这个对象向客户端发送响应数据。

复制代码
HttpServletResponse对象                    浏览器收到的响应
┌─────────────────────────┐               ┌─────────────────┐
│ setStatus(200)          │               │ HTTP/1.1 200 OK │
│ setContentType("text")  │   发送到      │ Content-Type:   │
│ getWriter().write(...)  │ ──────────▶  │ text/html       │
│ addCookie(cookie)       │               │ Set-Cookie: xxx │
└─────────────────────────┘               │ <html>...</html>│
                                          └─────────────────┘

8.2 设置响应行

java 复制代码
/**
 * 设置HTTP响应行
 * 
 * 响应行格式:HTTP/1.1 200 OK
 */
@WebServlet("/responseDemo")
public class ResponseLineServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // ========== 设置响应状态码 ==========
        
        // 方式1:只设置状态码
        response.setStatus(200);  // 200 表示成功
        
        // 常见状态码:
        // 200 - OK,请求成功
        // 302 - Found,重定向
        // 304 - Not Modified,使用缓存
        // 400 - Bad Request,请求语法错误
        // 401 - Unauthorized,未授权
        // 403 - Forbidden,禁止访问
        // 404 - Not Found,资源不存在
        // 500 - Internal Server Error,服务器内部错误
        
        // 方式2:设置状态码并发送错误信息
        // response.sendError(404, "您访问的资源不存在");
    }
}

8.3 设置响应头

java 复制代码
/**
 * 设置HTTP响应头
 */
@WebServlet("/responseHeader")
public class ResponseHeaderServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // ========== 设置响应头 ==========
        
        // 1. 设置内容类型(最常用)
        response.setContentType("text/html;charset=UTF-8");
        // 等价于:
        // response.setHeader("Content-Type", "text/html;charset=UTF-8");
        
        // 2. 设置字符编码
        response.setCharacterEncoding("UTF-8");
        
        // 3. 设置自定义响应头
        response.setHeader("My-Header", "Hello");
        
        // 4. 添加响应头(可以添加多个同名的头)
        response.addHeader("Set-Cookie", "name=zhangsan");
        response.addHeader("Set-Cookie", "age=18");
        
        // 5. 设置缓存控制
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", -1);  // 立即过期
        
        // 6. 设置内容长度
        response.setContentLength(1024);
        
        // 7. 设置刷新(3秒后跳转到百度)
        response.setHeader("Refresh", "3;url=https://www.baidu.com");
    }
}

8.4 设置响应体

java 复制代码
/**
 * 设置HTTP响应体 - 向客户端输出数据
 */
@WebServlet("/responseBody")
public class ResponseBodyServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // ========== 方式1:字符输出流(输出文本数据) ==========
        
        // 设置编码,防止中文乱码
        response.setContentType("text/html;charset=UTF-8");
        
        // 获取字符输出流
        PrintWriter writer = response.getWriter();
        
        // 输出文本数据
        writer.write("Hello, World!");
        writer.write("<h1>这是一个标题</h1>");
        writer.println("<p>这是一个段落</p>");
        
        // 输出JSON数据
        // response.setContentType("application/json;charset=UTF-8");
        // writer.write("{\"name\":\"张三\",\"age\":18}");
    }
    
    /**
     * 输出字节数据(图片、文件等)
     */
    protected void outputBytes(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // ========== 方式2:字节输出流(输出二进制数据) ==========
        
        // 设置内容类型为图片
        response.setContentType("image/jpeg");
        
        // 获取字节输出流
        ServletOutputStream outputStream = response.getOutputStream();
        
        // 读取图片文件并输出
        String imagePath = request.getServletContext().getRealPath("/images/logo.jpg");
        FileInputStream fis = new FileInputStream(imagePath);
        
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            outputStream.write(buffer, 0, len);
        }
        
        fis.close();
    }
}

⚠️ 注意getWriter()getOutputStream() 不能同时使用,否则会抛出异常!

8.5 重定向

java 复制代码
/**
 * 重定向 - 让浏览器重新发起请求
 * 
 * 特点:
 * 1. 浏览器地址栏会变化
 * 2. 可以重定向到任意资源(包括外部网站)
 * 3. 是两次请求,不能共享request域中的数据
 */
@WebServlet("/redirectDemo")
public class RedirectServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        System.out.println("RedirectServlet被访问了...");
        
        // ========== 重定向的两种方式 ==========
        
        // 方式1:简单方式(推荐)
        response.sendRedirect("/项目名/targetPage.html");
        
        // 方式2:手动设置状态码和Location头
        // response.setStatus(302);
        // response.setHeader("Location", "/项目名/targetPage.html");
        
        // 重定向到外部网站
        // response.sendRedirect("https://www.baidu.com");
        
        // 动态获取项目路径
        String contextPath = request.getContextPath();
        response.sendRedirect(contextPath + "/success.html");
    }
}

8.6 请求转发 vs 重定向

特性 请求转发(forward) 重定向(redirect)
地址栏 不变 变化
请求次数 1次 2次
数据共享 可以共享request域数据 不能共享
跳转范围 只能跳转到服务器内部资源 可以跳转到任意资源
路径写法 不需要加项目名 需要加项目名
效率
使用场景 查询数据后展示 登录成功后跳转
java 复制代码
// 请求转发 - 路径不需要项目名
request.getRequestDispatcher("/targetServlet").forward(request, response);

// 重定向 - 路径需要项目名
response.sendRedirect(request.getContextPath() + "/targetServlet");

9. Servlet容器原理

9.1 什么是Servlet容器?

Servlet容器(也叫Web容器)是运行Servlet的环境,它负责:

  • 🏗️ 创建和管理Servlet实例
  • 📡 处理网络通信
  • 🔄 管理Servlet生命周期
  • 🗺️ URL到Servlet的映射

9.2 Servlet容器的本质

Servlet容器的本质是一个HashMap,用于存储URL路径和Servlet对象的映射关系。

java 复制代码
/**
 * Servlet容器的简化模型
 * 
 * key: Servlet的访问路径(如 "/login", "/user")
 * value: 对应的Servlet对象实例
 */
Map<String, Servlet> servletContainer = new HashMap<>();

// 容器中存储的内容示例:
// "/login"  →  LoginServlet对象 (0x1)
// "/user"   →  UserServlet对象 (0x2)
// "/order"  →  OrderServlet对象 (0x3)
复制代码
Servlet容器(HashMap)
┌─────────────────────────────────────────────────────────┐
│                                                         │
│   ┌──────────┬─────────────────────────────────────┐   │
│   │   Key    │              Value                   │   │
│   ├──────────┼─────────────────────────────────────┤   │
│   │  /login  │  LoginServlet对象 (内存地址: 0x1)    │   │
│   ├──────────┼─────────────────────────────────────┤   │
│   │  /user   │  UserServlet对象 (内存地址: 0x2)     │   │
│   ├──────────┼─────────────────────────────────────┤   │
│   │  /order  │  OrderServlet对象 (内存地址: 0x3)    │   │
│   └──────────┴─────────────────────────────────────┘   │
│                                                         │
└─────────────────────────────────────────────────────────┘

9.3 请求处理流程

复制代码
1. 浏览器发送请求:http://localhost:8080/myapp/login
                                              ↓
2. Tomcat解析请求,提取路径:/login
                                              ↓
3. 在Servlet容器(HashMap)中查找:
   servletContainer.get("/login")
                                              ↓
4. 找到对应的Servlet对象:LoginServlet
                                              ↓
5. 调用Servlet的service方法处理请求:
   loginServlet.service(request, response)
                                              ↓
6. 根据请求方式调用doGet或doPost:
   loginServlet.doPost(request, response)
                                              ↓
7. 生成响应数据返回给浏览器

9.4 Servlet是单例的

java 复制代码
/**
 * Servlet是单例的 - 整个应用中只有一个实例
 * 
 * 这意味着:
 * 1. 多个请求共享同一个Servlet对象
 * 2. Servlet中的成员变量是共享的(线程不安全)
 * 3. 不要在Servlet中定义可修改的成员变量
 */
@WebServlet("/singletonDemo")
public class SingletonServlet extends HttpServlet {
    
    // ⚠️ 危险!这个变量会被多个请求共享
    private int count = 0;
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // 每次请求count都会增加
        count++;
        
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("访问次数:" + count);
        
        // 如果两个用户同时访问,可能出现数据混乱
    }
}

⚠️ 线程安全问题:由于Servlet是单例的,多个线程(请求)会同时访问同一个Servlet对象。如果Servlet中有可修改的成员变量,就会出现线程安全问题。

解决方案

  1. 不要在Servlet中定义可修改的成员变量
  2. 使用局部变量(每个线程独立)
  3. 使用同步机制(synchronized)- 但会影响性能

10. 自定义Tomcat实现原理

10.1 实现思路

要实现一个简易版的Tomcat,需要完成以下步骤:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                     自定义Tomcat实现流程                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 启动阶段                                                     │
│     ├── 扫描指定文件夹下的所有Servlet类文件                        │
│     ├── 获取@WebServlet注解中的访问路径                           │
│     └── 通过反射创建Servlet对象,存入HashMap容器                   │
│                                                                 │
│  2. 监听阶段                                                     │
│     ├── 创建ServerSocket,监听指定端口(如8080)                   │
│     └── 等待客户端连接(BIO阻塞等待)                              │
│                                                                 │
│  3. 请求处理阶段                                                  │
│     ├── 接收客户端Socket连接                                      │
│     ├── 通过InputStream读取请求数据                               │
│     ├── 解析请求,封装成HttpServletRequest对象                    │
│     ├── 根据请求路径从容器中获取对应的Servlet                      │
│     ├── 调用Servlet的service方法处理请求                          │
│     └── 通过OutputStream返回响应数据                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

10.2 核心代码实现

10.2.1 简易HttpServletRequest实现
java 复制代码
package com.example.tomcat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

/**
 * 简易版HttpServletRequest
 * 
 * 功能:解析HTTP请求,提取请求方法、URI、参数等信息
 */
public class MyHttpServletRequest {
    
    // 请求方法(GET、POST等)
    private String method;
    
    // 请求URI(如 /login)
    private String uri;
    
    // 请求参数
    private Map<String, String> parameters = new HashMap<>();
    
    // 请求头
    private Map<String, String> headers = new HashMap<>();
    
    /**
     * 构造方法 - 解析输入流中的HTTP请求
     * 
     * @param inputStream Socket的输入流
     */
    public MyHttpServletRequest(InputStream inputStream) throws IOException {
        // 使用BufferedReader读取请求数据
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(inputStream, "UTF-8")
        );
        
        // 1. 读取请求行(第一行)
        // 格式:GET /login?username=admin HTTP/1.1
        String requestLine = reader.readLine();
        if (requestLine != null && !requestLine.isEmpty()) {
            parseRequestLine(requestLine);
        }
        
        // 2. 读取请求头
        String headerLine;
        while ((headerLine = reader.readLine()) != null && !headerLine.isEmpty()) {
            parseHeader(headerLine);
        }
        
        // 3. 如果是POST请求,读取请求体
        if ("POST".equalsIgnoreCase(method)) {
            // 获取Content-Length
            String contentLength = headers.get("Content-Length");
            if (contentLength != null) {
                int length = Integer.parseInt(contentLength);
                char[] body = new char[length];
                reader.read(body, 0, length);
                parseParameters(new String(body));
            }
        }
    }
    
    /**
     * 解析请求行
     * 
     * 输入示例:GET /login?username=admin&password=123 HTTP/1.1
     */
    private void parseRequestLine(String requestLine) {
        // 按空格分割:["GET", "/login?username=admin&password=123", "HTTP/1.1"]
        String[] parts = requestLine.split(" ");
        
        if (parts.length >= 2) {
            // 获取请求方法
            this.method = parts[0];  // GET
            
            // 获取URI和参数
            String fullUri = parts[1];  // /login?username=admin&password=123
            
            // 检查是否有查询参数
            int questionMarkIndex = fullUri.indexOf("?");
            if (questionMarkIndex != -1) {
                // 有参数
                this.uri = fullUri.substring(0, questionMarkIndex);  // /login
                String queryString = fullUri.substring(questionMarkIndex + 1);  // username=admin&password=123
                parseParameters(queryString);
            } else {
                // 无参数
                this.uri = fullUri;
            }
        }
    }
    
    /**
     * 解析请求头
     * 
     * 输入示例:Content-Type: application/x-www-form-urlencoded
     */
    private void parseHeader(String headerLine) {
        int colonIndex = headerLine.indexOf(":");
        if (colonIndex != -1) {
            String name = headerLine.substring(0, colonIndex).trim();
            String value = headerLine.substring(colonIndex + 1).trim();
            headers.put(name, value);
        }
    }
    
    /**
     * 解析参数
     * 
     * 输入示例:username=admin&password=123
     */
    private void parseParameters(String queryString) {
        if (queryString == null || queryString.isEmpty()) {
            return;
        }
        
        // 按&分割:["username=admin", "password=123"]
        String[] pairs = queryString.split("&");
        for (String pair : pairs) {
            // 按=分割:["username", "admin"]
            String[] keyValue = pair.split("=");
            if (keyValue.length == 2) {
                parameters.put(keyValue[0], keyValue[1]);
            } else if (keyValue.length == 1) {
                parameters.put(keyValue[0], "");
            }
        }
    }
    
    // ========== Getter方法 ==========
    
    public String getMethod() {
        return method;
    }
    
    public String getRequestURI() {
        return uri;
    }
    
    public String getParameter(String name) {
        return parameters.get(name);
    }
    
    public Map<String, String> getParameterMap() {
        return parameters;
    }
    
    public String getHeader(String name) {
        return headers.get(name);
    }
}
10.2.2 简易HttpServletResponse实现
java 复制代码
package com.example.tomcat;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;

/**
 * 简易版HttpServletResponse
 * 
 * 功能:构建HTTP响应,向客户端发送数据
 */
public class MyHttpServletResponse {
    
    // Socket的输出流
    private OutputStream outputStream;
    
    // 响应状态码
    private int status = 200;
    
    // 响应内容类型
    private String contentType = "text/html;charset=UTF-8";
    
    // 响应体内容
    private StringWriter bodyWriter = new StringWriter();
    private PrintWriter writer = new PrintWriter(bodyWriter);
    
    /**
     * 构造方法
     * 
     * @param outputStream Socket的输出流
     */
    public MyHttpServletResponse(OutputStream outputStream) {
        this.outputStream = outputStream;
    }
    
    /**
     * 设置响应状态码
     */
    public void setStatus(int status) {
        this.status = status;
    }
    
    /**
     * 设置内容类型
     */
    public void setContentType(String contentType) {
        this.contentType = contentType;
    }
    
    /**
     * 获取输出流(用于写入响应内容)
     */
    public PrintWriter getWriter() {
        return writer;
    }
    
    /**
     * 发送响应到客户端
     * 
     * 构建完整的HTTP响应报文并发送
     */
    public void send() throws IOException {
        // 获取响应体内容
        String body = bodyWriter.toString();
        
        // 构建HTTP响应报文
        StringBuilder response = new StringBuilder();
        
        // 1. 响应行
        response.append("HTTP/1.1 ").append(status).append(" ");
        response.append(getStatusMessage(status)).append("\r\n");
        
        // 2. 响应头
        response.append("Content-Type: ").append(contentType).append("\r\n");
        response.append("Content-Length: ").append(body.getBytes("UTF-8").length).append("\r\n");
        response.append("Connection: close\r\n");
        response.append("\r\n");  // 空行,分隔头和体
        
        // 3. 响应体
        response.append(body);
        
        // 发送响应
        outputStream.write(response.toString().getBytes("UTF-8"));
        outputStream.flush();
    }
    
    /**
     * 根据状态码获取状态消息
     */
    private String getStatusMessage(int status) {
        switch (status) {
            case 200: return "OK";
            case 302: return "Found";
            case 400: return "Bad Request";
            case 404: return "Not Found";
            case 500: return "Internal Server Error";
            default: return "Unknown";
        }
    }
}
10.2.3 简易Tomcat服务器实现
java 复制代码
package com.example.tomcat;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

/**
 * 简易版Tomcat服务器
 * 
 * 实现功能:
 * 1. 扫描Servlet类并注册到容器
 * 2. 监听端口,接收HTTP请求
 * 3. 解析请求,调用对应的Servlet处理
 * 4. 返回响应给客户端
 */
public class MyTomcat {
    
    // 监听端口
    private int port;
    
    // Servlet容器(核心!)
    // key: 访问路径(如 "/login")
    // value: Servlet对象
    private Map<String, HttpServlet> servletContainer = new HashMap<>();
    
    /**
     * 构造方法
     * 
     * @param port 监听端口
     */
    public MyTomcat(int port) {
        this.port = port;
    }
    
    /**
     * 启动服务器
     */
    public void start() {
        System.out.println("========================================");
        System.out.println("       MyTomcat 服务器启动中...          ");
        System.out.println("========================================");
        
        // 1. 扫描并注册Servlet
        scanAndRegisterServlets();
        
        // 2. 启动Socket监听
        startSocketListener();
    }
    
    /**
     * 扫描指定包下的Servlet类,并注册到容器中
     * 
     * 实现步骤:
     * 1. 扫描指定目录下的所有.class文件
     * 2. 检查类是否有@WebServlet注解
     * 3. 如果有,通过反射创建实例,存入容器
     */
    private void scanAndRegisterServlets() {
        System.out.println("[启动] 开始扫描Servlet...");
        
        try {
            // 这里简化处理,手动注册几个Servlet
            // 实际Tomcat会扫描WEB-INF/classes目录
            
            // 示例:注册LoginServlet
            // 假设LoginServlet类上有 @WebServlet("/login") 注解
            Class<?> loginClass = Class.forName("com.example.servlet.LoginServlet");
            WebServlet annotation = loginClass.getAnnotation(WebServlet.class);
            
            if (annotation != null) {
                // 获取注解中的访问路径
                String[] urlPatterns = annotation.urlPatterns();
                if (urlPatterns.length == 0) {
                    urlPatterns = annotation.value();
                }
                
                // 通过反射创建Servlet实例
                HttpServlet servlet = (HttpServlet) loginClass.newInstance();
                
                // 注册到容器中
                for (String pattern : urlPatterns) {
                    servletContainer.put(pattern, servlet);
                    System.out.println("[注册] " + pattern + " -> " + loginClass.getSimpleName());
                }
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        System.out.println("[启动] Servlet扫描完成,共注册 " + servletContainer.size() + " 个Servlet");
    }
    
    /**
     * 启动Socket监听,等待客户端连接
     */
    private void startSocketListener() {
        try {
            // 创建ServerSocket,监听指定端口
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("[启动] 服务器启动成功,监听端口:" + port);
            System.out.println("[启动] 访问地址:http://localhost:" + port);
            System.out.println("========================================\n");
            
            // 循环等待客户端连接(BIO阻塞模式)
            while (true) {
                // accept()方法会阻塞,直到有客户端连接
                Socket clientSocket = serverSocket.accept();
                
                // 为每个客户端创建一个新线程处理请求
                new Thread(() -> handleRequest(clientSocket)).start();
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 处理客户端请求
     * 
     * @param clientSocket 客户端Socket连接
     */
    private void handleRequest(Socket clientSocket) {
        try {
            System.out.println("[请求] 收到来自 " + clientSocket.getInetAddress() + " 的请求");
            
            // 1. 获取输入流,读取请求数据
            InputStream inputStream = clientSocket.getInputStream();
            
            // 2. 解析请求,封装成Request对象
            MyHttpServletRequest request = new MyHttpServletRequest(inputStream);
            
            // 3. 创建Response对象
            OutputStream outputStream = clientSocket.getOutputStream();
            MyHttpServletResponse response = new MyHttpServletResponse(outputStream);
            
            // 4. 获取请求的URI
            String uri = request.getRequestURI();
            System.out.println("[请求] " + request.getMethod() + " " + uri);
            
            // 5. 从容器中查找对应的Servlet
            HttpServlet servlet = servletContainer.get(uri);
            
            if (servlet != null) {
                // 6. 找到了,调用Servlet处理请求
                // 注意:这里简化了,实际应该调用service方法
                // servlet.service(request, response);
                
                // 由于我们的Request/Response是简化版,这里模拟处理
                response.setContentType("text/html;charset=UTF-8");
                response.getWriter().write("<h1>请求处理成功</h1>");
                response.getWriter().write("<p>访问路径:" + uri + "</p>");
                response.getWriter().write("<p>请求方法:" + request.getMethod() + "</p>");
                
            } else {
                // 7. 没找到,检查是否是静态资源
                if (isStaticResource(uri)) {
                    // 返回静态资源
                    serveStaticResource(uri, response);
                } else {
                    // 返回404错误
                    response.setStatus(404);
                    response.setContentType("text/html;charset=UTF-8");
                    response.getWriter().write("<h1>404 Not Found</h1>");
                    response.getWriter().write("<p>请求的资源不存在:" + uri + "</p>");
                }
            }
            
            // 8. 发送响应
            response.send();
            
            // 9. 关闭连接
            clientSocket.close();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 判断是否是静态资源请求
     */
    private boolean isStaticResource(String uri) {
        return uri.endsWith(".html") || 
               uri.endsWith(".css") || 
               uri.endsWith(".js") || 
               uri.endsWith(".jpg") || 
               uri.endsWith(".png") ||
               uri.endsWith(".gif");
    }
    
    /**
     * 返回静态资源
     */
    private void serveStaticResource(String uri, MyHttpServletResponse response) {
        // 这里简化处理,实际需要读取文件内容
        response.setStatus(200);
        response.getWriter().write("静态资源:" + uri);
    }
    
    /**
     * 主方法 - 启动服务器
     */
    public static void main(String[] args) {
        MyTomcat tomcat = new MyTomcat(8080);
        tomcat.start();
    }
}

10.3 反射创建Servlet对象

java 复制代码
/**
 * 通过反射创建Servlet对象的三种方式
 * 
 * 反射是Java的一种机制,可以在运行时动态获取类的信息并操作类
 */
public class ReflectDemo {
    
    public static void main(String[] args) throws Exception {
        
        // ========== 方式1:Class.forName() ==========
        // 最常用,通过类的全限定名获取Class对象
        Class<?> clazz1 = Class.forName("com.example.servlet.LoginServlet");
        Object servlet1 = clazz1.newInstance();  // 创建实例
        System.out.println("方式1:" + servlet1);
        
        // ========== 方式2:类名.class ==========
        // 需要导入类,编译时就确定
        Class<?> clazz2 = LoginServlet.class;
        Object servlet2 = clazz2.newInstance();
        System.out.println("方式2:" + servlet2);
        
        // ========== 方式3:对象.getClass() ==========
        // 通过已有对象获取Class对象
        LoginServlet servlet = new LoginServlet();
        Class<?> clazz3 = servlet.getClass();
        System.out.println("方式3:" + clazz3.getName());
        
        // ========== 获取注解信息 ==========
        WebServlet annotation = clazz1.getAnnotation(WebServlet.class);
        if (annotation != null) {
            String[] urlPatterns = annotation.urlPatterns();
            System.out.println("访问路径:" + Arrays.toString(urlPatterns));
        }
        
        // ========== 调用方法 ==========
        // 获取doGet方法
        Method doGetMethod = clazz1.getDeclaredMethod("doGet", 
            HttpServletRequest.class, HttpServletResponse.class);
        // 设置可访问(如果是private方法)
        doGetMethod.setAccessible(true);
        // 调用方法
        // doGetMethod.invoke(servlet1, request, response);
    }
}

10.4 完整的请求处理流程图

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                           完整请求处理流程                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  浏览器                    Tomcat服务器                      Servlet        │
│    │                          │                               │            │
│    │  1. 发送HTTP请求         │                               │            │
│    │  GET /login HTTP/1.1     │                               │            │
│    │ ─────────────────────▶  │                               │            │
│    │                          │                               │            │
│    │                    2. Socket接收请求                      │            │
│    │                    3. InputStream读取数据                 │            │
│    │                    4. 解析请求,创建Request对象            │            │
│    │                    5. 创建Response对象                    │            │
│    │                          │                               │            │
│    │                    6. 根据URI查找Servlet                  │            │
│    │                    servletContainer.get("/login")        │            │
│    │                          │                               │            │
│    │                          │  7. 调用service方法            │            │
│    │                          │ ─────────────────────────────▶│            │
│    │                          │                               │            │
│    │                          │                    8. 根据请求方式          │
│    │                          │                       调用doGet/doPost     │
│    │                          │                               │            │
│    │                          │                    9. 处理业务逻辑          │
│    │                          │                       查询数据库等          │
│    │                          │                               │            │
│    │                          │  10. 写入响应数据              │            │
│    │                          │ ◀─────────────────────────────│            │
│    │                          │                               │            │
│    │                   11. OutputStream发送响应                │            │
│    │  12. 接收响应             │                               │            │
│    │ ◀─────────────────────  │                               │            │
│    │                          │                               │            │
│    │  13. 渲染页面             │                               │            │
│    │                          │                               │            │
└─────────────────────────────────────────────────────────────────────────────┘

11. 常见面试题

📝 基础概念题

Q1: 什么是Servlet?它的作用是什么?

答案

Servlet是运行在服务器端的Java小程序,是Sun公司提供的用于开发动态Web资源的技术规范。

主要作用

  1. 接收客户端(浏览器)发送的HTTP请求
  2. 处理请求(调用业务逻辑、访问数据库等)
  3. 生成动态内容并返回给客户端

核心特点

  • Servlet是单例的,整个应用只有一个实例
  • Servlet是多线程的,多个请求会并发访问同一个Servlet
  • Servlet由容器(如Tomcat)管理生命周期

Q2: Servlet的生命周期是怎样的?

答案

Servlet的生命周期包含4个阶段:

复制代码
1. 加载和实例化
   - 时机:第一次访问时(或配置loadOnStartup后在启动时)
   - 操作:容器通过反射创建Servlet实例
   - 次数:只执行一次

2. 初始化(init方法)
   - 时机:实例创建后立即调用
   - 操作:读取配置、初始化资源
   - 次数:只执行一次

3. 服务(service方法)
   - 时机:每次请求到达时
   - 操作:处理请求,生成响应
   - 次数:每次请求都执行

4. 销毁(destroy方法)
   - 时机:容器关闭或应用卸载时
   - 操作:释放资源、保存数据
   - 次数:只执行一次

Q3: Servlet是线程安全的吗?如何保证线程安全?

答案
Servlet本身不是线程安全的。因为Servlet是单例的,多个请求会并发访问同一个Servlet实例。

线程安全问题示例

java 复制代码
public class UnsafeServlet extends HttpServlet {
    private int count = 0;  // 共享变量,线程不安全!
    
    protected void doGet(...) {
        count++;  // 多线程并发修改,可能出现数据不一致
    }
}

保证线程安全的方法

  1. 避免使用成员变量(推荐)
  2. 使用局部变量(每个线程独立)
  3. 使用同步机制(synchronized,但影响性能)
  4. 使用ThreadLocal(线程本地变量)
java 复制代码
public class SafeServlet extends HttpServlet {
    protected void doGet(...) {
        int count = 0;  // 局部变量,线程安全
        count++;
    }
}

Q4: 请求转发(forward)和重定向(redirect)的区别?

答案

特性 请求转发(forward) 重定向(redirect)
请求次数 1次 2次
地址栏 不变 变化
数据共享 可以共享request域数据 不能共享
跳转范围 只能跳转服务器内部资源 可以跳转任意URL
效率 高(服务器内部跳转) 低(需要两次请求)
路径写法 不需要项目名 需要项目名

代码示例

java 复制代码
// 请求转发
request.getRequestDispatcher("/target").forward(request, response);

// 重定向
response.sendRedirect(request.getContextPath() + "/target");

使用场景

  • 转发:查询数据后展示结果页面
  • 重定向:登录成功后跳转首页、防止表单重复提交

Q5: GET和POST请求的区别?

答案

特性 GET POST
参数位置 URL中(?后面) 请求体中
参数长度 有限制(约2KB) 无限制
安全性 低(参数可见) 相对高(参数不可见)
缓存 可以被缓存 不会被缓存
书签 可以保存为书签 不能保存为书签
幂等性 幂等(多次请求结果相同) 非幂等
使用场景 查询数据 提交数据、上传文件

Q6: 什么是Servlet容器?它的作用是什么?

答案
Servlet容器(也叫Web容器)是运行Servlet的环境,如Tomcat、Jetty等。

主要作用

  1. 管理Servlet生命周期:创建、初始化、调用、销毁
  2. 处理网络通信:监听端口、接收请求、发送响应
  3. URL映射:将请求URL映射到对应的Servlet
  4. 多线程处理:为每个请求分配线程
  5. 安全管理:身份认证、访问控制

容器的本质

java 复制代码
// Servlet容器本质上是一个HashMap
Map<String, Servlet> container = new HashMap<>();
// key: 访问路径
// value: Servlet对象

📝 进阶面试题

Q7: Servlet的三大作用域是什么?

答案

作用域 对象 范围 生命周期
request HttpServletRequest 一次请求 请求开始到响应结束
session HttpSession 一次会话 第一次访问到会话超时/失效
application ServletContext 整个应用 应用启动到应用关闭

使用方法

java 复制代码
// request域
request.setAttribute("key", value);
request.getAttribute("key");

// session域
HttpSession session = request.getSession();
session.setAttribute("key", value);
session.getAttribute("key");

// application域
ServletContext context = getServletContext();
context.setAttribute("key", value);
context.getAttribute("key");

Q8: 如何解决Servlet中的中文乱码问题?

答案

1. 请求乱码(获取参数时乱码)

java 复制代码
// POST请求:设置请求体编码(必须在获取参数之前)
request.setCharacterEncoding("UTF-8");

// GET请求(Tomcat 8.0+默认UTF-8,无需处理)
// Tomcat 7及以下需要在server.xml中配置:
// <Connector URIEncoding="UTF-8" ... />

2. 响应乱码(输出内容乱码)

java 复制代码
// 方式1:同时设置内容类型和编码(推荐)
response.setContentType("text/html;charset=UTF-8");

// 方式2:分别设置
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html");

Q9: 什么是loadOnStartup?有什么作用?

答案
loadOnStartup 是Servlet的配置属性,用于指定Servlet的加载时机。

默认行为:Servlet在第一次被访问时才创建实例(懒加载)

配置loadOnStartup后:Servlet在Tomcat启动时就创建实例

java 复制代码
// 注解方式
@WebServlet(urlPatterns = "/init", loadOnStartup = 1)

// web.xml方式
<servlet>
    <servlet-name>InitServlet</servlet-name>
    <servlet-class>com.example.InitServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

数值含义

  • 负数或不配置:第一次访问时加载(默认)
  • 0或正数:启动时加载,数字越小优先级越高

使用场景

  • 初始化配置信息
  • 预加载数据到缓存
  • 启动定时任务

Q10: Servlet和JSP的区别?

答案

特性 Servlet JSP
本质 Java类 HTML页面中嵌入Java代码
编写方式 在Java代码中输出HTML 在HTML中嵌入Java代码
运行机制 直接编译运行 先转换为Servlet再编译运行
适用场景 处理业务逻辑 展示页面
修改后 需要重新编译 自动重新编译

关系:JSP本质上就是Servlet,JSP会被容器转换成Servlet类再执行。

复制代码
JSP文件 → 转换为Servlet源码 → 编译为.class → 执行

Q11: 什么是Cookie和Session?它们的区别?

答案

Cookie :存储在客户端(浏览器)的小型文本数据
Session:存储在服务器端的会话数据

特性 Cookie Session
存储位置 客户端(浏览器) 服务器端
安全性 较低(可被查看/篡改) 较高
存储大小 有限制(约4KB) 无限制(受服务器内存限制)
生命周期 可设置过期时间 默认30分钟(可配置)
跨域 不支持 不支持

工作原理

复制代码
1. 用户首次访问,服务器创建Session,生成SessionID
2. 服务器将SessionID通过Cookie发送给浏览器
3. 浏览器后续请求携带Cookie(包含SessionID)
4. 服务器根据SessionID找到对应的Session
java 复制代码
// 获取Session
HttpSession session = request.getSession();

// 存储数据
session.setAttribute("user", userObject);

// 获取数据
User user = (User) session.getAttribute("user");

// 设置过期时间(秒)
session.setMaxInactiveInterval(1800);  // 30分钟

// 销毁Session
session.invalidate();

Q12: 如何实现Servlet的单点登录?

答案

单点登录(SSO)是指用户只需登录一次,就可以访问多个相互信任的应用系统。

实现思路

复制代码
1. 用户访问系统A,未登录,跳转到认证中心
2. 用户在认证中心登录成功,生成Token
3. 认证中心将Token返回给系统A
4. 系统A验证Token,登录成功
5. 用户访问系统B,携带Token
6. 系统B向认证中心验证Token
7. 验证通过,用户无需再次登录

Q13: Servlet中如何实现文件上传?

答案

java 复制代码
/**
 * 文件上传Servlet
 * 
 * 注意:需要添加 @MultipartConfig 注解
 */
@WebServlet("/upload")
@MultipartConfig(
    maxFileSize = 1024 * 1024 * 10,      // 单个文件最大10MB
    maxRequestSize = 1024 * 1024 * 50,   // 请求最大50MB
    fileSizeThreshold = 1024 * 1024      // 超过1MB写入磁盘
)
public class FileUploadServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        request.setCharacterEncoding("UTF-8");
        
        // 获取上传的文件
        Part filePart = request.getPart("file");
        
        // 获取文件名
        String fileName = filePart.getSubmittedFileName();
        
        // 获取保存路径
        String uploadPath = getServletContext().getRealPath("/uploads");
        
        // 创建目录
        File uploadDir = new File(uploadPath);
        if (!uploadDir.exists()) {
            uploadDir.mkdirs();
        }
        
        // 保存文件
        String filePath = uploadPath + File.separator + fileName;
        filePart.write(filePath);
        
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("文件上传成功:" + fileName);
    }
}

Q14: 什么是过滤器(Filter)?有什么作用?

答案
Filter(过滤器) 是Servlet规范中的组件,用于在请求到达Servlet之前或响应返回客户端之前进行预处理。

作用

  1. 统一设置编码
  2. 登录验证
  3. 权限控制
  4. 日志记录
  5. 敏感词过滤
java 复制代码
/**
 * 编码过滤器示例
 */
@WebFilter("/*")  // 过滤所有请求
public class EncodingFilter implements Filter {
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("过滤器初始化");
    }
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        
        // 前置处理:设置编码
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        
        // 放行,继续执行后续的Filter或Servlet
        chain.doFilter(request, response);
        
        // 后置处理(可选)
        System.out.println("请求处理完成");
    }
    
    @Override
    public void destroy() {
        System.out.println("过滤器销毁");
    }
}

执行流程

复制代码
请求 → Filter1 → Filter2 → Servlet → Filter2 → Filter1 → 响应

Q15: 什么是监听器(Listener)?有哪些类型?

答案
Listener(监听器) 用于监听Web应用中的事件,当特定事件发生时自动执行相应的代码。

常用监听器类型

监听器 监听对象 触发时机
ServletContextListener ServletContext 应用启动/关闭
HttpSessionListener HttpSession Session创建/销毁
ServletRequestListener ServletRequest 请求开始/结束
HttpSessionAttributeListener Session属性 属性添加/修改/删除
java 复制代码
/**
 * 应用启动监听器
 */
@WebListener
public class AppStartListener implements ServletContextListener {
    
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("应用启动了!");
        // 可以在这里初始化资源、加载配置等
    }
    
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("应用关闭了!");
        // 可以在这里释放资源
    }
}

📝 实战面试题

Q16: 如何防止表单重复提交?

答案

方案1:Token机制(推荐)

java 复制代码
// 1. 显示表单时,生成Token存入Session
String token = UUID.randomUUID().toString();
session.setAttribute("formToken", token);

// 2. 表单中携带Token
<input type="hidden" name="token" value="${formToken}">

// 3. 提交时验证Token
String formToken = request.getParameter("token");
String sessionToken = (String) session.getAttribute("formToken");

if (sessionToken != null && sessionToken.equals(formToken)) {
    // Token有效,处理请求
    session.removeAttribute("formToken");  // 移除Token,防止重复提交
    // 处理业务逻辑...
} else {
    // Token无效,拒绝请求
    response.getWriter().write("请勿重复提交!");
}

方案2:PRG模式(Post-Redirect-Get)

java 复制代码
// 处理POST请求后,重定向到结果页面
protected void doPost(...) {
    // 处理业务逻辑...
    
    // 重定向,而不是转发
    response.sendRedirect("success.html");
}

方案3:前端禁用按钮

javascript 复制代码
document.getElementById("submitBtn").onclick = function() {
    this.disabled = true;  // 禁用按钮
    this.form.submit();
};

Q17: 如何实现登录验证过滤器?

答案

java 复制代码
/**
 * 登录验证过滤器
 * 
 * 功能:检查用户是否登录,未登录则跳转到登录页面
 */
@WebFilter("/*")
public class LoginFilter implements Filter {
    
    // 不需要登录就能访问的路径
    private static final Set<String> EXCLUDE_PATHS = new HashSet<>(Arrays.asList(
        "/login",           // 登录页面
        "/doLogin",         // 登录处理
        "/register",        // 注册页面
        "/css/",            // 静态资源
        "/js/",
        "/images/"
    ));
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        
        // 获取请求路径
        String uri = request.getRequestURI();
        String contextPath = request.getContextPath();
        String path = uri.substring(contextPath.length());
        
        // 检查是否是排除路径
        if (isExcludePath(path)) {
            chain.doFilter(request, response);  // 放行
            return;
        }
        
        // 检查是否已登录
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("user") != null) {
            chain.doFilter(request, response);  // 已登录,放行
        } else {
            // 未登录,重定向到登录页面
            response.sendRedirect(contextPath + "/login");
        }
    }
    
    /**
     * 检查是否是排除路径
     */
    private boolean isExcludePath(String path) {
        for (String excludePath : EXCLUDE_PATHS) {
            if (path.startsWith(excludePath)) {
                return true;
            }
        }
        return false;
    }
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}
    
    @Override
    public void destroy() {}
}

12. 完整实战案例:用户登录系统

12.1 项目结构

复制代码
LoginDemo/
├── src/
│   └── com/
│       └── example/
│           ├── servlet/
│           │   ├── LoginServlet.java      # 登录处理
│           │   ├── LogoutServlet.java     # 退出登录
│           │   └── HomeServlet.java       # 首页
│           ├── filter/
│           │   └── LoginFilter.java       # 登录过滤器
│           └── entity/
│               └── User.java              # 用户实体类
├── web/
│   ├── WEB-INF/
│   │   └── web.xml
│   ├── login.html                         # 登录页面
│   └── home.html                          # 首页
└── pom.xml

12.2 用户实体类

java 复制代码
package com.example.entity;

/**
 * 用户实体类
 */
public class User {
    
    private Integer id;          // 用户ID
    private String username;     // 用户名
    private String password;     // 密码
    private String email;        // 邮箱
    
    // 无参构造
    public User() {}
    
    // 全参构造
    public User(Integer id, String username, String password, String email) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
    }
    
    // Getter和Setter方法
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    @Override
    public String toString() {
        return "User{id=" + id + ", username='" + username + "', email='" + email + "'}";
    }
}

12.3 登录页面

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用户登录</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .login-container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 15px 35px rgba(0,0,0,0.2);
            width: 350px;
        }
        .login-container h2 {
            text-align: center;
            margin-bottom: 30px;
            color: #333;
        }
        .form-group {
            margin-bottom: 20px;
        }
        .form-group label {
            display: block;
            margin-bottom: 5px;
            color: #666;
        }
        .form-group input {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        .form-group input:focus {
            outline: none;
            border-color: #667eea;
        }
        .btn-login {
            width: 100%;
            padding: 12px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
        }
        .btn-login:hover {
            opacity: 0.9;
        }
        .error-msg {
            color: red;
            text-align: center;
            margin-bottom: 15px;
        }
    </style>
</head>
<body>
    <div class="login-container">
        <h2>用户登录</h2>
        
        <!-- 错误提示 -->
        <div class="error-msg" id="errorMsg"></div>
        
        <form action="doLogin" method="post" id="loginForm">
            <div class="form-group">
                <label for="username">用户名</label>
                <input type="text" id="username" name="username" 
                       placeholder="请输入用户名" required>
            </div>
            
            <div class="form-group">
                <label for="password">密码</label>
                <input type="password" id="password" name="password" 
                       placeholder="请输入密码" required>
            </div>
            
            <div class="form-group">
                <label>
                    <input type="checkbox" name="remember"> 记住我
                </label>
            </div>
            
            <button type="submit" class="btn-login">登 录</button>
        </form>
    </div>
    
    <script>
        // 获取URL参数中的错误信息
        const urlParams = new URLSearchParams(window.location.search);
        const error = urlParams.get('error');
        if (error) {
            document.getElementById('errorMsg').textContent = decodeURIComponent(error);
        }
    </script>
</body>
</html>

12.4 登录处理Servlet

java 复制代码
package com.example.servlet;

import com.example.entity.User;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;

/**
 * 登录处理Servlet
 * 
 * 功能:
 * 1. 接收用户名和密码
 * 2. 验证登录信息
 * 3. 登录成功:创建Session,跳转首页
 * 4. 登录失败:返回登录页,显示错误信息
 */
@WebServlet("/doLogin")
public class LoginServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // 1. 设置请求编码,防止中文乱码
        request.setCharacterEncoding("UTF-8");
        
        // 2. 获取表单参数
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String remember = request.getParameter("remember");
        
        // 3. 参数校验
        if (username == null || username.trim().isEmpty() ||
            password == null || password.trim().isEmpty()) {
            // 参数为空,返回登录页
            response.sendRedirect("login.html?error=" + 
                java.net.URLEncoder.encode("用户名和密码不能为空", "UTF-8"));
            return;
        }
        
        // 4. 验证登录(这里简化处理,实际应该查询数据库)
        User user = validateUser(username, password);
        
        if (user != null) {
            // 5. 登录成功
            
            // 5.1 创建Session,存储用户信息
            HttpSession session = request.getSession();
            session.setAttribute("user", user);
            session.setMaxInactiveInterval(30 * 60);  // 30分钟过期
            
            // 5.2 如果勾选"记住我",设置Cookie
            if ("on".equals(remember)) {
                // 创建Cookie保存用户名
                Cookie usernameCookie = new Cookie("remember_username", username);
                usernameCookie.setMaxAge(7 * 24 * 60 * 60);  // 7天有效
                usernameCookie.setPath("/");
                response.addCookie(usernameCookie);
            }
            
            // 5.3 重定向到首页(使用重定向防止表单重复提交)
            response.sendRedirect(request.getContextPath() + "/home");
            
        } else {
            // 6. 登录失败,返回登录页
            response.sendRedirect("login.html?error=" + 
                java.net.URLEncoder.encode("用户名或密码错误", "UTF-8"));
        }
    }
    
    /**
     * 验证用户登录信息
     * 
     * 实际项目中应该查询数据库
     * 这里简化处理,使用硬编码的用户信息
     */
    private User validateUser(String username, String password) {
        // 模拟数据库中的用户
        if ("admin".equals(username) && "123456".equals(password)) {
            return new User(1, "admin", "123456", "admin@example.com");
        }
        if ("user".equals(username) && "123456".equals(password)) {
            return new User(2, "user", "123456", "user@example.com");
        }
        return null;  // 用户不存在或密码错误
    }
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // GET请求重定向到登录页面
        response.sendRedirect("login.html");
    }
}

12.5 首页Servlet

java 复制代码
package com.example.servlet;

import com.example.entity.User;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 首页Servlet
 * 
 * 显示登录用户的信息
 */
@WebServlet("/home")
public class HomeServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // 设置响应编码
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        // 获取Session中的用户信息
        HttpSession session = request.getSession(false);
        User user = null;
        if (session != null) {
            user = (User) session.getAttribute("user");
        }
        
        // 输出页面
        out.println("<!DOCTYPE html>");
        out.println("<html>");
        out.println("<head>");
        out.println("<meta charset='UTF-8'>");
        out.println("<title>首页</title>");
        out.println("<style>");
        out.println("body { font-family: Arial; padding: 50px; background: #f5f5f5; }");
        out.println(".container { max-width: 600px; margin: 0 auto; background: white; ");
        out.println("             padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }");
        out.println("h1 { color: #333; }");
        out.println(".user-info { background: #f9f9f9; padding: 20px; border-radius: 5px; margin: 20px 0; }");
        out.println(".btn-logout { background: #e74c3c; color: white; padding: 10px 20px; ");
        out.println("              border: none; border-radius: 5px; cursor: pointer; text-decoration: none; }");
        out.println("</style>");
        out.println("</head>");
        out.println("<body>");
        out.println("<div class='container'>");
        
        if (user != null) {
            out.println("<h1>欢迎回来," + user.getUsername() + "!</h1>");
            out.println("<div class='user-info'>");
            out.println("<p><strong>用户ID:</strong>" + user.getId() + "</p>");
            out.println("<p><strong>用户名:</strong>" + user.getUsername() + "</p>");
            out.println("<p><strong>邮箱:</strong>" + user.getEmail() + "</p>");
            out.println("<p><strong>Session ID:</strong>" + session.getId() + "</p>");
            out.println("</div>");
            out.println("<a href='logout' class='btn-logout'>退出登录</a>");
        } else {
            out.println("<h1>您尚未登录</h1>");
            out.println("<p><a href='login.html'>点击登录</a></p>");
        }
        
        out.println("</div>");
        out.println("</body>");
        out.println("</html>");
    }
}

12.6 退出登录Servlet

java 复制代码
package com.example.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;

/**
 * 退出登录Servlet
 * 
 * 功能:销毁Session,清除Cookie,重定向到登录页
 */
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // 1. 获取Session并销毁
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();  // 销毁Session
        }
        
        // 2. 清除"记住我"的Cookie
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("remember_username".equals(cookie.getName())) {
                    cookie.setMaxAge(0);  // 设置为0表示立即删除
                    cookie.setPath("/");
                    response.addCookie(cookie);
                    break;
                }
            }
        }
        
        // 3. 重定向到登录页
        response.sendRedirect("login.html");
    }
}

13. 总结与学习路线

13.1 知识点总结

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                        Servlet + Tomcat 知识体系                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  基础概念                                                                    │
│  ├── Servlet:服务器端的Java小程序,处理HTTP请求                              │
│  ├── Tomcat:Servlet容器,管理Servlet的运行环境                               │
│  ├── Socket:网络通信的"管道"                                                │
│  └── BIO/NIO/AIO:不同的I/O模型                                              │
│                                                                             │
│  Servlet体系                                                                 │
│  ├── Servlet接口:定义生命周期方法                                            │
│  ├── GenericServlet:实现除service外的所有方法                                │
│  ├── HttpServlet:针对HTTP协议,根据请求方式分发                              │
│  └── 自定义Servlet:继承HttpServlet,重写doGet/doPost                        │
│                                                                             │
│  生命周期                                                                    │
│  ├── 加载实例化:反射创建对象(1次)                                          │
│  ├── 初始化init:加载配置(1次)                                              │
│  ├── 服务service:处理请求(每次请求)                                        │
│  └── 销毁destroy:释放资源(1次)                                             │
│                                                                             │
│  核心对象                                                                    │
│  ├── HttpServletRequest:封装请求信息                                        │
│  ├── HttpServletResponse:构建响应数据                                       │
│  ├── HttpSession:会话管理                                                   │
│  └── ServletContext:应用上下文                                              │
│                                                                             │
│  重要组件                                                                    │
│  ├── Filter:过滤器,请求预处理                                               │
│  ├── Listener:监听器,事件监听                                               │
│  └── Cookie/Session:状态管理                                                │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

13.2 学习路线建议

复制代码
第1阶段:基础入门(1-2周)
├── 理解HTTP协议基础
├── 安装配置Tomcat
├── 编写第一个Servlet
└── 掌握Request和Response

第2阶段:核心掌握(2-3周)
├── 深入理解Servlet生命周期
├── 掌握请求转发和重定向
├── 学习Cookie和Session
└── 实现简单的登录功能

第3阶段:进阶提升(2-3周)
├── 学习Filter过滤器
├── 学习Listener监听器
├── 文件上传下载
└── 实现完整的CRUD项目

第4阶段:框架学习
├── 学习JSP(了解即可)
├── 学习Spring MVC
├── 学习Spring Boot
└── 理解框架底层原理

13.3 推荐练习项目

  1. 用户管理系统:登录、注册、用户列表、修改、删除
  2. 留言板:发布留言、查看留言、删除留言
  3. 文件管理系统:上传、下载、预览、删除
  4. 简易博客:文章发布、分类、评论

📚 参考资料


📝 作者寄语

Servlet是Java Web开发的基石,虽然现在大多数项目都使用Spring Boot等框架,但理解Servlet的原理对于深入学习框架、排查问题都非常有帮助。

建议大家不要只看不练,一定要动手敲代码,遇到问题多思考、多调试。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注!有问题可以在评论区留言讨论。

相关推荐
想个名字太难1 小时前
ElasticSearch编程操作
java·elasticsearch·全文检索
小马爱打代码1 小时前
Spring AI:RAG 增强检索介绍
java·人工智能·spring
Franciz小测测1 小时前
Python APScheduler 定时任务 独立调度系统设计与实现
java·数据库·sql
天一生水water1 小时前
Eclipse数值模拟软件详细介绍(油藏开发的“工业级仿真引擎”)
java·数学建模·eclipse
谷粒.4 小时前
Cypress vs Playwright vs Selenium:现代Web自动化测试框架深度评测
java·前端·网络·人工智能·python·selenium·测试工具
uzong7 小时前
程序员从大厂回重庆工作一年
java·后端·面试
kyle~7 小时前
C++---value_type 解决泛型编程中的类型信息获取问题
java·开发语言·c++
开心香辣派小星11 小时前
23种设计模式-15解释器模式
java·设计模式·解释器模式
Halo_tjn11 小时前
虚拟机相关实验概述
java·开发语言·windows·计算机