Servlet概述

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 接口,并对 initdestroy 做了基本实现。它不绑定任何协议(如 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

相关推荐
恼书:-(空寄9 小时前
拦截器获取不到 POST 请求 JSON 结构体参数(完整解决方案)
java·spring boot·spring·servlet
_BugMan2 天前
【SSE】
java·servlet·tomcat
虚拟世界AI3 天前
Java服务器开发:零基础实战指南
java·servlet·tomcat
爱敲代码的菜菜3 天前
【项目】基于正倒排索引的Java文档搜索引擎
java·开发语言·前端·javascript·搜索引擎·servlet
清空mega5 天前
第7章:JavaBean、Servlet 与 MVC——从 JSP 页面开发走向规范项目
java·servlet·mvc
huohuopro5 天前
idea配置servlet项目
java·servlet·intellij-idea
沉默-_-5 天前
接收请求:HttpServletRequest的几种用法
前端·servlet·firefox
我真会写代码5 天前
手写tomcat框架
java·servlet·tomcat
TDengine (老段)6 天前
TDengine 视图功能使用
大数据·数据库·servlet·时序数据库·tdengine·涛思数据