JavaWeb的三大组件
Servlet是JavaWeb三大组件之一,它是我们学习JavaWeb最为基本的组件,也就是说你一定要100%的掌握它。
其它两种:Filter(过滤器)、Listener(观察者模式),后续讲解
Servlet作用
Servlet 是运行在 Web 服务器或应用服务器上的 Java 程序,它充当了来自 HTTP 客户端的请求与服务器上数据库或应用程序之间的中间层。
当客户端发出请求时,Web 容器(如 Tomcat)会解析请求的 URL,并根据配置映射(Annotation 或 web.xml)找到对应的 Servlet。Servlet 的核心作用是接收并处理这些请求,执行业务逻辑,并生成动态响应。
初识Servlet
Servlet 核心定义与接口规范
Servlet 是运行在 Web 服务器中的 Java 小程序,主要通过 HTTP 协议接收和响应客户端请求。编写一个 Servlet 必须实现 javax.servlet.Servlet 接口,并将其部署到 Web 容器中。
注意: 现代开发(Servlet 3.0+)通常使用 @WebServlet 注解进行部署,只有在老旧项目或特殊配置时才强制使用 web.xml。
Servlet 接口方法详解
| 方法名称 | 返回类型 | 参数 | 执行频率 | 核心考点(面试必背) |
|---|---|---|---|---|
init |
void |
ServletConfig config |
仅一次 | 初始化阶段:在实例创建后立即调用,用于加载资源(如数据库连接池)。 |
service |
void |
ServletReq, ServletRes |
每次请求 | 运行阶段:处理业务逻辑的核心入口。容器为每个请求开启新线程调用此方法。 |
destroy |
void |
无 | 仅一次 | 销毁阶段:在实例被移除前调用(如卸载应用或关机),用于释放资源。 |
getServletConfig |
ServletConfig |
无 | 按需调用 | 获取配置 :返回包含初始化参数的 ServletConfig 对象。 |
getServletInfo |
String |
无 | 极少调用 | 获取描述:返回作者、版本等描述信息。实际开发中基本不用。 |
创建实操
第一步:实现Servlet的接口,实现接口中的方法;
java
package com.example;
import javax.servlet.*;
import java.io.IOException;
/**
* 【方法注释:类级别】
* 名称:MyServlet
* 角色:JavaWeb 开发的核心组件------Servlet。
* 核心逻辑:实现 javax.servlet.Servlet 接口。这是 Servlet 的最底层标准。
* 面试必记:Servlet 必须由 Servlet 容器(如 Tomcat)来管理,开发者不手动 new 对象。
*/
public class MyServlet implements Servlet {
// 【行内注释】定义一个成员变量,用于在 init 方法和 getServletConfig 方法之间传递配置对象
private ServletConfig config;
/**
* 【生命周期方法 1:初始化】
* 执行时机:Servlet 实例被创建后立即执行。
* 执行次数:整个生命周期内【仅执行一次】。
* 面试点:init 接收一个 ServletConfig 对象,它代表了 web.xml 中为该 Servlet 配置的初始化参数。
*/
@Override
public void init(ServletConfig servletConfig) throws ServletException {
// 【行内注释】将容器传入的配置对象保存起来,否则 getServletConfig 方法将返回 null
this.config = servletConfig;
System.out.println("init: 执行初始化配置。");
// 获取配置参数
String sname = config.getServletName();
String hello1 = config.getInitParameter("hellomyservlet");
String hello2 = config.getInitParameter("hello");
System.out.println("init: 获取初始化参数:"+
"sname:" + sname +
"hello1:" + hello1 +
"hello2:" + hello2
);
}
/**
* 【配置获取方法】
* 作用:返回当前 Servlet 的专属配置对象 ServletConfig。
* 面试点:如果你在自定义 Servlet 中需要获取 web.xml 里的 <init-param>,就靠它。
*/
@Override
public ServletConfig getServletConfig() {
return this.config;
}
/**
* 【生命周期方法 2:服务处理】
* 执行时机:每次客户端发送请求(GET/POST 等)时,Tomcat 都会调用此方法。
* 执行次数:【多次执行】,请求多少次就跑多少次。
* 面试点:该方法是在多线程环境下运行的,必须注意线程安全!
*/
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse)
throws ServletException, IOException {
// 【行内注释】这里是业务逻辑的核心。ServletRequest 负责拿数据,ServletResponse 负责给数据。
System.out.println("service: 正在处理业务逻辑...");
}
/**
* 【信息获取方法】
* 作用:返回 Servlet 的描述性信息(如作者、版本)。
* 面试点:实际开发几乎不用,返回空字符串或 null 即可,面试时提到"元数据描述"即可。
*/
@Override
public String getServletInfo() {
return "MyServlet v1.0 - Author: Explainer";
}
/**
* 【生命周期方法 3:销毁】
* 执行时机:Web 应用被卸载、服务器关闭或重启时。
* 执行次数:整个生命周期内【仅执行一次】。
* 作用:在员工离职(对象回收)前处理收尾工作,如关闭数据库、保存缓存数据。
*/
@Override
public void destroy() {
System.out.println("destroy: delete something in this function");
}
}
/*
*
* 1. 【关于单例与多线程(高频考点)】
* - 现象:Servlet 是单例的(Singleton)。Tomcat 默认只为每个 Servlet 类创建一个对象。
* - 风险:当 1000 个用户并发访问时,1000 个线程会同时进入同一个 service 方法。
* - 准则:不要在 Servlet 类中定义"有状态"的成员变量(即会被修改的变量)。
* 如果你在类中定义了 int count = 0; 并在 service 里 count++,结果一定会乱。
*
* 2. 【Servlet 的继承体系】
* - Servlet 接口:最底层,定义了 5 个方法(即本类实现的方式)。
* - GenericServlet 抽象类:实现了 Servlet 接口,并把除了 service 以外的方法都写好了。
* - HttpServlet 抽象类(重点):继承自 GenericServlet。它根据 HTTP 协议进行了增强。
* 它在 service 方法里判断了请求方式(GET/POST/PUT...),然后分发给相应的 doXXX 方法。
* 实际开发中,我们 100% 都是继承 HttpServlet 并重写 doGet/doPost。
*
* 3. 【load-on-startup 配置的影响】
* - 默认:Servlet 是在"第一次被访问"时才初始化的(懒加载)。
* - 配置:在 web.xml 或注解中设置 <load-on-startup>。
* - 效果:若值为 0 或正整数,容器会在"服务器启动时"就完成 init。数值越小,优先级越高。
*
* 4. 【路径映射匹配规则(优先级)】
* - 精确匹配:/abc (最高)
* - 路径匹配:/abc/*
* - 扩展名匹配:*.do (最低)
* - 注意:路径匹配和扩展名匹配不能同时使用,如 /abc/*.do 是非法的。
*
* 5. 【ServletContext vs ServletConfig】
* - ServletConfig:每个 Servlet 独有的(私人笔记),只能看自己的配置。
* - ServletContext:整个 Web 应用共享的(公共大屏幕),所有 Servlet 都能看。
* 常用功能:获取全局参数、作为域对象存取共享数据、获取资源的真实物理路径。
*
* ==============================================================================
*/
配置servlet的访问路径
在web/WEB-INF/web.xml目录下添加以下内容
xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 【方法注释:根标签】
web-app 是 Web 应用的根配置。
version="4.0" 代表支持 Servlet 4.0 规范,这意味着你可以混用 XML 和注解。 -->
<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">
<!-- 1. 【全局配置:ServletContext 参数】
执行时机:服务器启动时加载。
作用范围:整个 Web 应用,所有 Servlet 都能访问(公共大仓库)。 -->
<context-param>
<param-name>globalConfig</param-name>
<param-value>shared-data-for-all</param-value>
</context-param>
<!-- 第一个 Servlet: MyServlet -->
<servlet>
<servlet-name>hello1</servlet-name>
<servlet-class>com.example.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello1</servlet-name>
<url-pattern>/hello1</url-pattern>
</servlet-mapping>
</web-app>
JavaWeb请求响应流程

