🎯 适合人群 :Java Web开发初学者、准备面试的开发者
📚 阅读时间 :约30分钟
💡 建议:边看边敲代码,效果更佳!
📖 目录
- 什么是Servlet?
- 什么是Tomcat?
- Tomcat工作原理详解
- Servlet体系结构
- Servlet生命周期
- 第一个Servlet程序
- HttpServletRequest详解
- HttpServletResponse详解
- Servlet容器原理
- 自定义Tomcat实现原理
- 常见面试题
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中有可修改的成员变量,就会出现线程安全问题。
解决方案:
- 不要在Servlet中定义可修改的成员变量
- 使用局部变量(每个线程独立)
- 使用同步机制(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资源的技术规范。
主要作用:
- 接收客户端(浏览器)发送的HTTP请求
- 处理请求(调用业务逻辑、访问数据库等)
- 生成动态内容并返回给客户端
核心特点:
- 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++; // 多线程并发修改,可能出现数据不一致
}
}
保证线程安全的方法:
- 避免使用成员变量(推荐)
- 使用局部变量(每个线程独立)
- 使用同步机制(synchronized,但影响性能)
- 使用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等。
主要作用:
- 管理Servlet生命周期:创建、初始化、调用、销毁
- 处理网络通信:监听端口、接收请求、发送响应
- URL映射:将请求URL映射到对应的Servlet
- 多线程处理:为每个请求分配线程
- 安全管理:身份认证、访问控制
容器的本质:
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之前或响应返回客户端之前进行预处理。
作用:
- 统一设置编码
- 登录验证
- 权限控制
- 日志记录
- 敏感词过滤
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 推荐练习项目
- 用户管理系统:登录、注册、用户列表、修改、删除
- 留言板:发布留言、查看留言、删除留言
- 文件管理系统:上传、下载、预览、删除
- 简易博客:文章发布、分类、评论
📚 参考资料
📝 作者寄语:
Servlet是Java Web开发的基石,虽然现在大多数项目都使用Spring Boot等框架,但理解Servlet的原理对于深入学习框架、排查问题都非常有帮助。
建议大家不要只看不练,一定要动手敲代码,遇到问题多思考、多调试。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!有问题可以在评论区留言讨论。