学习目标:
- 理解 Servlet 在 Java Web 体系中的定位,并说清它与 HTTP、Tomcat、Web 应用之间的关系
- 掌握 Web 应用标准目录结构与 Maven Web 工程目录结构的对应关系
- 能在 IDEA 中完成 Tomcat 配置、项目部署与基础访问验证
- 掌握
HttpServlet的开发方式、请求分发机制与常见注解配置- 能解释 Servlet 生命周期、
loadOnStartup与单例多线程模型带来的影响- 能区分
ServletConfig与ServletContext的职责,并完成基础案例本章重点:
- Servlet 到底解决什么问题,以及它为什么必须运行在容器中
@WebServlet、URL-pattern、doGet()、doPost()这些核心开发入口- 生命周期:
init()、service()、destroy()的执行时机ServletConfig与ServletContext的使用边界- 404、500、乱码、线程安全等高频问题的定位方式
1. 前置知识准备
- 已理解 HTTP 请求与响应的基本结构
- 已完成 Tomcat 的安装、启动与基础部署
- 已具备 Java 面向对象基础:类、继承、接口、多态
- 已了解注解基本语法,如
@interface、@Target、@Retention
如果你对 Tomcat 的目录结构、部署方式和 URL 映射还不熟悉,建议先回看上一章《Tomcat服务器详解》,再学习本章。
2. Servlet 在 Java Web 中的定位
1.1 为什么会出现 Servlet
仅靠静态资源,服务器只能“把现成文件返回给浏览器”。
但真实业务通常需要:
- 根据用户输入动态生成内容
- 从数据库读取数据后再响应
- 做登录校验、权限控制、表单处理
- 根据不同 URL 执行不同业务逻辑
这时就需要一类运行在服务器端、可以接收请求并生成响应的程序。Servlet 就是 Java Web 中最早、最基础的这类组件。
1.2 Servlet 到底是什么
Servlet = Server + Applet,可以理解为“运行在服务器端的小程序”。
更准确地说:
- Servlet 是一个 Java 类
- 它不是独立运行的主程序
- 它必须运行在 Web 容器中,例如 Tomcat
- 它的任务是接收 HTTP 请求、处理业务、返回 HTTP 响应
可以用下面这条链路理解:
浏览器 -> HTTP请求 -> Tomcat -> Servlet -> 业务处理 -> HTTP响应 -> 浏览器1.3 Servlet 与静态资源、动态资源
| 类型 | 处理方式 | 例子 |
|---|---|---|
| 静态资源 | 服务器直接读取并返回 | HTML、CSS、JS、图片 |
| 动态资源 | 服务器程序参与处理后再生成内容 | Servlet、JSP、接口返回数据 |
Servlet 的核心价值就在于:它让服务器能“根据请求动态做事”,而不只是返回文件。
1.4 Servlet 与后续课程的关系
在 Java Web 学习路径中,Servlet 不是孤立的一章,而是后续内容的基础:
HTTP -> Tomcat -> Servlet -> Request/Response -> 会话技术 -> Filter/Listener -> MVC你后面学到的请求参数、响应输出、Cookie、Session、过滤器,最终都要落回 Servlet 所在的运行环境中。
本章小结
本章核心概念:Servlet 是运行在 Web 容器中的 Java 类,用来处理 HTTP 请求并生成动态响应。
你现在应该掌握:
- Servlet 不是独立程序,而是容器托管组件
- Tomcat 负责接收请求并把请求分发给 Servlet
- Servlet 是后续 Java Web 各章节的基础入口
3. Web 应用目录与运行方式
2.1 标准 Web 应用目录结构
一个典型 Web 应用的标准结构如下:
WebRoot/
├── html、css、js、images 等静态资源
├── META-INF/
└── WEB-INF/
├── classes/
├── lib/
└── web.xml其中最重要的是 WEB-INF/:
- 浏览器不能直接访问
WEB-INF下的内容 - 编译后的
.class文件通常放在WEB-INF/classes - 依赖 jar 包通常放在
WEB-INF/lib web.xml是传统 Web 应用的配置文件
下图可以帮助你直观看懂:哪些资源位于 Web 根路径下可以直接访问,哪些资源进入 WEB-INF 后会受到保护。