Tomcat 处理 Servlet 请求流程图
| 步骤 | 动作类型 | 具体操作 |
|---|---|---|
| 1. 匹配路径 | 解析 URL | Tomcat 截取请求路径 /logon,在 web.xml 中寻找对应的 <url-pattern>。 |
| 2. 映射名称 | 查找别名 | 根据匹配到的 pattern 找到关联的 <servlet-name>(如 login)。 |
| 3. 定位类名 | 获取全限定名 | 根据 servlet-name 找到对应的 <servlet-class>(如 com.rl.servlet.LoginServlet)。 |
| 4. 实例检查 | 内存检索 | Tomcat 查看 Servlet 容器(池) 中是否已存在该类的实例。 |
| 5. 实例化/初始化 | 反射创建 | 若不存在 :通过 Class.forName().newInstance() 反射创建对象,并立即调用 init() 方法。 |
| 6. 执行业务 | 多线程调用 | 从线程池分配一个线程,调用该实例的 service(request, response) 方法。 |

Servlet 生命周期核心机制
| 生命周期阶段 | 对应方法 | 触发频率 | 执行触发者 | 核心意义 |
|---|---|---|---|---|
| 实例化与初始化 | init(ServletConfig) |
仅一次 | Web 容器 (Tomcat) | 实例创建后立即执行,用于准备资源(如 DB 连接、配置读取)。 |
| 请求处理 | service(Req, Res) |
每次请求 | Web 容器 (Tomcat) | 容器分配新线程执行。负责根据 HTTP 方法分发业务逻辑。 |
| 实例销毁 | destroy() |
仅一次 | Web 容器 (Tomcat) | 实例移除前执行,用于释放资源(如关闭流、清理缓存)。 |
HttpServlet(精通)
HttpServlet介绍
因为现在我们的请求都是基于HTTP协议的,所以我们应该专门为HTTP请求写一个Servlet做为通用父类。

