Servlet :生命周期、配置与实战

一、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() 方法被调用(处理请求)

再次访问时,只输出:

  1. service() 方法被调用(处理请求)

关闭服务器时,输出:

  1. 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 类都可以使用。

相关推荐
Dr.kangder9 小时前
嵌入式软件程序分析技术:原理、方法与实战
servlet·架构·嵌入式·dsp开发
上海安当技术3 天前
老系统零改造接入:旁路代理统一认证实战
java·servlet·架构·kubernetes·jenkins
Mr. zhihao3 天前
Jenkins 从节点连不上主节点?一次网络排查全记录 + 机器间通信“拦截层“全景讲解
网络·servlet·jenkins
米码收割机5 天前
【javaEE】Java Servlet JSP 影院票务管理系统(源码+文档)【独一无二】
java·servlet·java-ee
Hacker_xingchen7 天前
手把手教你配置Jenkins自动化邮件通知
自动化测试·软件测试·servlet·自动化·jenkins
fīɡЙtīиɡ ℡8 天前
内存泄漏产生的原因
java·spring·servlet
码上有光10 天前
异常和智能指针
java·大数据·c++·servlet·异常·智能指针
圣光SG12 天前
Servlet学习笔记
笔记·学习·servlet
智码看视界15 天前
Tomcat架构深度拆解:Connector和Container到底怎么配合的?
java·servlet·架构·tomcat·web服务器