一、Servlet 概述
1.1 JavaWeb 的三大组件
JavaWeb 开发中有三大核心组件,它们是构建 Java Web 应用的基石:
组件一:Servlet
作用:处理客户端请求,生成动态响应内容。是 JavaWeb 最基础的组件,必须 100% 掌握。
组件二:Filter
作用:在请求到达 Servlet 之前或响应返回客户端之后,对请求和响应进行预处理和后处理。例如字符编码过滤、登录校验、日志记录等。
组件三:Listener
作用:基于观察者模式,监听 Web 应用中的事件(如 ServletContext 创建、销毁,Session 创建、销毁,属性变化等),在事件发生时执行特定的业务逻辑。
1.2 Servlet 的作用
Servlet,即 Server + Let,意为“服务器端的小程序”。它的核心作用就是处理用户请求。
当客户端(浏览器)发出一个 HTTP 请求时,Tomcat(或其他 Servlet 容器)会根据请求的 URL 找到对应的 Servlet 来处理这个请求。
具体场景举例:
用户发出登录请求 → 由 LoginServlet 处理
用户发出注册请求 → 由 RegisterServlet 处理
用户发出查询商品列表请求 → 由 ProductListServlet 处理
也就是说,Servlet 是 MVC 模式中的 Controller(控制器)层,负责接收请求、调用业务逻辑、返回响应。
1.3 Servlet 与 Tomcat 的关系
很多初学者会混淆 Servlet 和 Tomcat 的关系,这里做一个清晰说明:
Tomcat 是一个 Servlet 容器(也叫 Web 服务器),它的主要职责是:
管理 Servlet 的生命周期(创建、初始化、调用、销毁)
将客户端的 HTTP 请求解析并转发给对应的 Servlet
接收 Servlet 的响应并返回给客户端
而 Servlet 是我们开发者编写的 Java 类,它实现了 javax.servlet.Servlet 接口,用来处理具体的业务逻辑。
Tomcat 负责“管”,Servlet 负责“做”。Tomcat 是舞台,Servlet 是演员。
二、第一个 Servlet
2.1 Servlet 接口详解
所有 Servlet 都必须实现 javax.servlet.Servlet 接口。该接口定义了以下 5 个方法,其中前三个是生命周期方法:
方法一:void init(ServletConfig config)
当 Tomcat 创建 Servlet 实例后,会立即调用 init() 方法。
该方法在整个生命周期中只被调用一次。
用于执行 Servlet 的初始化工作,例如加载配置文件、建立数据库连接等。
方法二:void service(ServletRequest request, ServletResponse response)
每次客户端请求到达时,Tomcat 都会调用 service() 方法。
该方法会被多次调用(每来一次请求就调用一次)。
用于处理请求并生成响应,是 Servlet 的核心业务方法。
方法三:void destroy()
当 Tomcat 要销毁 Servlet 实例时(通常是在服务器关闭或应用卸载时),会先调用 destroy() 方法。
该方法在整个生命周期中只被调用一次。
用于释放资源,例如关闭数据库连接、停止后台线程等。
方法四:ServletConfig getServletConfig()
返回当前 Servlet 的配置信息对象。
通常在 init() 方法中保存传入的 ServletConfig 对象,然后在本方法中返回。
方法五:String getServletInfo()
返回一个描述当前 Servlet 的字符串,基本用不上。
2.2 创建第一个 Servlet
第一步:编写 Servlet 实现类
package cn.tx.servlet; import javax.servlet.*; import java.io.IOException; public class Servlet1 implements Servlet { @Override public void init(ServletConfig servletConfig) throws ServletException { System.out.println("Servlet 初始化完成"); } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { System.out.println("处理请求中..."); servletResponse.getWriter().write("Hello Servlet!"); } @Override public String getServletInfo() { return null; } @Override public void destroy() { System.out.println("Servlet 即将销毁"); } }第二步:在 web.xml 中配置 Servlet
<?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_3_1.xsd" version="3.1"> <!-- 声明 Servlet --> <servlet> <servlet-name>hello</servlet-name> <servlet-class>cn.tx.servlet.Servlet1</servlet-class> </servlet> <!-- 映射 Servlet 的访问路径 --> <servlet-mapping> <servlet-name>hello</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>配置说明:
<servlet-name>:为 Servlet 起一个别名,在项目内必须唯一。
<servlet-class>:Servlet 类的全限定名(包名 + 类名)。
<url-pattern>:客户端访问该 Servlet 的 URL 路径(相对于项目上下文路径)。
2.3 web.xml 配置详解
web.xml 是 JavaWeb 应用的部署描述符,位于 WEB-INF 目录下。它告诉 Tomcat 这个 Web 应用有哪些 Servlet、Filter、Listener,以及它们的映射关系。
重要规则:
<servlet-name> 在 <servlet> 和 <servlet-mapping> 中必须完全一致。
<url-pattern> 必须以 "/" 开头(除非是扩展名匹配)。
一个 Servlet 可以有多个 <url-pattern>,即多个路径映射到同一个 Servlet。
三、Servlet 生命周期
3.1 生命周期三阶段
Servlet 的生命周期由 Tomcat 管理,包含三个阶段:
阶段一:实例化与初始化
时机:第一次访问该 Servlet 时(或服务器启动时,若配置了 load-on-startup)。
动作:Tomcat 通过反射创建 Servlet 实例 → 调用 init() 方法。
次数:整个生命周期中只执行一次。
阶段二:服务
时机:每次客户端请求到达时。
动作:Tomcat 调用 Servlet 的 service() 方法。
次数:每来一次请求就执行一次,可执行多次。
阶段三:销毁
时机:服务器关闭或 Web 应用被卸载时。
动作:Tomcat 先调用 destroy() 方法,再销毁 Servlet 实例。
次数:整个生命周期中只执行一次。
3.2 生命周期完整演示代码
为了清晰地观察生命周期方法被调用的时机,我们可以编写如下测试代码:
package cn.tx.servlet; import javax.servlet.*; import java.io.IOException; public class LifecycleServlet implements Servlet { public LifecycleServlet() { System.out.println("1. 构造方法被调用(实例化)"); } @Override public void init(ServletConfig servletConfig) throws ServletException { System.out.println("2. init() 方法被调用(初始化)"); } @Override public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { System.out.println("3. service() 方法被调用(处理请求)"); response.getWriter().write("生命周期测试成功!"); } @Override public void destroy() { System.out.println("4. destroy() 方法被调用(销毁前)"); } @Override public ServletConfig getServletConfig() { return null; } @Override public String getServletInfo() { return null; } }部署运行后,首次访问该 Servlet 时,控制台输出:
1. 构造方法被调用(实例化)
2. init() 方法被调用(初始化)
3. service() 方法被调用(处理请求)
再次访问时,只输出:
3. service() 方法被调用(处理请求)
关闭服务器时,输出:
4. destroy() 方法被调用(销毁前)
重要结论:
Servlet 是单例的,一个 Servlet 类在 Tomcat 中只有一个实例。
多个线程可能同时访问同一个 Servlet 的 service() 方法,因此 Servlet 是线程不安全的。
四、HttpServlet
4.1 为什么需要 HttpServlet
我们已经看到,实现 Servlet 接口需要实现 5 个方法,其中 service() 方法的参数是 ServletRequest 和 ServletResponse,没有提供专门针对 HTTP 协议的方法(如 doGet、doPost)。
而现在的 Web 应用都是基于 HTTP 协议的,所以需要针对 HTTP 协议做一层封装。
继承关系如下:
Servlet(接口)
↑
GenericServlet(抽象类)—— 实现了 Servlet 接口,提供了通用的 servlet 功能
↑
HttpServlet(抽象类)—— 继承自 GenericServlet,专门处理 HTTP 请求
HttpServlet 为我们提供了以下便利:
自动将 ServletRequest 和 ServletResponse 转换为 HttpServletRequest 和 HttpServletResponse。
提供了 doGet()、doPost()、doPut()、doDelete() 等方法,分别对应不同的 HTTP 请求方法。
开发者只需继承 HttpServlet,重写需要处理的方法即可,无需再实现 service()。
4.2 HTTP 请求方法概述
HTTP 协议定义了多种请求方法,常用的有以下几种:
GET:请求获取指定资源,常用于查询操作(如搜索、查看详情)。参数在 URL 中可见,有长度限制。
POST:向服务器提交数据,常用于表单提交、上传文件等。参数在请求体中,安全性相对较好。
PUT:更新指定资源,用于修改数据。
DELETE:删除指定资源。
HEAD:类似 GET,但只返回响应头,不返回响应体,用于检查资源是否存在。
OPTIONS:查询服务器支持的请求方法。
TRACE:用于诊断,回显服务器收到的请求。
CONNECT:用于代理传输,如 HTTPS。
在实际开发中,90% 以上的场景只需要 GET 和 POST 两种方法。
4.3 创建 HttpServlet 的两种方式
方式一:手动创建类继承 HttpServlet
package cn.tx.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class Servlet2 extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.getWriter().write("处理 GET 请求"); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // POST 请求默认调用 doGet,也可以单独实现 doGet(request, response); } }然后在 web.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>方式二:使用 IDEA 快捷创建
在 IDEA 中,右键项目 → New → Servlet,填写类名和包名,IDEA 会自动生成继承 HttpServlet 的代码框架,并在 web.xml 中自动注册。开发者只需在 doGet/doPost 中编写业务逻辑即可。
4.4 load-on-startup 配置(创建顺序)
默认情况下,Servlet 在第一次被访问时才创建实例。但有些 Servlet 需要在服务器启动时就完成初始化(例如数据库连接池的初始化、定时任务调度等),可以通过 <load-on-startup> 实现。
配置方式:
<servlet> <servlet-name>hello</servlet-name> <servlet-class>cn.tx.servlet.Servlet1</servlet-class> <load-on-startup>1</load-on-startup> </servlet></servlet>
<load-on-startup> 的值是一个正整数,表示加载顺序:
值越小,优先级越高,越先被加载。
如果有多个 Servlet 都配置了该元素,Tomcat 会按照值从小到大依次创建。
值从 0 开始,通常使用 1、2、3...。
配置了 load-on-startup 的 Servlet,在 Tomcat 启动时就会创建实例并调用 init() 方法,而不需要等待第一次请求。
五、ServletConfig
5.1 ServletConfig 概述
ServletConfig 对象对应 web.xml 中某个 <servlet> 元素的配置信息。每个 Servlet 都有自己的 ServletConfig 对象。
ServletConfig 的主要作用:
获取当前 Servlet 在 web.xml 中配置的名称(<servlet-name>)。
获取当前 Servlet 的初始化参数(<init-param>)。
获取 ServletContext 对象。
ServletConfig 对象由 Tomcat 创建,通过 init() 方法传递给 Servlet。我们需要在 init() 方法中保存它,以便后续使用。
5.2 配置初始化参数
在 web.xml 的 <servlet> 元素中,可以添加多个 <init-param> 子元素来配置初始化参数:
<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>注意:<init-param> 必须写在 <servlet> 元素内部,不能写在 <servlet-mapping> 中。
5.3 获取初始化参数代码示例
在 Servlet 的 init() 方法中,通过 ServletConfig 对象获取参数:
@Override public void init(ServletConfig config) throws ServletException { // 获取单个参数 String username = config.getInitParameter("username"); System.out.println("username: " + username); // 获取所有参数名称(Enumeration 枚举类型) Enumeration<String> parameterNames = config.getInitParameterNames(); while (parameterNames.hasMoreElements()) { String paramName = parameterNames.nextElement(); String paramValue = config.getInitParameter(paramName); System.out.println(paramName + ": " + paramValue); } }在 HttpServlet 的子类中,可以直接调用 getServletConfig() 方法获取 ServletConfig 对象,无需在 init() 中保存。
六、Servlet 路径映射
6.1 URL-Pattern 三种匹配方式
<url-pattern> 的配置有三种方式,每种方式的语法和适用场景不同:
方式一:完全路径匹配
以 "/" 开头,表示一个精确的路径。
示例:/hello、/user/login
、/admin/user/list
特点:只能匹配一个固定的路径,精确度最高。
方式二:目录匹配
以 "/" 开头,以 "/*" 结尾,表示匹配该目录下的所有路径。
示例:/admin/*、/user/*、/*
特点:匹配指定目录下的所有请求,适用于批量路由。
方式三:扩展名匹配
不以 "/" 开头,以 ".*" 开头,表示匹配具有特定扩展名的请求。
示例:*.do、*.action、*.jsp
特点:匹配所有以指定扩展名结尾的请求。
6.2 匹配优先级规则
当一个请求 URL 同时匹配多个 <url-pattern> 时,Tomcat 按照以下优先级选择:
优先级从高到低:
第 1 优先级:完全路径匹配(精确匹配)
第 2 优先级:目录匹配(越长越精确越优先)
第 3 优先级:扩展名匹配
6.3 常见错误与经典案例
错误示例:
<url-pattern>/*.do</url-pattern> ← 这是错误的!目录匹配和扩展名匹配不能混用。
经典案例解析:
假设有以下 4 个 Servlet 映射:
Servlet11 → /abc/*
Servlet22 → /*
Servlet33 → /abc
Servlet44 → *.do
问题 1:访问 /abc/a.html
匹配结果:Servlet11(/abc/*)和 Servlet22(/*)都匹配,但 /abc/* 更精确,所以 Servlet11 响应。
问题 2:访问 /abc
匹配结果:Servlet11(/abc/*)和 Servlet33(/abc)都匹配,但完全路径匹配优先级最高,所以 Servlet33 响应。
问题 3:访问 /abc/a.do
匹配结果:Servlet11(/abc/*)和 Servlet44(*.do)都匹配,但目录匹配优先级高于扩展名匹配,所以 Servlet11 响应。
问题 4:访问 /a.do
匹配结果:Servlet22(/*)和 Servlet44(*.do)都匹配,目录匹配优先级高于扩展名匹配,所以 Servlet22 响应。
问题 5:访问 /xxx/yyy/a.do
匹配结果:Servlet22(/*)和 Servlet44(*.do)都匹配,目录匹配优先级更高,所以 Servlet22 响应。
总结:精确匹配 > 长目录匹配 > 短目录匹配 > 扩展名匹配。
七、相对路径与绝对路径
7.1 相对路径
相对路径是相对于当前资源所在的路径,通过 "."(当前目录)和 ".."(上级目录)来定位目标资源。
示例:
当前页面路径:http://localhost:8080/day5/user/list.html
目标资源:../index.html → 实际指向 http://localhost:8080/day5/index.html
相对路径的缺点:根据当前路径不同,写法也会不同,容易出错,不推荐在 Web 开发中使用。
7.2 绝对路径
绝对路径是指从根目录开始的完整路径。
方式一:带协议的完整路径(跨网站)
http://localhost:8080/day5/hello
方式二:以 "/" 开头的路径(同一站点内)
/day5/hello
7.3 客户端路径与服务器端路径的区别
在 Web 开发中," / " 在不同场景下有不同含义:
客户端路径(浏览器地址栏、HTML 中的 href、src 等):
"/" 代表服务器的根路径,即 http://localhost:8080/。
示例:<a href="/day5/hello"> 实际访问 http://localhost:8080/day5/hello
服务器端路径(Servlet 跳转、请求转发、包含等):
"/" 代表 Web 应用的根路径,即 http://localhost:8080/项目名/。
示例:request.getRequestDispatcher("/hello") 转发到当前项目下的 /hello
重要结论:
客户端使用时,绝对路径需要带上项目上下文名称。
服务器端使用时,绝对路径不需要带项目上下文名称,Tomcat 会自动补全。
八、ServletContext
8.1 ServletContext 概述
ServletContext 是一个全局的、所有用户共享的存储空间。它的生命周期与 Web 应用一致:当 Tomcat 启动 Web 应用时创建,当 Web 应用被卸载或 Tomcat 关闭时销毁。
重要特性:
一个 Web 应用只有一个 ServletContext 对象。
所有 Servlet 共享同一个 ServletContext 对象。
可以用来实现 Servlet 之间的数据通信。
被称为 "context 域对象",可以存储键值对数据。
8.2 ServletContext API 详解
主要方法:
Object getAttribute(String name)
功能:获取绑定在 context 中的属性值。
void setAttribute(String name, Object object)
功能:向 context 中绑定属性(键值对)。
String getContextPath()
功能:获取当前 Web 应用的上下文路径(即项目名)。
String getInitParameter(String name)
功能:获取 Web 应用的全局初始化参数。
String getRealPath(String path)
功能:将虚拟路径转换为文件系统中的真实物理路径。
InputStream getResourceAsStream(String path)
功能:获取指定路径资源的输入流。
8.3 获取全局初始化参数
在 web.xml 中配置全局参数(<context-param> 是 <web-app> 的直接子元素):
<context-param> <param-name>company</param-name> <param-value>达内教育</param-value> </context-param>在 Servlet 中获取:
ServletContext servletContext = getServletContext(); String company = servletContext.getInitParameter("company"); System.out.println("company: " + company);System.out.println("company: " + company);
注意:<context-param> 与 <init-param> 的区别:
<context-param> 是全局的,所有 Servlet 都能获取到。
<init-param> 是单个 Servlet 私有的,只能被当前 Servlet 获取。
8.4 实现数据共享:统计网站访问次数
使用 ServletContext 实现所有用户共享的访问计数器:
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 获取 ServletContext 对象 ServletContext servletContext = getServletContext(); // 从 context 中获取访问次数 Integer visitNums = (Integer) servletContext.getAttribute("visitNums"); // 如果为空,说明是第一次访问,初始化为 0 visitNums = (visitNums == null) ? 0 : visitNums; // 次数加 1 并保存回 context visitNums++; servletContext.setAttribute("visitNums", visitNums); // 响应给客户端 response.setCharacterEncoding("UTF-8"); response.getWriter().write("当前网站访问人次: " + visitNums); }每次用户访问该 Servlet,计数器都会加 1,且所有用户看到的是同一个计数结果。
8.5 读取资源文件的三种方式
在 JavaWeb 项目中,经常需要读取配置文件(如 .properties 文件),以下是三种常用的读取方式:
方式一:使用 ServletContext.getRealPath() 获取绝对路径后再读取
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); String username = p.getProperty("username"); System.out.println("username: " + username); } catch (Exception e) { e.printStackTrace(); }方式二:使用 ServletContext.getResourceAsStream() 直接获取流
ServletContext servletContext = getServletContext(); // 第一个 "/" 代表 Web 应用的根目录 try (InputStream is = servletContext.getResourceAsStream("/WEB-INF/tx1.properties")) { Properties p = new Properties(); p.load(is); String username = p.getProperty("username"); System.out.println("username: " + username); } catch (Exception e) { e.printStackTrace(); }方式三:使用类加载器读取 classpath 下的资源
// 使用当前类的类加载器,读取 classes 目录下的资源 try (InputStream is = this.getClass().getClassLoader() .getResourceAsStream("tx2.properties")) { Properties p = new Properties(); p.load(is); String username = p.getProperty("username"); System.out.println("username: " + username); } catch (Exception e) { e.printStackTrace(); }三种方式对比:
方式一:需要先获取绝对路径,再创建流,步骤稍多。
方式二:直接获取流,更简洁,推荐用于 Web 项目中读取资源。
方式三:不依赖 ServletContext,通用性最强,任何 Java 类都可以使用。