| 类/接口名称 | 类型 | 核心作用与特点 |
|---|---|---|
Servlet |
接口 | 顶层规范:定义了所有 Servlet 必须实现的五个生命周期方法(init, service, destroy 等)。 |
GenericServlet |
抽象类 | 协议无关性 :实现了 Servlet 接口,并对 init 和 destroy 做了基本实现。它不绑定任何协议(如 HTTP、FTP),通过 service 方法处理通用请求。 |
HttpServlet |
抽象类 | HTTP 协议定制 :继承自 GenericServlet,专门针对 HTTP 协议进行了优化。它重写了 service 方法,根据请求方式分发到 doGet, doPost 等方法。 |
创建HttpServlet
上文我们创建了MyServlet实现了Servlet接口,现在要学习的HttpServlet 是对 Servlet 接口的 HTTP 专用封装;
它的 service() 方法已经帮你完成了"根据 HTTP 方法分发到 doGet/doPost"的工作;
你只需要重写 doGet、doPost 等方法,无需碰 service()(除非有特殊需求,比如统一日志、权限检查等,这时可以重写 service() 但通常建议用 Filter。
开发一个标准的 Servlet 非常简单,通常我们只需要继承 HttpServlet 即可。
创建
Java
package cn.tx.servlet;
import javax.servlet.http.HttpServlet;
public class Servlet2 extends HttpServlet {
// 继承 HttpServlet 后,通常重写 doGet 或 doPost 方法来处理具体业务
}
在 web.xml 中配置映射
为了让 Tomcat 容器能够找到并路由到你的 Servlet,需要在 web.xml 中进行注册和路径映射:
xml
<servlet>
<servlet-name>hello2</servlet-name>
<servlet-class>cn.tx.servlet.Servlet2</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello2</servlet-name>
<url-pattern>/hello2</url-pattern>
</servlet-mapping>
控制 Servlet 的创建时机:load-on-startup
默认情况下,Servlet 实例是在客户端第一次访问时才被创建的(懒加载)。但有些 Servlet 承担着框架初始化或加载全局配置的重任,我们希望它们在 Tomcat 启动时就立刻完成初始化。
此时,可以使用 标签:
xml
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>cn.tx.servlet.Servlet1</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
工作原理:当标签值为 0 或正整数时,Web 容器会在应用启动时就实例化该 Servlet 并调用其 init() 方法。
加载顺序:该元素的值代表了一个优先级序号,数字越小,优先级越高。Tomcat 会按照这个序号从小到大依次创建 Servlet 实例。
ServletConfig:读取初始化参数
ServletConfig 对象是对 web.xml 中 标签配置信息的封装。它常用于获取当前 Servlet 的专属配置参数,比如特定的文件路径或安全凭证。
配置初始化参数
我们在 标签内,通过 定义参数(注意:不是配置在 中):
xml
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>cn.tx.servlet.Servlet1</servlet-class>
<init-param>
<param-name>username</param-name>
<param-value>root</param-value>
</init-param>
<init-param>
<param-name>password</param-name>
<param-value>txjava</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
Tomcat 会在调用 Servlet 的 init() 方法时,自动传入 ServletConfig 对象。在代码中可以通过以下方式读取:
java
// 获取单个参数
String username = getServletConfig().getInitParameter("username");
System.out.println("username: " + username);
// 遍历获取所有参数
Enumeration<String> parameterNames = getServletConfig().getInitParameterNames();
while (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
String paramValue = getServletConfig().getInitParameter(paramName);
System.out.println(paramName + " : " + paramValue);
}
Servlet URL 路径映射规则
理解 url-pattern 的匹配规则是解决很多 404 错误和路由冲突的关键。
匹配规则与优先级
- 完全路径匹配:以 / 开头。例如 /aaa, /aaa/bbb
- 目录匹配:以 / 开头,并以 /* 结尾。例如 /abc/, /
- 扩展名匹配:不能以 / 开头,直接以 *. 开始。例如 *.do, *.action
⚠️ 经典错误排查: 绝对不要写成 /*.do,这违反了 Servlet 规范,容器会报错或忽略。
优先级规则:
完全匹配 > 目录匹配 > 扩展名匹配。 (如果同为目录匹配,匹配范围越精确、路径越长的优先级越高)。
相对与绝对路径
在 Web 开发中寻找资源,底层本质上都依赖绝对路径。所谓的相对路径,只是 API 在底层帮助我们通过当前位置换算出了绝对路径。
相对路径
通过 .(当前目录)和 ...(上一级目录)来寻找目标资源。
缺点:过于依赖当前资源的位置。如果当前资源路径发生变化,相对路径的代码就会失效,维护成本较高。
绝对路径
绝对路径是指从根节点出发,直接精确定位到目标位置。在 Web 开发中,绝对路径通常以 / 开始。
关于 / 的含义,需要严格区分是客户端路径还是服务端路径:
客户端访问路径(交由浏览器解析):
这里的 / 代表的是整个服务器的主机根目录(如 http://localhost:8080/)。
因此,跨站访问或前端页面请求资源时,必须带上项目名,如 /day5/hello。
服务器内部路径(交由 Web 容器解析):
这里的 / 代表的是当前 Web 应用的根目录。
多用于服务器内部的请求转发(Forward)或页面包含。例如:request.getRequestDispatcher("/hello").forward(request, response);。
ServletContext
ServletContext是一个全局的储存信息的空间,服务器开始,其就存在,服务器关闭,其才释放。request,一个用户可有多个;session,一个用户一个;而servletContext,所有用户共用一个。所以,为了节省空间,提高效率,ServletContext中,要放必须的、重要的、所有用户需要共享的线程又是安全的一些信息 。

核心 API 概览
| 方法声明 | 核心功能说明 |
|---|---|
setAttribute(String name, Object object) |
将对象绑定到 Context 域中,实现全局共享。 |
getAttribute(String name) |
根据名称从 Context 域中获取绑定的对象。 |
getContextPath() |
获取当前 Web 应用的上下文路径(即虚拟目录名)。 |
getInitParameter(String name) |
获取 Web 应用级别的全局初始化参数。 |
getRealPath(String path) |
将 Web 应用内的虚拟路径转换为服务器磁盘上的绝对物理路径。 |
getResourceAsStream(String path) |
以输入流的形式读取 Web 应用内的资源文件。 |
经典实战场景
场景一:读取全局初始化参数
有时候,我们需要为整个项目配置一些通用信息(如公司名称、全局编码格式等),这可以通过 web.xml 的 来实现,而不是配在单个 Servlet 中。
xml
<context-param>
<param-name>company</param-name>
<param-value>qcby</param-value>
</context-param>
代码中获取:
java
// 从任意 Servlet 获取全局配置
ServletContext servletContext = getServletConfig().getServletContext();
String company = servletContext.getInitParameter("company");
System.out.println("全局配置的公司名称: " + company);
场景二:全站数据共享(以统计站点总访问量为例)
利用 Context 域对象的全局生命周期,我们可以轻松实现一个全站级别的计数器。只要 Tomcat 不重启,这个数据就会一直累加。
java
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1. 获取全局 Context 对象
ServletContext servletContext = getServletContext();
// 2. 尝试从域中获取当前访问次数
Integer visitNums = (Integer) servletContext.getAttribute("visitNums");
// 3. 如果是第一次访问,初始化为0
visitNums = (visitNums == null) ? 0 : visitNums;
// 4. 访问量加1,并重新存入域中
servletContext.setAttribute("visitNums", ++visitNums);
// 5. 响应给客户端
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("当前网站历史总访问人次: " + visitNums);
}
场景三:读取资源文件(配置文件加载)
方式 1:通过 getRealPath() 获取绝对路径(传统方式)
这种方式通过相对路径计算出服务器磁盘上的真实物理路径,然后再通过标准的 FileInputStream 去读取。
注意:getRealPath 的参数内容不会被提前校验,只有在真正触发 IO 操作时才知道路径是否正确。
java
ServletContext servletContext = getServletContext();
// 获取 web 目录下文件的绝对物理路径
String path = servletContext.getRealPath("tx.properties");
System.out.println("文件真实路径: " + path);
try (InputStream is = new FileInputStream(path)) {
Properties p = new Properties();
p.load(is);
System.out.println(p.getProperty("username"));
} catch (Exception e) {
e.printStackTrace();
}
方式 2:通过 getResourceAsStream() 直接获取流(Web 专属)
直接一步到位,将 Web 目录下的文件读取为输入流。这里的 / 代表当前 Web 项目的根目录。
java
// 直接读取 WEB-INF 下的资源文件
InputStream is = getServletContext().getResourceAsStream("/WEB-INF/tx1.properties");
方式 3:通过类加载器 ClassLoader 读取
这是实际开发中最优雅的方式。它不依赖于 Web 容器的 ServletContext 对象,只要资源文件放在 classpath(如 Maven 项目的 resources 目录,编译后会放在 classes 下)下,任何普通的 Java 类都可以随时获取。
java
// 好处:解耦,不需要指定 /WEB-INF/classes 等繁琐前缀
InputStream is = this.getClass().getClassLoader().getResourceAsStream("tx2.properties");
面试题回顾
Servlet 是单例的吗?它是线程安全的吗?(重点理解)
Servlet 在 Web 容器(如 Tomcat)中默认是单例多线程的。
-
关于单例机制:这种单例并非通过传统的 Java 单例模式(如私有构造函数)硬编码实现的,而是由 Tomcat 容器托管保证的。容器内部维护了一个实例集合,对于同一个映射路径的 Servlet 类,只会在第一次访问(或容器启动)时反射创建一个实例。
-
关于线程安全:面对高并发,Tomcat 会从其线程池中为每一个 HTTP 请求分配一个独立的线程,这些线程会并发地调用同一个 Servlet 实例的 service() 方法。因此,Servlet 是线程不安全的。
如何解决 Servlet 的线程安全问题?
-
最佳实践:绝对不要在 Servlet 中定义可修改的成员变量(实例属性)。业务处理逻辑中所需的数据,全部使用局部变量(因为局部变量分配在每个线程私有的栈内存中,天生隔离,绝对安全)。
-
避坑指南:千万不要尝试在 service 或 doGet/doPost 方法上加 synchronized 同步锁。这虽然能解决安全问题,但会将高并发请求强制变成单线程排队执行,导致服务器吞吐量断崖式下跌。
请简述 Servlet 的生命周期
Servlet 的生命周期完全由 Web 容器(Tomcat)进行控制,主要分为以下四个阶段:
-
实例化与初始化:容器通过反射机制创建 Servlet 实例,并立刻调用 init() 方法完成初始化工作。这个阶段在整个生命周期中只执行一次(默认在第一次请求时触发,可通过 提前至容器启动时)。
-
请求处理:每次有客户端请求到达时,容器都会分配一个新线程来调用 service() 方法,并根据具体的 HTTP 方法分发到对应的 doGet 或 doPost 中。这个阶段会频繁执行。
-
实例销毁:当 Web 应用被卸载或 Tomcat 正常关闭前,容器会调用 destroy() 方法,用于释放数据库连接等系统资源。这个阶段只执行一次。
-
垃圾回收:Servlet 实例脱离容器管理后,最终由 JVM 的垃圾回收器(GC)在合适的时机进行内存回收。
end