2.2 Maven Web 工程目录结构
实际开发中,我们更常见的是 Maven Web 工程:
project/
├── pom.xml
└── src/
├── main/
│ ├── java/
│ ├── resources/
│ └── webapp/
│ ├── WEB-INF/
│ └── 静态资源
└── test/它与标准 Web 结构的对应关系如下:
| Maven 目录 | 部署后位置 | 说明 |
|---|---|---|
src/main/java |
WEB-INF/classes |
Java 源码编译后输出到这里 |
src/main/resources |
WEB-INF/classes |
配置文件等资源会被复制到这里 |
src/main/webapp |
Web 根目录 | HTML、CSS、JS、图片等会直接发布 |
pom.xml 中依赖 |
WEB-INF/lib |
依赖 jar 会被放到应用运行时目录 |
如果你总觉得 “Maven 目录”和“最终部署后的 Web 目录”对不上,可以结合下面这张图一起记:

2.3 IDEA 中部署 Web 项目时发生了什么
IDEA 配合 Tomcat 运行 Web 项目时,本质上做了三件事:
- 编译 Java 源码
- 按 Web 应用结构组装输出目录
- 把输出目录映射给 Tomcat 访问
常见配置步骤如下:
Run -> Edit Configurations- 添加
Tomcat Server -> Local - 配置
Tomcat Home - 在
Deployment中添加war exploded - 设置
Application Context
其中第 4 步特别容易选错。对 Maven Web 项目来说,通常应该选择 war exploded,这样 Tomcat 访问到的是“展开后的可运行目录结构”,更符合我们调试 Servlet、静态资源和 WEB-INF 的课堂场景。

如果你继续追问 “IDEA 到底把哪些内容放进了这个可部署结构里”,可以看下面这张图。它对应的正是我们前面说的那三部分:
src/main/java和src/main/resources编译后进入WEB-INF/classes- 依赖进入
WEB-INF/lib src/main/webapp作为 Web 资源目录进入最终输出

其中 Application Context 决定访问路径前缀。
例如:
Application Context = /demo那么访问地址通常就是:
http://localhost:8080/demo/hello这里的 URL 并不是“随便写的字符串”,它和 Tomcat 中的应用路径、资源路径是对应起来的。先看下面这张图,建立对 docBase、上下文路径和资源路径关系的整体印象:

如果把视角再放回到 Tomcat 服务器本身,可以进一步理解:
webapps下的目录天然会形成可访问应用ROOT对应访问/- 其他目录名通常对应自己的上下文路径
- 通过
conf/Catalina/localhost/*.xml配置docBase时,本质上是在把某个访问路径映射到指定磁盘目录

2.4 目录问题为什么会导致 404
Servlet 相关的很多 404,本质上都不是“Servlet 写错了”,而是部署结构不对。
常见错误包括:
- 静态资源放错目录,导致浏览器找不到
Application Context配错,访问地址少了或多了前缀- 类没有编译进
WEB-INF/classes - 依赖没打进去,导致启动失败后你误以为是 404
所以学习 Servlet 时,目录结构和部署链路必须一起掌握。
本章小结
本章核心概念:Servlet 运行是否正常,不只取决于代码,还取决于项目目录结构与部署结果是否正确。
你现在应该掌握:
WEB-INF是受保护目录,浏览器不能直接访问- Maven Web 工程和标准 Web 目录之间是一一映射关系
- IDEA 运行 Web 项目,本质是在帮你完成编译、组装和部署映射
4. 第一个 Servlet:从依赖到请求分发
3.1 引入 Servlet API
在 Maven 中,Servlet API 通常这样配置:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>这里 scope 必须是 provided,原因是:
- 编译时需要这个依赖
- 运行时 Tomcat 已经自带 Servlet API(lib目录下的servlet.jar)
- 如果再把它打进项目里,容易发生版本冲突
3.2 三种开发方式
Servlet 的开发方式主要有三种:
| 方式 | 说明 | 是否推荐 |
|---|---|---|
实现 Servlet 接口 |
最底层,方法最多 | 不推荐作为入门写法 |
继承 GenericServlet |
屏蔽部分模板代码 | 一般了解即可 |
继承 HttpServlet |
适合 HTTP 请求处理 | 推荐 |
在 Web 开发里,绝大多数情况下都应该直接继承 HttpServlet。
3.3 最小可运行示例
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;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().write("Hello Servlet");
}
}访问:
http://localhost:8080/应用上下文/hello如果页面能看到 Hello Servlet,说明整个链路已经通了。
3.4 service()、doGet()、doPost() 的关系
初学 Servlet 时,一个最关键的问题是:
浏览器发请求之后,到底是哪个方法被调用?
答案是:先到 service(),再由 service() 分发到 doGet()、doPost() 等方法。
可以用简化后的伪代码理解:
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod();
if ("GET".equals(method)) {
doGet(req, resp);
} else if ("POST".equals(method)) {
doPost(req, resp);
} else if ("PUT".equals(method)) {
doPut(req, resp);
} else if ("DELETE".equals(method)) {
doDelete(req, resp);
}
}所以我们平时重写 doGet()、doPost(),其实是在参与 HttpServlet 已经帮我们搭好的请求分发流程。
3.5 GET 与 POST 的基本分工
| 方法 | 常见用途 | 特点 |
|---|---|---|
| GET | 查询、访问页面 | 参数通常跟在 URL 后 |
| POST | 提交表单、提交数据 | 参数通常在请求体中 |
一个常见写法是:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}这表示“先统一到同一套处理逻辑”。 但要注意:只有当 GET 和 POST 的业务语义确实一致时,才适合这样写。
本章小结
本章核心概念:开发 Servlet 的推荐入口是 HttpServlet,而请求真正的分发核心在 service()。
你现在应该掌握:
- 为什么
servlet-api要用provided - 为什么入门开发几乎总是继承
HttpServlet - GET 请求最终会走到
doGet(),POST 请求最终会走到doPost()
5. @WebServlet 与 URL 映射规则
4.1 @WebServlet 常用属性
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebServlet {
String name() default "";
String[] value() default {};
String[] urlPatterns() default {};
int loadOnStartup() default -1;
WebInitParam[] initParams() default {};
}最常用的几个属性是:
| 属性 | 作用 |
|---|---|
value |
指定 URL 映射路径 |
urlPatterns |
与 value 等价 |
loadOnStartup |
指定启动时是否提前加载 |
initParams |
指定 Servlet 的初始化参数 |
4.2 value 和 urlPatterns
最常见写法:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {}当只写一个 value 属性时,可以省略 value =。
以下写法也成立:
@WebServlet(value = "/hello", loadOnStartup = 1)
public class HelloServlet extends HttpServlet {}多个路径映射到同一个 Servlet 也可以:
@WebServlet({"/hello", "/hi", "/greeting"})
public class HelloServlet extends HttpServlet {}4.3 URL-pattern 四种常见形式
| 类型 | 示例 | 说明 |
|---|---|---|
| 精确匹配 | /hello |
只匹配指定路径 |
| 路径匹配 | /user/* |
匹配这一前缀下的路径 |
| 扩展名匹配 | *.do |
匹配指定扩展名 |
| 缺省匹配 | / |
匹配没有被其他规则处理的请求 |
4.4 匹配优先级
匹配优先级遵循:
精确匹配 > 路径匹配 > 扩展名匹配 > 缺省匹配例如:
@WebServlet("/user/login") // A
@WebServlet("/user/*") // B
@WebServlet("*.do") // C
@WebServlet("/") // D| 请求 URL | 实际命中 |
|---|---|
/user/login |
A |
/user/profile |
B |
/order/list.do |
C |
/other/path |
D |
4.5 缺省 Servlet 与静态资源
Tomcat 内部有一个默认 Servlet,专门处理静态资源。
这意味着:
- 浏览器访问图片、CSS、JS 等静态资源时,通常不是你自己写的 Servlet 在处理
- 如果你自己写了
@WebServlet("/"),就可能把默认静态资源处理逻辑“截胡”
示例:
@WebServlet("/")
public class MyDefaultServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<h1>自定义缺省处理</h1>");
}
}这类写法在框架内部有它的用途,但对于入门阶段来说,要先知道风险:
- 静态资源可能访问不到
- 所有未命中的请求都会落到这个 Servlet
本章小结
本章核心概念:Servlet 能否被访问到,本质取决于 URL 映射规则,而不是“类写没写出来”。
你现在应该掌握:
@WebServlet最常用的是value、loadOnStartup、initParams- URL 匹配存在固定优先级,不是“谁先写谁生效”
@WebServlet("/")有特殊含义,使用前必须知道它会影响静态资源处理
6. Servlet 生命周期与单例多线程模型
5.1 生命周期总览
Servlet 的生命周期由容器管理,不由我们手动控制。
可以用这条链路理解:
加载类 -> 创建实例 -> init() -> service() 多次调用 -> destroy()5.2 各个方法的执行时机
| 方法 | 执行次数 | 谁调用 | 典型用途 |
|---|---|---|---|
| 构造方法 | 1 次 | 容器 | 创建对象 |
init() |
1 次 | 容器 | 初始化资源 |
service() |
多次 | 容器 | 分发请求 |
destroy() |
1 次 | 容器 | 释放资源 |
理解重点:
init()不是每次请求都执行destroy()也不是每次关闭浏览器都执行- 只有请求处理阶段,才会反复进入
service()
5.3 生命周期观察示例
@WebServlet(value = "/life", loadOnStartup = 1)
public class LifeServlet extends HttpServlet {
public LifeServlet() {
System.out.println("1. 构造方法执行");
}
@Override
public void init() throws ServletException {
System.out.println("2. init() 执行");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
System.out.println("3. doGet() 执行");
resp.getWriter().write("ok");
}
@Override
public void destroy() {
System.out.println("4. destroy() 执行");
}
}你应该在日志里观察到:
- Tomcat 启动时,若配置了
loadOnStartup,会先执行构造和init() - 每访问一次
/life,会执行一次doGet() - 正常关闭容器时,才会执行
destroy()
5.4 loadOnStartup 的意义
loadOnStartup 默认是 -1,表示第一次访问时再初始化。
@WebServlet(value = "/demo", loadOnStartup = 1)
public class DemoServlet extends HttpServlet {}当值大于等于 0 时:
- Tomcat 启动阶段就会初始化这个 Servlet
- 数字越小,优先级通常越高
适用场景:
- 需要预加载配置
- 需要启动时初始化缓存
- 希望第一次访问时响应更快
5.5 为什么说 Servlet 默认是“单例多线程”
容器通常只创建一个 Servlet 实例,但会用多个线程处理不同请求。
这就意味着:
- 局部变量一般是线程安全的
- 成员变量可能被多个请求共享
- 把“请求相关状态”放进成员变量是很危险的
错误示例:
@WebServlet("/counter")
public class CounterServlet extends HttpServlet {
private int count = 0;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
count++;
resp.getWriter().write("count = " + count);
}
}问题在于:多个请求同时进来时,count++ 不是线程安全操作。
更安全的写法:
import java.util.concurrent.atomic.AtomicInteger;
@WebServlet("/counter")
public class CounterServlet extends HttpServlet {
private final AtomicInteger count = new AtomicInteger(0);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
int current = count.incrementAndGet();
resp.getWriter().write("count = " + current);
}
}本章小结
本章核心概念:生命周期由容器掌控,而单例多线程模型决定了 Servlet 编码时必须注意共享状态。
你现在应该掌握:
init()、service()、destroy()的调用时机loadOnStartup会影响初始化时机- 不要把请求级数据随手放到 Servlet 成员变量里
7. ServletConfig 与 ServletContext
6.1 两者的职责差异
这是初学 Servlet 时最容易混淆的一组对象。
| 对象 | 作用域 | 典型用途 |
|---|---|---|
ServletConfig |
单个 Servlet | 读取当前 Servlet 的初始化参数 |
ServletContext |
整个 Web 应用 | 共享全局数据、读取应用级信息 |
记忆方式:
Config更偏“我这个 Servlet 的配置”Context更偏“整个应用的上下文”
6.2 ServletConfig 示例
@WebServlet(
value = "/config",
initParams = {
@WebInitParam(name = "username", value = "root"),
@WebInitParam(name = "password", value = "123456")
}
)
public class ConfigServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
ServletConfig config = getServletConfig();
String username = config.getInitParameter("username");
String password = config.getInitParameter("password");
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().write(username + " / " + password);
}
}这一组参数只服务于当前这个 Servlet,不会自动共享给其他 Servlet。
6.3 ServletContext 示例
@WebServlet("/put")
public class PutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
ServletContext context = getServletContext();
context.setAttribute("appName", "ServletDemo");
}
}
@WebServlet("/get")
public class GetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
ServletContext context = getServletContext();
Object appName = context.getAttribute("appName");
resp.getWriter().write(String.valueOf(appName));
}
}这里两个 Servlet 访问到的是同一个 ServletContext。
6.4 获取 ServletContext 的常见方式
ServletContext context1 = getServletContext();
ServletContext context2 = getServletConfig().getServletContext();
ServletContext context3 = req.getServletContext();
ServletContext context4 = req.getSession().getServletContext();入门阶段推荐直接记住这一种:
ServletContext context = getServletContext();6.5 getRealPath() 要知道但不要滥用
String rootPath = getServletContext().getRealPath("/");它可以拿到 Web 应用部署后的真实路径,但你要知道两个风险:
- 部署方式变化后,真实路径可能变化
- 某些打包或云部署环境下,真实路径不一定稳定
因此:
- 本地教学、演示时可以用来帮助理解目录映射
- 真正读取配置文件时,优先考虑
classpath或流方式,而不是死依赖真实磁盘路径
本章小结
本章核心概念:ServletConfig 管“当前 Servlet 自己的配置”,ServletContext 管“整个应用共享的上下文”。
你现在应该掌握:
- 什么场景下读初始化参数该用
ServletConfig - 什么场景下共享数据该用
ServletContext getRealPath()能帮助理解部署路径,但不该变成唯一依赖手段
8. XML 配置、JSP 关系与高频问题
7.1 web.xml 配置 Servlet
虽然现代项目更多使用注解,但 web.xml 仍值得了解,因为:
- 老项目仍然常见
- 它能帮助你理解 Servlet 注册的本质
- 有些配置在 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-name>helloServlet</servlet-name>
<servlet-class>com.demo.HelloServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>7.2 注解配置与 XML 配置怎么选
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 注解配置 | 直观、贴近代码 | 配置分散在类上 | 新项目、教学示例 |
| XML 配置 | 集中、易统一查看 | 不够直观 | 老项目、集中治理 |
需要特别记住的一点是:
当注解配置和 XML 配置同时存在时,通常以 XML 为准。
7.3 JSP 与 Servlet 的关系
JSP 本质上并不是“完全不同的技术”,它最终还是会被转换成 Servlet。
可以用这条链路理解:
hello.jsp -> 编译成 Servlet 类 -> 再由容器执行 -> 输出 HTML所以你可以把 JSP 看成“更偏页面模板表达的 Servlet 形态”。
在现代开发里,JSP 已不再是主流重点,但理解它和 Servlet 的关系,有助于你看懂老项目。
7.4 高频问题排查表
404 类问题
| 现象 | 可能原因 | 优先检查 |
|---|---|---|
| 访问 Servlet 报 404 | URL 写错 | @WebServlet 路径、上下文路径 |
| 静态资源 404 | 资源目录放错 | 是否放在 webapp,是否被 / 映射截胡 |
| 项目根路径 404 | 部署失败 | IDEA Deployment、Tomcat 日志 |
500 类问题
| 现象 | 常见原因 | 优先检查 |
|---|---|---|
NullPointerException |
参数为空、对象未初始化 | 对象来源与执行顺序 |
ClassNotFoundException |
依赖缺失 | pom.xml、构建结果 |
IllegalStateException |
响应已提交后继续操作 | 输出顺序、转发逻辑 |
编码问题
请求乱码:
req.setCharacterEncoding("UTF-8");响应乱码:
resp.setContentType("text/html;charset=UTF-8");依赖冲突问题
如果把 servlet-api 错误地打进项目里,常见症状包括:
NoSuchMethodErrorClassCastException- 容器启动异常
解决方向只有一句话:
确保
servlet-api的作用域是provided。
本章小结
本章核心概念:注解和 XML 都是在“注册 Servlet”,而常见报错大多都能回到映射、部署、编码和依赖这四条主线。
你现在应该掌握:
web.xml注册 Servlet 的基本形式- JSP 最终仍会落回 Servlet 机制
- 404、500、乱码、依赖冲突应该从哪些方向排查
9. 本章总复盘
8.1 一条完整主线
可以把本章内容压缩成下面这条主线:
浏览器发请求
->
Tomcat 接收并解析路径
->
根据 URL 映射找到 Servlet
->
Servlet 进入生命周期与请求分发流程
->
读取参数、处理业务、写出响应
->
必要时通过 Config/Context 读取配置或共享数据8.2 你现在最该记住的五件事
- Servlet 是运行在容器中的 Java 类,不是独立启动程序
- 开发入口通常是继承
HttpServlet service()负责按 HTTP 方法分发到doGet()、doPost()- Servlet 默认是单例多线程模型,成员变量必须谨慎
ServletConfig管单个 Servlet,ServletContext管整个应用
8.3 学完这一章后,下一章会自然衔接什么
学完 Servlet 后,你最自然的下一步就是:
- 更细致地处理请求参数和响应输出
- 学会转发、重定向、响应头设置
- 理解 Request 与 Response 在开发中的更多用法
这正是下一章 Request & Response 要继续展开的内容。
10. 实战练习
<!-- 实战练习内容已分离到 practices/markdown/05-servlet-practice.md -->
建议先回看本章重点:URL 映射、生命周期、
HttpServlet开发方式、ServletContext共享数据,再进入配套练习。 练习顺序建议按照:基础 Servlet -> 生命周期观察 -> 访问统计 -> 登录案例。 配套练习文件:content/optimized/practices/markdown/05-servlet-practice.md