Servlet在Java Web中的定位
Servlet = Server + Applet,可以理解为“运行在服务器端的小程序”。
更准确地说:
- Servlet 是一个 Java 类
- 它不是独立运行的主程序
- 它必须运行在 Web 容器中,例如 Tomcat
- 它的任务是接收 HTTP 请求、处理业务、返回 HTTP 响应

浏览器 -> HTTP请求 -> Tomcat -> Servlet -> 业务处理 -> HTTP响应 -> 浏览器
Servlet 的核心价值就在于:它让服务器能“根据请求动态做事”,而不只是返回文件。
核心概念:Servlet 是运行在 Web 容器中的 Java 类,用来处理 HTTP 请求并生成动态响应。
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 应用的配置文件
实际开发中,我们更常见的是 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 会被放到应用运行时目录 |

使用war打包之后的java项目结构

开发和编译的路径对应:

IDEA 配合 Tomcat 运行 Web 项目时,本质上做了三件事:
- 编译 Java 源码
- 按 Web 应用结构组装输出目录
- 把输出目录映射给 Tomcat 访问
常见配置步骤如下:
Run -> Edit Configurations- 添加
Tomcat Server -> Local - 配置
Tomcat Home - 在
Deployment中添加war exploded - 设置
Application Context
图形化界面完成虚拟映射

映射文件存放在临时文件夹中,为了避免污染原本的tomcat配置

其中 Application Context 决定访问路径前缀。
例如:
Application Context = /demo
那么访问地址通常就是:
http://localhost:8080/demo/hello
这里的 URL 并不是“随便写的字符串”,它和 Tomcat 中的应用路径、资源路径是对应起来的。

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

Servlet 相关的很多 404,本质上都不是“Servlet 写错了”,而是部署结构不对。
常见错误包括:
- 静态资源放错目录,导致浏览器找不到
Application Context配错,访问地址少了或多了前缀- 类没有编译进
WEB-INF/classes - 依赖没打进去,导致启动失败后误以为是 404
依赖和请求发放
引入 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)
- 如果再把它打进项目里,容易发生版本冲突
Servlet 的三种开发方式
| 方式 | 说明 | 是否推荐 |
|---|---|---|
实现 Servlet 接口 | 最底层,方法最多 | 不推荐作为入门写法 |
继承 GenericServlet | 屏蔽部分模板代码 | 一般了解即可 |
继承 HttpServlet | 适合 HTTP 请求处理 | 推荐 |
在 Web 开发里,绝大多数情况下都应该直接继承 HttpServlet。
示例:
@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,说明整个链路已经通了。
请求分发流程

根据请求报文中的请求行中的请求方法的不同,执行不同的doXXX方法,Servlet的核心开发就是重写父类中的doXXX方法
Servlet被多线程访问,所以需要保证线程安全:

GET 与 POST 的基本分工
| 方法 | 常见用途 | 特点 |
|---|---|---|
| GET | 查询、访问页面 | 参数通常跟在 URL 后 |
| POST | 提交表单、提交数据 | 参数通常在请求体中 |
一个常见写法是:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
这表示“先统一到同一套处理逻辑”。 但要注意:只有当 GET 和 POST 的业务语义确实一致时,才适合这样写。
@WebServlet 与 URL 映射规则
@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 的初始化参数 |
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 {}
URL-pattern 四种常见形式
| 类型 | 示例 | 说明 |
|---|---|---|
| 精确匹配 | /hello | 只匹配指定路径 |
| 路径匹配 | /user/* | 匹配这一前缀下的路径 |
| 扩展名匹配 | *.do | 匹配指定扩展名 |
| 缺省匹配 | / | 匹配没有被其他规则处理的请求 |
匹配优先级遵循:精确匹配 > 路径匹配 > 扩展名匹配 > 缺省匹配
缺省 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 生命周期与单例多线程模型
Servlet 的生命周期由容器管理,不由我们手动控制。可以用这条链路理解:
加载类 -> 创建实例 -> init() -> service() 多次调用 -> destroy()
各个方法的执行时机
| 方法 | 执行次数 | 谁调用 | 典型用途 |
|---|---|---|---|
| 构造方法 | 1 次 | 容器 | 创建对象 |
init() | 1 次 | 容器 | 初始化资源 |
service() | 多次 | 容器 | 分发请求 |
destroy() | 1 次 | 容器 | 释放资源 |
生命周期观察示例
@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()
loadOnStartup 的意义
loadOnStartup 默认是 -1,表示第一次访问时再初始化。
@WebServlet(value = "/demo", loadOnStartup = 1)
public class DemoServlet extends HttpServlet {}
当值大于等于 0 时:
- Tomcat 启动阶段就会初始化这个 Servlet
- 数字越小,优先级通常越高
适用场景:
- 需要预加载配置
- 需要启动时初始化缓存
- 希望第一次访问时响应更快
为什么说 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);
}
}
ServletConfig 与 ServletContext
主要使用Context
| 对象 | 作用域 | 典型用途 |
|---|---|---|
ServletConfig | 单个 Servlet | 读取当前 Servlet 的初始化参数 |
ServletContext | 整个 Web 应用 | 共享全局数据、读取应用级信息 |
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。
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。
获取 ServletContext 的常见方式
ServletContext context1 = getServletContext();
ServletContext context2 = getServletConfig().getServletContext();
ServletContext context3 = req.getServletContext();
ServletContext context4 = req.getSession().getServletContext();
入门阶段推荐直接记住这一种:
ServletContext context = getServletContext();
getRealPath()
String rootPath = getServletContext().getRealPath("/");
它可以拿到 Web 应用部署后的真实路径,但要知道两个风险:
- 部署方式变化后,真实路径可能变化
- 某些打包或云部署环境下,真实路径不一定稳定