目录

1. Servlet 简介

为 Java Web 应用代码审计做好基础铺垫,虽然使用这些技术都是零几年较为流行,但是底层仍然会用到,避免后面学框架发懵,先了解写原理增加熟悉感。

整个 Java Web 技术发展路线:

  • Servlet
  • JSP
  • Struts2
  • Spring MVC
  • SpringBoot
  • SpringBoot MicroService

Servlet 是 WebServer 和 Java 应用之间遵守的一套规范,常见的 WebServer 有 JBoss、Tomcat、Jetty、WebLogic、WebShphere,而随着时间发展 Servlet 有很多版本,2022 年已发布 6.0 版本。

Servlet Spec JSP Spec EL Spec WebSocket Spec Authentication (JASPIC) Spec Apache Tomcat Version Latest Released Version Supported Java Versions
6.0 3.1 5.0 2.1 3.0 10.1.x 10.1.0-M17 (beta) 11 and later
5.0 3.0 4.0 2.0 2.0 10.0.x 10.0.22 8 and later
4.0 2.3 3.0 1.1 1.1 9.0.x 9.0.65 8 and later
3.1 2.3 3.0 1.1 1.1 8.5.x 8.5.81 7 and later
3.1 2.3 3.0 1.1 N/A 8.0.x (superseded) 8.0.53 (superseded) 7 and later
3.0 2.2 2.2 1.1 N/A 7.0.x (archived) 7.0.109 (archived) 6 and later (7 and later for WebSocket)
2.5 2.1 2.1 N/A N/A 6.0.x (archived) 6.0.53 (archived) 5 and later
2.4 2.0 N/A N/A N/A 5.5.x (archived) 5.5.36 (archived) 1.4 and later
2.3 1.2 N/A N/A N/A 4.1.x (archived) 4.1.40 (archived) 1.3 and later
2.2 1.1 N/A N/A N/A 3.3.x (archived) 3.3.2 (archived) 1.1 and later

https://tomcat.apache.org/whichversion.html

不同 WebServer 都实现了 Servlet 规范,以后切换到其他服务器大体使用方法也差不多。本文以 Tomcat 10.0.22 举例。

Tomcat 实现的 Servlet 5.0 规范,对应 API 文档如下。或者你直接找 jakarta Servlet 规范发布页面(Jakarta Servlet 5.0 Javadoc)也能得到最新讯息。

Tomcat Servlet API 实现文档.png

Tomcat 9/10 区别是 9 实现 Oracle Servlet,10 变成 Jakarta Servlet。

The Jakarta EE platform is the evolution of the Java EE platform. Tomcat 10 and later implement specifications developed as part of Jakarta EE. Tomcat 9 and earlier implement specifications developed as part of Java EE.

https://tomcat.apache.org/index.html

而且 API 包名发生改变。

Users of Tomcat 10 onwards should be aware that, as a result of the move from Java EE to Jakarta EE as part of the transfer of Java EE to the Eclipse Foundation, the primary package for all implemented APIs has changed from javax.* to jakarta.*. This will almost certainly require code changes to enable applications to migrate from Tomcat 9 and earlier to Tomcat 10 and later. A migration tool has been developed to aid this process.

https://tomcat.apache.org/download-10.cgi

Servlet 规范中 WebApp 目录结构

  • WEB-INF,里面存放的 Web 应用相关内容。无法直接通过 URL 访问到,直接访问会是 404。

    • /WEB-INF/classes/,放编译后的 .class 字节码文件。

    • /WEB-INF/lib/,用于存放着各种 *.jar 包,方便 Tomcat 找到并调用。

    • /WEB-INF/web.xml,配置文件映射路径和 Servlet 关系,方便通过 URL 来找到 Servlet 并执行。

      <?xml version="1.0" encoding="UTF-8"?>
      <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                            https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
        version="5.0"
        metadata-complete="true">
      </web-app>
      
    • /WEB-INF/META-INF/resources/,存放各种 html/js/css/image 静态文件。

1.1 Windows 安装 Tomcat

https://tomcat.apache.org/

Tomcat 目录结构:

  • conf,配置文件目录。
  • bin,Tomcat 本身是 Java 写的,放着 Tomcat 核心 jar 包。
  • temp,临时文件夹。
  • logs,日志文件夹。
  • webapps,默认应用部署文件夹。
  • work,应用运行时产生的 jsp class 文件等。

运行 startup.bat 根据提示配置环境变量。

  • JAVA_HOME,Tomcat 会读取找到 jdk 目录运行自己。或者配置

    • JRE_HOME,这两个二选一,反正要有 Java 才能运行。
  • CATALINA_HOME,Value 为 Tomcat 根目录,方便找到 Tomcat 位置。

运行 bin/startup.sh/bat 脚本。

访问 127.0.0.1:8080。

1.2 第一个 Servlet 程序

Servlet 编写初体验。

import jakarta.servlet.Servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;

// 实现 Servlet 接口
public class HelloServlet implements Servlet {
    // 实现 Servlet 接口中 5 个方法方法
    public void destroy() {}
    
    public ServletConfig getServletConfig() {
        return null;
    }
    
    public String getServletInfo() {
        return "";
    }
    
    public void init(ServletConfig config) throws ServletException {}
    
    public void service(ServletRequest req, ServletResponse res) throws ServletException, java.io.IOException {
        // 在控制台输出字符串
        System.out.println("/hello 被请求");
    }
}

编译肯定找不到到包。-encoding UTF-8 可以防止代码里的中文字符出现错误。

PS C:\Users\gbb\Desktop> javac -encoding UTF-8 -d . HelloServlet.java
HelloServlet.java:1: 错误: 程序包jakarta.servlet不存在
import jakarta.servlet.Servlet;
                      ^
HelloServlet.java:2: 错误: 程序包jakarta.servlet不存在
import jakarta.servlet.ServletConfig;
                      ^
HelloServlet.java:3: 错误: 程序包jakarta.servlet不存在
import jakarta.servlet.ServletException;
                      ^
HelloServlet.java:4: 错误: 程序包jakarta.servlet不存在
import jakarta.servlet.ServletRequest;
                      ^
HelloServlet.java:5: 错误: 程序包jakarta.servlet不存在
import jakarta.servlet.ServletResponse;
                      ^
HelloServlet.java:9: 错误: 找不到符号
public class HelloServlet implements Servlet {
                                     ^
  符号: 类 Servlet
HelloServlet.java:13: 错误: 找不到符号
    public ServletConfig getServletConfig() {
           ^
  符号:   类 ServletConfig
  位置: 类 HelloServlet
HelloServlet.java:21: 错误: 找不到符号
    public void init(ServletConfig config) throws ServletException {}
                     ^
  符号:   类 ServletConfig
  位置: 类 HelloServlet
HelloServlet.java:21: 错误: 找不到符号
    public void init(ServletConfig config) throws ServletException {}
                                                  ^
  符号:   类 ServletException
  位置: 类 HelloServlet
HelloServlet.java:23: 错误: 找不到符号
    public void service(ServletRequest req, ServletResponse res) throws ServletException, java.io.IOException {}
                        ^
  符号:   类 ServletRequest
  位置: 类 HelloServlet
HelloServlet.java:23: 错误: 找不到符号
    public void service(ServletRequest req, ServletResponse res) throws ServletException, java.io.IOException {}
                                            ^
  符号:   类 ServletResponse
  位置: 类 HelloServlet
HelloServlet.java:23: 错误: 找不到符号
    public void service(ServletRequest req, ServletResponse res) throws ServletException, java.io.IOException {}
                                                                        ^
  符号:   类 ServletException
  位置: 类 HelloServlet
12 个错误

需要配置 JVM 环境变量 CLASSPATH,告诉 JVM 你需要的 CLASS 文件在哪里能找到。

CLASPATH=.;%CATALINA_HOME%\lib\servlet-api.jar

这样做可能导致环境其他应用编译异常,最好编译时手动指定 -cp/--class-path/-classpath 告知位置,-d 则可以指定编译后的 class 文件存放位置。

javac -encoding UTF-8 -cp '.;D:\gbb\tools\apache-tomcat-10.0.22\lib\servlet-api.jar' -d . HelloServlet.java

根据 Servlet 目录规范,在 %CATALINA_HOME%/webapps 创建 HelloServlet 项目目录,整个目录结构如下。

HelloServlet
└─ WEB-INF
       ├─ classes
       └─ lib

将编译产生的文件放入 /WEB-INF/classes/,这个过程就是部署(deploy)。为了方便你也可以在编译时直接使用 -d 选项自动将编译后字节码文件存入此文件夹,免去手工移动。

为了在能够通过 URL 访问到 Servlet,需在 %CATALINA_HOME%/webapps/HelloServlet/WEB-INF/ 下创建 web.xml 注册 Servlet 类。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                      https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
  version="5.0"
  metadata-complete="true">
    <!--Servlet 描述信息-->
    <servlet>
        <servlet-name>First Servlet Program</servlet-name>
        <servlet-class>HelloServlet</servlet-class>
    </servlet>
    <!--Servlet 映射信息-->
    <servlet-mapping>
        <servlet-name>First Servlet Program</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

<servlet>...</servlet><servlet-mapping>...</servlet-mapping> 是成对出现的,可以有多个。

编写规则如下:

  1. <servlet-name>...</servlet-name>,需要一致,只是备注使用;

  2. <servlet-class>...</servlet-class>,是 Servlet 类全限定名,后续通过 URL 访问能够找的就是它;

  3. <url-pattern>...</url-pattern>,是 URL 请求路径,以 / 开始,这是浏览器访问的地址。

访问 http://127.0.0.1:8080/HelloServlet/hello 在 Tomcat Console 能看到字符串被输出。

Servlet 执行成功.png

如何通过 URL 就能运行对应 Servlet 文件?Servlet 制定了规范,采用配置文件方式一个 URL 路径对应一个 Servlet 文件,文件名还必须按照规范取名为 web.xml,存放到 WEB-INF 目录下,这样 Tomcat 才能知道怎么按照规范找到 Servlet 文件。

Tomcat 怎么运行你的 Servlet 文件的?当你访问 http://127.0.0.1:8080/HelloServlet/hello 时 Tomcat 会去 webapps 下找 HelloServlet 是否存在,接着匹配 <url-pattern>,根据 <servlet-name> 找到 <servlet-class>,因为每个 Servlet 一定会实现 Servlet 接口,由于填了全限定名,可以指反射具体类,调用已经实现的 Service 方法完成运行。如果 WEB-INF/classes/ 确认有编译后字节码文件,就直接调用 Service 方法。

接下来我们看看如何在页面上响应内容。

仅仅命令行输出内容没什么意思,实际上还是要和用户进行交互,比如用户访问页面后我们给它在页面上响应内容。

这要用到 ServletResponse 接口几个方法,更多的操作在后面专门介绍。

  • java.io.PrintWriter getWriter(),获取 PrintWriter 输出。
  • setContentType(java.lang.String type),设置响应头 MIME 类型,要在输出前设置(根据测试来看放在后面也是可以的)。
import java.io.PrintWriter;

public void service(ServletRequest req, ServletResponse res) throws ServletException, java.io.IOException {
    // 设置 MIME 为 HTML,字符编码为 UTF-8,防止返回内容乱码。
    res.SetContentType("text/html;charset=UTF-8");
    
    // 获取输出对象
    PrintWriter resObj = res.getWriter();
    // 输出字符到页面
    resObj.print("<h1>Servlet 返回 Body</h1>");
}

还是和前面一样重新编译部署 class 文件,手动重启 Tomcat。再浏览就发现页面已经输出内容。

HTTP/1.1 200 
Content-Type: text/html;charset=UTF-8
Content-Length: 28
Date: Tue, 26 Jul 2022 08:22:56 GMT
Connection: close

<h1>Servlet 返回 Body</h1>

1.3 IDEA 开发 Servlet

前面手动部署重复繁琐,IDEA 帮忙解决了这个问题。

创建空项目。

IDEA JavaWeb 创建项目.png

File -> New Module 在项目下新建普通 Java 模块。

IDEA JavaWeb 项目下创建模块.png

给模块添加框架支持。

给模块添加框架支持.png 框架支持添加 Web Application.png

使用 Servlet 4.0,会自动创建 Servlet 目录结构和 web.xml,还可以看到 web.xml 中属性 version 写着 4.0,这就表示使用 Servlet 4.0 规范和其他属性一起是一种固定写法,每个版本的固定写法不一样。由于 jsp 还没学,可以先删掉。

自动生成的目录和文件.png

src 是放 servlet 源码,web 目录上有个实心原点,这是 Web 应用根目录。

此时编译 servlet 会找不到 jakarta 包。

有两种方式可以加载 %CATALINA_HOME%/lib/servlet-api.jar 包。

1.File -> Project Structure -> Project Settings -> Libraries 添加 %CATALINA_HOME%/lib/servlet-api.jar 包才行。也可以选择目录,载入目录下所有 jar 包。

IDEA 添加 Libraries 1.png IDEA 添加 Libraries 2.png

为模块添加刚创建的 Library。

IDEA 添加 Libraries 3.png IDEA 添加 Libraries 4.png

2.为模块加载 JAR 包或目录下所有 JAR 包。

IDEA 添加 Libraries 5.png IDEA 添加 Libraries 6.png

依赖配完,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_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>First Servlet</servlet-name>
        <servlet-class>HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>First Servlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

将 Servlet 部署到 Tomcat 服务器。

由于之前没配置过,先得添加个本地 Tomcat。

配置本地 Tomcat.png

如果你 Tomcat %CATALINA_HOME% 环境变量没配置,这里就需要手动指定 Tomcat HOME 目录。

配置本地 Tomcat Home 目录.png

配置完后就能自动识别版本。

配置本地 Tomcat Home 目录配置成功.png

Open browser 是在 Tomcat 启动后是否自动打开浏览器,每次重启都会打开浏览器,有点讨厌,可以关闭。

接下来就部署 Servlet 应用。

部署 Servlet.png

这里要填 Application Context,是指应用根目录,web.xml 注册的 Servlet 路径是 /hello,你定义的上下文是 test,最终访问路径是 http://locahost/test/hello

部署 Servlet-配置 Application context.png

最后运行即可。

IDEA 部署应用成功.png

如果程序有任何改动,可以选中 Servlet 重新部署。

选中 Servlet 重新部署.png

原先运行配置文件使用的是监听更新按钮设置为 Restart server。

运行或Debug配置文件更新操作.png

可以直接点击更新按钮,默认选择重启服务器,但重启服务器、更新 classes、重新部署,这几个选项都可以生效。

选中 Servlet 重新部署方法2.png

最方便的是热部署,做任何更改后都能及时看到更改,只需将 Run/Debug 配置文件 on frame deactivation 改为 Update classes and resources,只要离开 IDEA 窗口就会自己动更新 class 和资源文件。

热部署.png

2. Servlet Life Cycle(Servlet 生命周期)

Servlet 生命周期就是 Servlet 创建、调用到销毁的过程,整个过程都由 Tomcat 管理。

创建 ServletLifeCycle.java。

import jakarta.servlet.*;

import java.io.IOException;

public class ServletLifeCycle implements Servlet{
    public ServletLifeCycle() {
        System.out.println("Servlet 被实例化");
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("第一次请求 Servlet 会被执行 init 方法");
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("每次请求必定会执行 service 方法");
    }

    @Override
    public void destroy() {
        System.out.println("Web Container 关闭时会执行 destroy 方法");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }
}

注册 Servlet。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <servlet>
        <servlet-name>Servlet LifeCycle</servlet-name>
        <servlet-class>ServletLifeCycle</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Servlet LifeCycle</servlet-name>
        <url-pattern>/lifeCycle</url-pattern>
    </servlet-mapping>
</web-app>

配置的 Application context 为 /lifeCycle。

2.1 Instantiation Servlet(实例化 Servlet)

默认情况下运行 Tomcat 后 Servlet 不会被实例化,也没必要提前将所有 Servlet 都实例化完,假若 Servlet 没被访问不就浪费资源吗。

只有 Servlet 对应 Pattern 被请求后 Tomcat 才反射调用 Servlet 无参构造方法。

要想 Tomcat 启动时就实例化需要在 <servlet></servlet> 内嵌套 load-on-startup。

<load-on-startup>{Number}</load-on-startup>

值为 0 或正整数,数字越小则优先级越高,优先级高的会提前实例化。填负数将不会生效。

访问 http://localhost:8080/lifeCycle/lifeCycle 控制台输出。

Servlet 被实例化

一旦对象被创建以后访问不会重复创建对象,只会创建一个对象。

2.2 Initialization(初始化)

init 方法出现就是用于替代构造方法。Tomcat 只能反射调无参构造方法,一旦写个有参构造方法会无法实例化 Servlet 对象。那么直接在无参构造方法里初始化不就好了?Servlet 规范不建议写无参构造方法,要用 init 方法做替代。

当 Tomcat 调无参构造方法实例化后会第一时间调用 init 实例方法。此方法只会执行一次,不再重复执行。

Tomcat 控制台输出。

第一次请求 Servlet 会被执行 init 方法

2.3 Request Handling(处理请求)

init 方法调用完成,会调 service 方法处理 Tomcat 接收到的请求。请求多少次就调多少次 service 方法。

Tomcat 控制台输出。

每次请求必定会执行 service 方法

2.4 End of Service(结束服务)

当 Tomcat 销毁 Servlet 对象之前会调用一次 destroy 方法 。要注意 destroy 是个实例方法,没有实例化过的 Servlet 不可能调 destroy,因为对象都没有 。

Tomcat 控制台输出。

Web Container 关闭时会执行 destroy 方法

3. GenericServlet

Servlet 接口中常用的只有 service 方法,由于是接口,里面所有方法都要实现(init、destroy、getServletInfo、getServletConfig),但是这些方法并不常用,做起来特别繁琐。可以通过适配器设计模式,用一个类将它们都实现了,后续所有类再继承使用,这个做法像是做包装。

创建 GenericServlet.java 适配器,由它实现 Servlet 接口方法,仅将 service 方法做抽象,交由子类实现。

import jakarta.servlet.*;

import java.io.IOException;

public abstract class GenericServlet implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {}

    @Override
    public ServletConfig getServletConfig() { return null; }

    /**
     * 将常用的 service 给设置为抽象方法,交给子类自己实现。
     * @param servletRequest
     * @param servletResponse
     * @throws ServletException
     * @throws IOException
     */
    @Override
    public abstract void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException;

    @Override
    public String getServletInfo() { return null; }

    @Override
    public void destroy() {}
}

创建 TestServlet.java,用来实现 service 方法。

import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

import java.io.IOException;

public class TestServlet extends GenericServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("使用适配器");
    }
}

那假如 TestServlet 要使用 init 方法呢?直接重写 GenericServlet 类 init 方法,会导致 GenericServlet 类 init 方法内容消失。

最简单的方法是在 GenericServlet 里面重载 init 方法,再去原有 init 再去调重载方法

import jakarta.servlet.*;

import java.io.IOException;

public abstract class GenericServlet implements Servlet {
    /**
     * 设置 final 禁止重写误操作
     * @param servletConfig
     * @throws ServletException
     */
    @Override
    public final void init(ServletConfig servletConfig) throws ServletException {
        // init 则调重载方法即可。
        this.init();
    }

    /**
     * 交由子类重写
     */
    public void init() {}

    @Override
    public ServletConfig getServletConfig() { return null; }

    /**
     * 将常用的 service 给设置为抽象方法,交给子类自己实现。
     * @param servletRequest
     * @param servletResponse
     * @throws ServletException
     * @throws IOException
     */
    @Override
    public abstract void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException;

    @Override
    public String getServletInfo() { return null; }

    @Override
    public void destroy() {}
}

而 TestServlet 去重写 GenericServlet 刚刚重载的 init 方法即可。

import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

import java.io.IOException;

public class TestServlet extends GenericServlet {
    @Override
    public void init() {
        System.out.println("初始化");
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("使用适配器");
    }
}

如果 GenericServlet 类 init 方法 ServletConfig 参数要给到其他方法调用,可以直接写入成员变量,通过调 getServletConfig 方法获取。

import jakarta.servlet.*;

import java.io.IOException;

public abstract class GenericServlet implements Servlet {
    // 定义成员变量存放 ServletConfig 对象
    private ServletConfig servletConfig;

    /**
     * 设置 final 禁止重写误操作
     *
     * @param servletConfig
     * @throws ServletException
     */
    @Override
    public final void init(ServletConfig servletConfig) throws ServletException {
        // 将 ServletConfig 对象存入成员变量,方便其他方法调用
        this.servletConfig = servletConfig;

        // init 则调重载方法即可。
        this.init();

        System.out.println(servletConfig);
    }

    /**
     * 交由子类重写
     */
    public void init() {
    }

    /**
     * 获取 servletConfig 对象
     *
     * @return
     */
    @Override
    public ServletConfig getServletConfig() {
        return servletConfig;
    }

    /**
     * 将常用的 service 给设置为抽象方法,交给子类自己实现。
     *
     * @param servletRequest
     * @param servletResponse
     * @throws ServletException
     * @throws IOException
     */
    @Override
    public abstract void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException;

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
    }
}

子类直接可以调继承来的 getServletConfig 方法得到 ServletConfig 对象。

import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

import java.io.IOException;

public class TestServlet extends GenericServlet {
    @Override
    public void init() {
        System.out.println("初始化");
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("使用适配器");
        // 成功调取	
        System.out.println("子类能 service 够使用 ServletConfig 对象" + this.getServletConfig());
    }
}

而这些 jakarta.servlet.GenericServlet 已经实现,也是它实现的思路。

3.1 ServletConfig

了解下 init 参数 ServletConfig。

void init(ServletConfig config) throws ServletException

jakarta.servlet.Servlet.ServletConfig 是 Servlet 规范,每个 WebServer 都会实现此接口,Tomcat 是由 lib/catalina.jar 核心包 org.apache.catalina.core.StandardWrapperFacade 实现 ServletConfig 接口。

Tomcat 实例化 Servlet 对象后会调用 init 方法,传入的 ServletConfig 对象就是 StandardWrapperFacade,对象具体包含 web.xml <servlet>...</servlet> 标签信息。要注意的是每个 Servlet 调用 init 方法前都会创建 ServletConfig 对象,因此它们不共享一个 ServletConfig 对象。

Servlet 初始化参数可以在 web.xml 内 <servlet>...<servlet/> 中配置多组 <init-param>...<init-param>

<servlet>
    ......
    <init-param>
        <param-name></param-name>
        <param-value></param-value>
    </init-param>
</servlet>

ServletConfig 接口共有 4 个方法。

  • java.lang.String getServletName(),获取 <servlet-name></servlet-name> 值。
  • java.lang.String getInitParameter(java.lang.String name),先获取 <param-name></param-name> 值,当做参数传入才能得到 <param-value></param-value> 值。
  • java.util.Enumeration<java.lang.String> getInitParameterNames(),获取所有 init-param 内 <param-value></param-value> 值。
  • ServletContext getServletContext(),获取 ServletContext 对象。

这四个方法 jakarta.servlet.GenericServlet 都做了包装可以直接使用,无需通过 getServletConfig() 获取 ServletConfig 对象再调用它们。

创建 UseServletConfig.java。

import jakarta.servlet.GenericServlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Iterator;

public class UseServletConfig extends GenericServlet {
    @Override
    public void init() throws ServletException {
        System.out.println("Config 对象 <servlet-name> 值:" + getServletName());
        System.out.println();

        System.out.println("循环获取所有参数 name");
        Enumeration<String> initParameterNames = getInitParameterNames();
        while(initParameterNames.hasMoreElements()) {
            System.out.println(initParameterNames.nextElement());
        }
        System.out.println();

        System.out.println("指定参数名获取参数值");
        System.out.println("CustomConfigure=" + getInitParameter("CustomConfigure"));
        System.out.println("Status=" + getInitParameter("Status"));
        System.out.println();

        System.out.println("循环获取所有参数名/值");
        Iterator<String> stringIterator = getInitParameterNames().asIterator();
        while(stringIterator.hasNext()) {
            String name = stringIterator.next();
            String value = getInitParameter(name);
            System.out.println(name + "=" + value );
        }
        System.out.println();

        System.out.println("获取 ServletContext 对象" + getServletContext());
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

    }
}

注册 Servlet。

<servlet>
    <servlet-name>UseServletConfig12312312</servlet-name>
    <servlet-class>UseServletConfig</servlet-class>
    <load-on-startup>1</load-on-startup>
    <init-param>
        <param-name>CustomConfigure</param-name>
        <param-value>1</param-value>
    </init-param>
    <init-param>
        <param-name>Status</param-name>
        <param-value>0</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>UseServletConfig12312312</servlet-name>
    <url-pattern>/ServletConfigTest</url-pattern>
</servlet-mapping>

getServletName

获取注册的 ServletName 值。

System.out.println("Config 对象 <servlet-name> 值:" + getServletName());

输出。

Config 对象 <servlet-name> 值:UseServletConfig1231231

getInitParameterNames

获取的 names 是 Enumeration 枚举类型,和集合类似,通过一个一个取,一旦取完就 Enumeration 就是空的。

Enumeration<String> initParameterNames = getInitParameterNames();
while(initParameterNames.hasMoreElements()) {
    System.out.println(initParameterNames.nextElement());
}

输出。

循环获取所有参数 name
Status
CustomConfigure

getInitParameter

获取值有两种方式,1.直接指定参数名,2.循环获取所有参数名来查。

参数名一旦错了,就返回 String 对象默认值 null。

// 方式 1
System.out.println("指定参数名获取参数值");
System.out.println("CustomConfigure=" + getInitParameter("CustomConfigure"));
System.out.println("Status=" + getInitParameter("Status"));
System.out.println();

// 方式 2
System.out.println("循环获取所有参数名/值");
Iterator<String> stringIterator = getInitParameterNames().asIterator();
while(stringIterator.hasNext()) {
    // 获取参数名和参数值
    String name = stringIterator.next();
    String value = getInitParameter(name);
        
    System.out.println(name + "=" + value );
}

Enumeration<String> names = getInitParamenterNames();

while(names.hasMoreElements()) {
    // 获取元素
    String name = names.nextElement();
    
    // 获取值
    String value = getInitParameter(name);
}

输出。

指定参数名获取参数值
CustomConfigure=1
Status=0

循环获取所有参数名/值
Status=0
CustomConfigure=1

getServletContext

获取 ServletContext 对象。具体这个对象什么意思,在 ServletContext 小节有讲。

System.out.println("获取 ServletContext 对象" + getServletContext());

输出。

获取 ServletContext 对象org.apache.catalina.core.ApplicationContextFacade@a71cfd4

3.2 ServletContext

jakarta.servlet.ServletContext 规范由 Tomcat 中 org.apache.catalina.core.ApplicationContextFacade 类实现。它还有上下文、应用域的别称。

ServleContext 对象在服务器启动后部署应用阶段创建,里面存放所有在 web.xml 注册过的 Servlet 对象,这些 Servlet 对象都共享一个 ServletContext 对象,因为都是在一个 Webapp 下,只有不用应用才会生成不同的 ServletContext 对象。

ServletContext 由 ServletConfig 对象 getServletContext 方法获取。

ServletContext 有几个常用方法:

  • java.util.Enumeration<java.lang.String> getInitParameterNames(),获取所有 <param-name> 值。

    java.lang.String getInitParameter(java.lang.String name),通过参数名得到 <param-value> 值。

  • java.lang.String getContextPath(),获取应用路径,如应用定义的路径是 /test 就会获取到 test。

  • java.lang.String getRealPath(java.lang.String path),获取应用根目录下文件绝对路径,也可以直接获取跟路径。

  • void log(java.lang.String msg),向 logs/localhost.xxxx-xx-xx.log 写日志。

    void log(java.lang.String message, java.lang.Throwable throwable),不光能写日志,还能将日常也写进入。

  • void setAttribute(java.lang.String name, java.lang.Object object),给所有 ServletContext 设置属性。

  • java.util.Enumeration<java.lang.String> getAttributeNames(),获取 ServletContext 所有属性名称。

  • java.lang.Object getAttribute(java.lang.String name),获取 ServletContext 属性对应对对象。

  • void removeAttribute(java.lang.String name),删除 ServletContext 属性。

getInitParameterNames/getInitParameter

配置 web.xml 全局参数。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <context-param>
        <param-name>name1</param-name>
        <param-value>value1</param-value>
    </context-param>

    <context-param>
        <param-name>name2</param-name>
        <param-value>value2</param-value>
    </context-param>
    
    <servlet>
        <servlet-name>Servlet LifeCycle</servlet-name>
        <servlet-class>ServletLifeCycle</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Servlet LifeCycle</servlet-name>
        <url-pattern>/lifeCycle</url-pattern>
    </servlet-mapping>
</web-app>

和 servlet 里 <init-param> 配置有什么区别?<context-param> 是所有 Servlet 实例共享的,因此能放一些全局配置,而 <init-param> 是 Servlet 实例参数,只能实例自己访问。

import jakarta.servlet.*;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Iterator;

public class GetServletContextInitParameter extends GenericServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 获取上下文对象
        ServletContext servletContext = getServletContext();

        System.out.println("获取上下文参数值方法一,传入参数名获取参数值。");
        System.out.println(servletContext.getInitParameter("name1"));
        System.out.println(servletContext.getInitParameter("name2"));
        System.out.println();

        System.out.println("获取上下文参数值方法二,获取所有参数名再获取参数值。");
        Enumeration<String> names = servletContext.getInitParameterNames();
        Iterator<String> names1 = servletContext.getInitParameterNames().asIterator();
        
        while(names.hasMoreElements()) {
            System.out.println(servletContext.getInitParameter(names.nextElement()));
        }
        
        while(names1.hasNext()) {
            System.out.println(servletContext.getInitParameter((names1.next())));
        }
    }
}

输出。

获取上下文参数值方法一,传入参数名获取参数值。
value1
value2

获取上下文参数值方法二,获取所有参数名再获取参数值。
value2
value1
value2
value1

getContextPath

获取定义的应用根路径,相当于 WebApps 底下的每个 Webapp 的名称,而每个 Webapp 里面都是一个独立的 Web 项目,里面会有很多 Servlet,他们肯定都放在这个目录底下,因此都共享一个 ContextPath。

import jakarta.servlet.*;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Iterator;

public class GetServletContextPath extends GenericServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 获取上下文对象
        ServletContext servletContext = getServletContext();

        System.out.println("WebApp 路径:" + servletContext.getContextPath());
    }
}

比如访问 http://localhost:8080/ServerletContext/b,这相当于在 Webapps 下面 ServerletContext 目录,访问就会得到 ServerletContext。

WebApp 路径:/ServerletContext

如果你就部署在 ROOT 目录下,那访问就是空字符串。比如 http://localhost:8080/b 直接访问 b,此时就是空字符串。

getRealPath

获取站点根路径或根路径下的文件绝对路径。

import jakarta.servlet.*;

import java.io.IOException;

public class GetServletRealPath extends GenericServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 获取上下文对象
        ServletContext servletContext = getServletContext();

        System.out.println("WebApp 路径:" + servletContext.getRealPath(""));
        System.out.println("WebApp 路径:" + servletContext.getRealPath("/"));
        System.out.println("WebApp 路径:" + servletContext.getRealPath("servlet03.iml"));
    }
}

输出。

WebApp 路径:D:\gbb\learn\JavaProgrammingFundamentals\notes\JavaWebProject\out\artifacts\servlet03_war_exploded\
WebApp 路径:D:\gbb\learn\JavaProgrammingFundamentals\notes\JavaWebProject\out\artifacts\servlet03_war_exploded\
WebApp 路径:D:\gbb\learn\JavaProgrammingFundamentals\notes\JavaWebProject\out\artifacts\servlet03_war_exploded\servlet03.iml

Tomcat 日志

Tomcat 部署的应用日志会记录到 CATALINA_HOME/logs。

日志文件命名。

  • catalina.XXXX-XX-XX.log.txt,Tomcat 控制台日志
  • localhost_access_log.XXXX-XX-XX.txt,HTTP 请求日志
  • localhost.XXXX-XX-XX.log.txt,Servlet 调用 log 方法输出日志

IDEA 则不同,将放在自己的 Tomcat 目录下。

log 日志输出位置.png log 日志输出具体内容.png

应用域属性

会用到 setAttribute、getAttributeNames、getAttribute、removeAttribute 这几个操作属性的方法。

全局属性一般存放小数据,因为 Servlet 一旦运行通常不重启或关闭,整个生命周期长,数据大就占用内存多,长时间不用加上不销毁对象就浪费空间,当其他应用需要大内存,性能会差。

全局属性存放的数据一般不会变动,一旦变动会存在线程安全问题。

存放在全局属性相当于缓存在内存,获取速度比从数据库查来的快。

自定义一个对象,后面设置属性用。

/**
 * 自定义对象
 */
public class TmpObject {
    private String propertie1;
    private String propertie2;

    public TmpObject(String str1, String str2) {
        this.propertie1 = str1;
        this.propertie2 = str2;
    }

    @Override
    public String toString() {
        return "{\"propertie1\": " + propertie1 + ", propertie2: " + propertie2 + "}";
    }
}

定义设置属性的 Servlet,将 TmpObject 对象存入 attribute 属性。

import jakarta.servlet.*;

import java.io.IOException;

/**
 * Servlet 1
 */
public class SetPropertie extends GenericServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 设置属性
        ServletContext servletContext = getServletContext();
        servletContext.setAttribute("attribute", new TmpObject("属性值1", "属性值2"));

        servletResponse.setContentType("text/html;charset=UTF-8");
        servletResponse.getWriter().print("attribute 属性设置成功");
    }
}

定义获取属性值的 Servlet。

import jakarta.servlet.*;

import java.io.IOException;
import java.util.Iterator;

/**
 * Servlet 2
 */
public class GetPropertie extends GenericServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 获取属性
        ServletContext servletContext = getServletContext();

        // 获取所有属性名输出到页面上
        servletResponse.getWriter().print("所有属性名:<br />");
        Iterator<String> stringIterator = servletContext.getAttributeNames().asIterator();
        while(stringIterator.hasNext()) {
            servletResponse.getWriter().print(stringIterator.next() + "<br />");
        }

        // 将获取到的对象输出到页面
        Object object = servletContext.getAttribute("attribute");
        servletResponse.setContentType("text/html;charset=UTF-8");
        servletResponse.getWriter().print("<br />" + "attribute 对象:" + object);
    }
}

Application Context 为 ServletContext,并注册 Servlet。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <!-- 设置属性 -->
    <servlet>
        <servlet-name>setAttribute</servlet-name>
        <servlet-class>SetPropertie</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>setAttribute</servlet-name>
        <url-pattern>/setAttribute</url-pattern>
    </servlet-mapping>

    <!-- 获取属性 -->
    <servlet>
        <servlet-name>getAttribute</servlet-name>
        <servlet-class>GetPropertie</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>getAttribute</servlet-name>
        <url-pattern>/getAttribute</url-pattern>
    </servlet-mapping>
</web-app>

访问对应 url-pattern。

设置属性和获取属性.png

可以看到属性名不光有我们自己定义的 attribute,还有其他默认存在的属性。属性成功获取到,如果删除属性后再获取就是 null。

4. HttpServlet

实际开发 Servlet 不会直接实现 GenericServlet 抽象类,而是继承 jakarta.servlet.http.HttpServlet 抽象类,它做了更好的包装并提供几个使用的方法来处理不同请求。现实中开发中所有的 Servlet 都是继承它来写。

模板方法设计模式

为了后面分析 HttpServlet 源码提前看下模板方法设计模式。

先看不使用模板方法怎么写。

// 业务类
class B {   
    // 业务方法
    public void dynamicFunction() {
        System.out.prinln("自定义实现1");
        System.out.prinln("算法实施完毕");
    }
}

// 业务类
class C {
    // 业务方法
    public void dynamicFunction() {
        System.out.prinln("自定义实现2");
        System.out.prinln("算法实施完毕");
    }
}

定义了 B、C 两个业务类,都定义了 dynamicFunction 业务方法。可以发现每个业务方法实现都不一样,但是它们都有一个固定流程就是执行完 System.out.prinln("自定义实现2"); 一定会执行 System.out.prinln("算法实施完毕");

整个流程需要在两个方法中重复写,很冗余。

可以把 B、C 多段代码逻辑或算法相同可以抽出来作为模板,把各自不同业务方法定义为抽象方法,由子类继承实现。

// 模板类
class abstract A {
    public final void fixFuntion() {
        this.dynamicFunction();
        System.out.prinln("算法实施完毕");
    }
    
    // 不同的方法定义为抽象,由子类实现
    public void abstract dynamicFunction;
}

// 业务类
class B extends A {   
    // 业务方法
    @Override
    public void dynamicFunction() {
        System.out.prinln("自定义实现1");
    }
    
    fixFunction();
}

// 业务类
class C extends A {   
    // 业务方法
    @Override
    public void dynamicFunction() {
        System.out.prinln("自定义实现2");
    }
    
    fixFunction();
}

我们来分析下这个模板设计模式。

A 类就是模板类,fixFunction 是模板方法,这个模板方法存在意义是把 B、C 两个业务类相同的算法或业务逻辑步骤抽出来——这里一样的步骤就是执行完业务逻辑后一定会输出 ”算法实施完毕“ 着句话,交给模板方法实现。

为避免内容被子类覆盖破坏流程可以加上 final,只让子类继承使用。

由于模板方法中具体业务逻辑第一步每个业务类实现都不一样,就设置为抽象方法让子类实现。但是整个模板方法 fixFuntion 执行流程或者说是核心逻辑是固定的。

那与适配器模式区别在哪?适配器可没有模板方法,它只是做了层包装去提前实现某些方法,相当于半成品,你只需要最后做一点点加工即可,并没有把具体算法步骤封装成一个方法,所有的步骤还是需要你自己做。

而模板方法是把所有通用步骤做封装,你只需要调用即可,通用步骤中某一步骤各个业务实现不一需要你自己实现。

4.1 HttpServlet 生命周期分析

HttpServlet 用法很简单,你需要对哪个方法进行操作就去重写哪个方法,如 doPost 就是处理 POST 方法请求,其他也是如此:

  • doDelete
  • doGet
  • doHead
  • doOptions
  • doPost
  • doPut
  • doTrace

下面一起分析下 HttpServlet 声明周期。

创建 UseHttpServlet.java,在接收到 POST 请求在控制台输出字符串。

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class UseHttpServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("发送了 POST 方法请求");
    }
}

将整个运行逻辑梳理一遍顺序,首先Tomcat 调用 UseHttpServlet 类无参构造方法创建对象。接着初始化对象,由于 HttpServlet 没有 init 方法,要调其父类 GenericServlet 的 init 方法。

public void init(ServletConfig config) throws ServletException {
    this.config = config;
    this.init();
}

public void init() throws ServletException {
}

UseHttpServlet 没有 service 方法,会调 HTTPSevlet service 方法。

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    HttpServletRequest request;
    HttpServletResponse response;
    try {
        // 将传递来的
        // org.apache.catalina.connector.RequestFacade
		// org.apache.catalina.connector.ResponseFacade
        // 转换成 HttpServletRequest 和 HttpServletResponse
        request = (HttpServletRequest)req;
        response = (HttpServletResponse)res;
    } catch (ClassCastException var6) {
        throw new ServletException(lStrings.getString("http.non_http"));
    }

    this.service(request, response);
}

其中还把 ServletRequestServletResponse 向下转型,最终传入重载方法 service(HttpServletRequest req, HttpServletResponse resp)

重载的 service 方法会对各种不同 HTTP 方法请求做处理。

/**
 * Receives standard HTTP requests from the public
 * <code>service</code> method and dispatches
 * them to the <code>do</code><i>Method</i> methods defined in
 * this class. This method is an HTTP-specific version of the
 * {@link jakarta.servlet.Servlet#service} method. There's no
 * need to override this method.
 *
 * @param req   the {@link HttpServletRequest} object that
 *                  contains the request the client made of
 *                  the servlet
 *
 * @param resp  the {@link HttpServletResponse} object that
 *                  contains the response the servlet returns
 *                  to the client
 *
 * @exception IOException   if an input or output error occurs
 *                              while the servlet is handling the
 *                              HTTP request
 *
 * @exception ServletException  if the HTTP request
 *                                  cannot be handled
 *
 * @see jakarta.servlet.Servlet#service
 */
protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
	// 1. 返回大写 HTTP 方法名
    String method = req.getMethod();

    // 2. 检查请求方法是否匹配
    if (method.equals(METHOD_GET)) {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // servlet doesn't support if-modified-since, no reason
            // to go through further expensive logic
            // 3. 执行指定方法
            doGet(req, resp);
        } else {
            long ifModifiedSince;
            try {
                ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
                // Invalid date header - proceed as if none was set
                ifModifiedSince = -1;
            }
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }

    } else if (method.equals(METHOD_HEAD)) {
        long lastModified = getLastModified(req);
        maybeSetLastModified(resp, lastModified);
        doHead(req, resp);

    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);

    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);

    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);

    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req,resp);

    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req,resp);

    } else {
        //
        // Note that this means NO servlet supports whatever
        // method was requested, anywhere on this server.
        //

        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);

        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

各种比较请求方法,一旦匹配就执行 this.doXXX 方法,只有 UseHttpServlet 重写了就调重写过的 doXXX 方法,UseHttpServlet 没有重写就调 HttpServlet 下 doXX 方法,里面向页面返回 405。

比如 UseHttpServlet 只重写了 doPost 方法,当你用其他请求方法访问 Servlet 就会产生 405。

以 GET 请求为例,你用 GET 请求访问最后会调用 HttpServlet 中的 doGet 犯法。

protected void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
{
    String msg = lStrings.getString("http.method_get_not_supported");
    // 发送错误信息
    sendMethodNotAllowed(req, resp, msg);
}

private void sendMethodNotAllowed(HttpServletRequest req, HttpServletResponse resp, String msg) throws IOException {
    String protocol = req.getProtocol();
    // Note: Tomcat reports "" for HTTP/0.9 although some implementations
    //       may report HTTP/0.9
    if (protocol.length() == 0 || protocol.endsWith("0.9") || protocol.endsWith("1.0")) {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
    } else {
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
    }
}

在响应中返回 405 状态码和对应错误内容 HTML。

HttpServlet 405 方法不允许.png

你是不是想为啥 UseHttpServlet 重写 HttpServlet doXXX 方法后为啥不是调用 HttpServlet 自己的而是子类 UseHttpServlet 的?原因是多态,Tomcat 调用 service 方法,而 UseHttpServlet 里没有 service方法,自然是通过 UseHttpServlet 对象调继承过来的 service 方法,所以 service 方法在里面调用 doGet,此时的 this 是 UseHttpServlet 对象。

下面给出多态例子。

class HttpServlet {
    public void service() {
        System.out.println(this);
        this.doGet();
    }
    protected void doGet () {
        System.out.println("GET 方法不支持");
    }
}

class UseHttpServlet extends HttpServlet{
    @Override
    protected void doGet() {
        System.out.println("child 重写了父类 HttpServlet doGet方法");
    }
}

public class TomcatServer {
    public static void main(String[] args) {
        UseHttpServlet useHttpServlet = new UseHttpServlet();
        useHttpServlet.service();
    }
}

输出。

UseHttpServlet@3b07d329
child 重写了父类 HttpServlet doGet方法

4.2 Web 应用根路径默认页面

每个应用根目录都有默认的页面,当你访问根路径时等同于请求对应首页。

Tomcat 在 CATALINA_HOME/conf/web.xml 配置文件中默认配置了首页文件名 index.html、index.htm、index.jsp。

<!-- ==================== Default Welcome File List ===================== -->
<!-- When a request URI refers to a directory, the default servlet looks  -->
<!-- for a "welcome file" within that directory and, if present, to the   -->
<!-- corresponding resource URI for display.                              -->
<!-- If no welcome files are present, the default servlet either serves a -->
<!-- directory listing (see default servlet configuration on how to       -->
<!-- customize) or returns a 404 status, depending on the value of the    -->
<!-- listings setting.                                                    -->
<!--                                                                      -->
<!-- If you define welcome files in your own application's web.xml        -->
<!-- deployment descriptor, that list *replaces* the list configured      -->
<!-- here, so be sure to include any of the default values that you wish  -->
<!-- to use within your application.                                       -->

<welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>

只需要将首页文件放入应用根目录即可。

如果不想用 Tomcat 默认首页配置文件,可以在 web.xml 为应用单独配置首页。

<welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>login/index.html</welcome-file>
</welcome-file-list>

这里我就设置了两个首页文件,一个是在应用根目录下 index.html,另一个是根目录下 login 目录中的 index.html,当定义了多个首页会按照定义先后顺序查找,全部找不到后就 404,

至于要不要添加在文件路径前面 /,这个无所谓,都能找到。

首页也还可以配置为 Servlet UrlPattern。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <!-- 配置欢迎页 -->
    <welcome-file-list>
    	<welcome-file>test/getAttribute</welcome-file>
	</welcome-file-list>
    
    <!-- 注册 Servlet -->
    <servlet>
        <servlet-name>getAttribute</servlet-name>
        <servlet-class>GetPropertie</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>getAttribute</servlet-name>
        <url-pattern>/test/getAttribute</url-pattern>
    </servlet-mapping>
</web-app>

语法相当简单,就是把 Servlet <url-pattern> 的值写到 <welcome-file> 即可。

4.3 HttpServletRequest 处理请求

req 对象的类型是 HttpServletReques,它也是一个规范,各个 WebServer 都会实现,它用于处理 HTTP 请求所有内容。

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
	System.out.println("POST Method");
}

HttpServletReques 是一个接口,通过打印 req 对象 toString() 方法发现是 org.apache.catalina.connector.RequestFacade 类实现了 HttpServletRequest 接口。

public class RequestFacade implements HttpServletRequest

HttpServletRequest 接口继承 ServletRequest 接口,所以 RequestFacade 对象包含请求所有细节,Tomcat 都帮你解析好封装到此对象里,只要调用方法就能得到数据。

req 和 resp 对象生命周期是每次请求和响应都会重新创建。

ServletRequest 接口常用方法

ServletRequest 是所有请求的父接口,就连 HttpServletRequest 接口也是继承它的,因此先把 ServletRequest 全部了解才能知道 HttpServletRequest 又新增了哪些内容。

获取请求参数方法

这里提到的获取参数是不限制请求方法的,不管是 POST 还是 GET 都能获取到。

  • java.util.Map<java.lang.String,java.lang.String[]> getParameterMap() ,获取包含请求参数的参数名和参数值数组 Map 对象。
  • java.util.Enumeration<java.lang.String> getParameterNames() ,获取 Map 集合中所有属性名;
  • java.lang.String[] getParameterValues(java.lang.String name),获取 Map 集合中参数值数组;
  • java.lang.String getParameter(java.lang.String name) ,通过参数名获取参数值数组的第一个值,同名参数多个值会按照先后提交顺序取先提交的。

创建 UseServletRequestMethod.java 体验这四个方法。

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Map;
import java.util.Set;

public class UseServletRequestMethod extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // getParameterMap()
        System.out.println("getParameterMap 获取参数 Map");
        Map<String, String[]> parameterMap = req.getParameterMap();
        Set<String> strings = parameterMap.keySet();
        for (String key:
             strings) {
            String[] valueArray = parameterMap.get(key);
            System.out.print(key + "=");
            for (String value:
                 valueArray) {
                System.out.print(value + ",");
            }
        }
        System.out.print("\n\n");

        // getParameterNames()
        Enumeration<String> names = req.getParameterNames();
        System.out.println("getParameterNames 获取所有参数名");
        while(names.hasMoreElements()) {
            System.out.print(names.nextElement() + ",");
        }
        System.out.print("\n\n");

        // getParameterValues(java.lang.String name)
        System.out.println("getParameterValues 输入字符串参数 a 获取参数数组");
        String[] values= req.getParameterValues("a");
        System.out.print(Arrays.toString(values));
        System.out.print("\n\n");

        // getParameter(java.lang.String name)
        System.out.println("getParameter 获取参数数组第一个值");
        String value = req.getParameter("a");
        System.out.print(value);
    }
}

这里就以 GET 方法做演示,像其他方法操作也是一样的只不过把 doGet 换成其他 doXXX 方法名。

curl "http://127.0.0.1:8080/servlet04/p?a=1&a=2&b=3"

Tomcat Console 输出。

getParameterMap 获取参数 Map
a=1,2,b=3,

getParameterNames 获取所有参数名
a,b,

getParameterValues 输入字符串参数 a 获取参数数组
[1, 2]

getParameter 获取参数数组第一个值
1
设置请求域属性方法
  • void setAttribute(java.lang.String name, java.lang.Object o),设置属性;
  • java.util.Enumeration<java.lang.String> getAttributeNames(),获取所有属性名称;
  • java.lang.Object getAttribute(java.lang.String name),通过属性获取对象。

请求域和 ServletContext 应用域类似,只是应用域是在应用启动时创建出来,请求域则是请求对象创建后开始生效,请求结束则销毁。

因请求生命周期问题,你访问 A Servlet 设置的属性,想通过访问 B Servlet 来获取,发现做不到,因为这是两个请求会生成两个请求对象,这谈不上共享。要是 B Servlet 也能读取才有用,具体怎么让 B Servlet 读取到,这涉及到请求转发。

请求转发需要用 getRequestDispatcher(String path) 方法获取 RequestDispatcher 对象,参数路径填你要转发的 Servlet 当前 url-pattern。

RequestDispatcher getRequestDispatcher(java.lang.String path)

通过 RequestDispatcher 对象调 forward 方法即可跳转,参数需要将当前 HttpServletRequest 请求和 HttpServletResponse 响应对象传入。

void forward(ServletRequest request, ServletResponse response)

综合起来使用就是。

req.getRequestDispatcher("/url-pattern").forward(req, resp);

创建 A Servlet 设置属性并转发到 B Servlet。

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class A extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 设置请求域属性
        req.setAttribute("a", new Object());

        // 转发请求到 /b servlet。
        req.getRequestDispatcher("/b").forward(req, resp);

        // 重定向完继续执行 A Servlet
        System.out.println("重定向完继续执行 A Servlet 内容");
    }
}

创建 B Servlet。

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Arrays;

public class B extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("B Servlet 获取到了属性:" + req.getAttribute("a"));
        System.out.println("B 请求对象获取到了参数:" + Arrays.toString(req.getParameterValues("name")));
    }
}

此时访问 A Servlet。

http://localhost:8080/servlet04/a?name=1&name=2

会发现控制台输出:

B Servlet 获取到了属性:java.lang.Object@15ad271
B 请求对象获取到了参数:[1, 2]
重定向完继续执行 A Servlet 内容

根据现象发现访问 A Servlet 时使用转发将当前请求对象转到 Url-Pattern 为 /b 的 Servlet,会将 B Servlet 代码也执行。这跟重定向不一样,相当于 Tomcat 把 A Servlet 请求对象交给 B Servlet 继续处理此请求。

这里需要注意,forward 是把 A Servlet GET 请求对象 req 当做参数传给了 B Servlet,此时会交给 B Servlet 中的 doGet 方法继续处理此请求,如果 B Servlet 中没有 doGet Tomcat 就会出 405 不支持此请求方法的错误。

有时候不一定非要转发到 Servlet,也可以转发到服务器其他内部资源,如 html、图片等等,最常见的一个场景是你没权限访问就给你转到应用根目录下 403 html 页面。

其他请求信息获取

获取客户端 IP 地址和端口:

  • java.lang.String getRemoteAddr(),使用 Tomcat 10.0.22 测试代理头 X-Forwarded-For、Proxy-Client-IP、WL-Proxy-Client-IP、X-Real-IP、HTTP_CLIENT_IP,并不会取其中的值,推测为客户端 TCP 连接的地址。
  • int getRemotePort(),客户端建立 TCP 连接的端口。

设置接收到的请求体编码:

  • void setCharacterEncoding(java.lang.String env)

从 Tomcat 10 开始默认请求体编码是 UTF-8,以前的版本如果传输的内容没有自动识别编码,参数会乱码,通过此方法在获取参数之前正确设置参数值的编码,后续就再获取参数值就不会乱码。

<!-- Tomcat 10 web.xml 配置文件中请求和响应体编码默认都是 UTF-8 -->
<request-character-encoding>UTF-8</request-character-encoding>
<response-character-encoding>UTF-8</response-character-encoding>

而且在 GET 参数中,Tomcat 8 及之前版本默认为 ISO-8859-1,并不是 UTF-8,需要手动修改 Tomcat 的 %CATALINA_HOME%/conf/server.xml 配置文件,对 Connector 标签添加 URIEncoding 属性即可。

<!-- Tomcat 8 起默认值是 UTF-8 -->
<Connector URIEncoding="UTF-8" />

HttpServletRequest 接口常用方法

HttpServletRequest

获取应用路径:

  • java.lang.String getContextPath(),获取应用根目录名称,和 ServletContext.getContextPath() 作用一致。也可以叫获取应用域,或者获取所有 Servlet 的应用域。

获取请求方法:

  • java.lang.String getMethod(),比如 GET、POST、HEAD、OPTIONS 这种。

获取请求 URI:

  • java.lang.String getRequestURI(),比如 http://host:port/a/b/c?d=1,将得到 /a/b/c。

获取 Servlet 请求路径:

  • java.lang.String getServletPath(),获取 web.xml 配置的 url-pattern,也可以叫获取 Servlet 请求域。

4.4 HttpServletResponse 处理响应⚒️

HttpServletResponse 处理响应

ServletResponse 接口方法: void setContentType​(java.lang.String type),设置 Content-Type 响应头值 java.io.PrintWriter getWriter() throws java.io.IOException 返回一个 PrintWriter,通过调用 PrintWriter 的 print() 可以在响应体中写入内容。

ServletResponse 接口方法: void sendRedirect​(java.lang.String location) throws java.io.IOException,302 临时重定向。

使用方式一般分三种,这里通过访问 http://localhost/servlet05/jump 触发,ContextPath 是 servlet05。

跳转带斜杠则是当前 ContextPath 中跳转,最终跳到 http://localhost/test

resp.sendRedirect("/test");
HTTP/1.1 302
Location: /test
Content-Length: 0
Date: Tue, 04 Jun 2024 09:08:32 GMT
Keep-Alive: timeout=20
Connection: keep-alive

跳转不带斜杠则是当前 URL 后面进行跳转,最终跳到 http://localhost/servlet05/test

resp.sendRedirect("test");
HTTP/1.1 302
Location: test
Content-Length: 0
Date: Tue, 04 Jun 2024 09:07:03 GMT
Keep-Alive: timeout=20
Connection: keep-alive

跳转填写 URL,就跳转指定 URL https://www.baidu.com

resp.sendRedirect("https://www.baidu.com");
HTTP/1.1 302
Location: https://www.baidu.com
Content-Length: 0
Date: Tue, 04 Jun 2024 09:10:50 GMT
Keep-Alive: timeout=20
Connection: keep-alive

5. 使用 @WebServlet 注解开发 Servlet

每当想要新增一个 URI 都需要配置 web.xml,到时候文件超级大,也不方便更改。Servlet 3.0 规范支持用注解 @WebServlet 配置 Servlet,所有 web.xml 中的对应标签他都可以通过注解解决,它的元注解 @Target(ElementType.TYPE) 显示只能在类上使用。

5.1 配置 Servlet 访问路径

先看如何配 URL Pattern,配置访问路径涉及两个元素:

  • String[] value() default {};
  • String[] urlPatterns() default {};

他们传递元素都是需要数组类型,这里用花括号快速创建数组。

// value
@WebServlet(value={"/pattern1"})
@WebServlet(value={"/pattern2", "/pattern3"})

// urlPatterns
@WebServlet(urlPatterns={"/pattern4", "/pattern5"})
@WebServlet(urlPatterns="/pattern7")

如果只需要传一个元素就可以省略花括号。

// value
@WebServlet(value="/pattern")

// urlPatterns
@WebServlet(urlPatterns={"/pattern6"})

传一个元素另一种方式更为简洁,什么都不写直接填访问路径。

@WebServlet("/pattern8")

这里访问路径还可以填星号,代表访问 pattern8 下面任何资源都会匹配到此 Servlet 上。不要误会,在 web.xml 也可以使用星号。

@WebServlet("/pattern8/*")

5.2 配置启动容器创建实例

int loadOnStartup() default -1;,这个元素跟 Web.xml 中 <load-on-startup> 原理一致,在 2.1 小节已经讲过。

@WebServlet(value = "/pattern3", loadOnStartup = 1)        

5.3 配置初始化参数

WebInitParam[] initParams() default {};,和 <init-param> 一样在 3.1 小节已经讲过。只是在使用注解需要注意,传递的元素 WebInitParam 也是个注解数组,这个 WebInitParam 注解中有三个参数:

  • String name();
  • String value();
  • String description() default "";
@WebServlet(value = "/pattern3", initParams = {
        @WebInitParam(name = "init", value = "parm", description = "description"),
        @WebInitParam(name = "init", value = "parm", description = "description")}
)

5.4 设置 ServletName

和 Web.xml 中 <servlet-name> 一样,用来告诉 Servlet 干啥用的。

@WebServlet(value = "/pattern3", name = "SetServletName")

这里创建 UseWebServletAnnotation.java 测试以上所有元素配置效果。

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Enumeration;

@WebServlet(value = "/pattern3", name = "SetServletName", initParams = {
        @WebInitParam(name = "init", value = "parm", description = "description"),
        @WebInitParam(name = "init1", value = "parm1", description = "description")},
        loadOnStartup = 1
)
public class UseWebServletAnnotation extends HttpServlet {
    public UseWebServletAnnotation() {
        System.out.println("UseWebServletAnnotation constructor create");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("ConText:" + req.getContextPath());
        System.out.println("getServerName: " + getServletName());
        
        System.out.println("InitParameter:" + getInitParameter("init"));
        Enumeration<String> initParameterNames = getInitParameterNames();
        while(initParameterNames.hasMoreElements()) {
            String initParameterName = initParameterNames.nextElement();
            System.out.println(initParameterName + "属性值是: " + getInitParameter(initParameterName));
        }
    }
}

部署访问输出。

[2024-06-05 08:37:00,977] Artifact servlet05: Artifact is being deployed, please wait...
UseWebServletAnnotation constructor create
[2024-06-05 08:37:01,581] Artifact servlet05: Artifact is deployed successfully
[2024-06-05 08:37:01,581] Artifact servlet05: Deploy took 605 milliseconds
ConText:/servlet05
getServerName: SetServletName
InitParameter:parm
init属性值是: parm
init1属性值是: parm1

5.5 与 Web.xml 混合使用

web.xml 中 <web-app> 标签属性 metadata-complete 控制着能否与 @WebServlet 一起使用,设置为 true 则不能一起用,为 false 可以共存。

默认情况下此属性不会存在,默认值是 false,等同于 metadata-complete="false",这会让 Tomcat 自动扫描所有类中的注解信息来做配置,主动设置为 true 不会扫描注解。

如果没有老项目在使用 web.xml 注册 Servlet,完全可以删除 web.xml 以注解方式替代。

6.会话管理

用自己的话阐述会话的重要性。

6.1 Session

使用 jakarta.servlet.http.HttpSession 接口完成所有 Session 操作。

6.1.1 创建 Session

用 HttpServletRequest 的 HttpSession getSession() 实例方法获取 Session,在获取不到的时候创建一个 Session。

创建 useSession01.java。

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

@WebServlet("/set")
public class useSession01 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 创建 Session
        HttpSession session = req.getSession();

        // 打印 Session 信息
        System.out.println("响应的 SessionId:" + session.getId());
        System.out.println("响应的 Cookie:" + resp.getHeader("Set-Cookie"));
    }
}

部署完第一次访问,发现自动创建了 Session,在控制台输出了 Session 信息。

响应的 SessionId:A7470957B8728CB0C7947B5FCDDB6680
响应的 Cookie:JSESSIONID=A7470957B8728CB0C7947B5FCDDB6680; Path=/; HttpOnly

会在响应头中添加 Set-Cookie:JSESSIONID=A7470957B8728CB0C7947B5FCDDB6680; Path=/; HttpOnly,自动把 Session Id 作为 Cookie 的值返回。

Name Value Domain Path Expires/Max-Age Size
JSESSIONID 6E1B67F0BBDE561F6764F57A1CBE9D3B localhost / Session 42

下次再访问 // 子路径的资源就会携带 Cookie: JSESSIONID=A7470957B8728CB0C7947B5FCDDB6680 发请求,而应用通过 Cookie 找到具体 Session 对象,没有再创建新的 Session。

这里返回的 Session Id 名称默认名称 JSESSIONID 是可以修改的,在 Web.xml 中 web-app 标签中添加 session 相关的配置。

<?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_4_0.xsd"
         version="4.0">
    <session-config>
        <cookie-config>
            <name>TEST</name>
        </cookie-config>
    </session-config>
</web-app>

再重新补充创建会话发现 SessionId 变成 TEST。

C:\Users\gbb>curl localhost/set -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /set HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200
< Set-Cookie: TEST=95ABC288431EE48314952210E6888BF5; Path=/; HttpOnly
< Content-Type: text/html;charset=utf-8
< Content-Length: 132
< Date: Wed, 12 Jun 2024 13:25:14 GMT
<
响应的 SessionId:95ABC288431EE48314952210E6888BF5
响应的 Cookie:TEST=95ABC288431EE48314952210E6888BF5; Path=/; HttpOnly
* Connection #0 to host localhost left intact

6.1.2 获取 Session

获取 Session 依然使用 getSession(false) 方法,这里传递了布尔值 false,意思是获取不到 Session 时返回 null。

具体是怎么获取呢?只要你携带键为 JSESSIONID 的 Cookie 访问应用,就拿着值找运行内存中有没有这个 SessionId,还会判断是否过期,只要存在并且有效就会成功获取 Session 会话,如果 SessionId 不对或者压根没有就是获取失败。这里说的 JSESSIONID 是默认名称,要是更改了就自动获取自定义名称。

创建 useSession02.java。

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/get")
public class useSession02 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取 Session,获取不到返回 null
        HttpSession session = req.getSession(false);

        // 向页面上输出获取到 Session 信息
        PrintWriter writer = resp.getWriter();
        if (session != null) {
            writer.println("获取到的 SessionId:" + session.getId());
        } else {
            writer.println("Session 为 null");
        }
    }
}

先访问 /set 获取 Cookie。

C:\Users\gbb>curl http://localhost/set -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /set HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200
< Set-Cookie: JSESSIONID=B9AE7703A6489DF0044DA8DE17C11754; Path=/; HttpOnly
< Content-Length: 0
< Date: Tue, 11 Jun 2024 14:18:32 GMT
<
* Connection #0 to host localhost left intact

不使用 Cookie 访问,不能获取 Session。

C:\Users\gbb>curl http://localhost/get -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /get HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200
< Content-Length: 18
< Date: Tue, 11 Jun 2024 14:20:28 GMT
<
Session 为 null
* Connection #0 to host localhost left intact

使用 Cookie 访问,成功获取到 Session 会话。

C:\Users\gbb>curl http://localhost/get -H "Cookie: JSESSIONID=B9AE7703A6489DF0044DA8DE17C11754" -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /get HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
> Cookie: JSESSIONID=B9AE7703A6489DF0044DA8DE17C11754
>
< HTTP/1.1 200
< Content-Length: 59
< Date: Tue, 11 Jun 2024 14:32:14 GMT
<
获取到的 SessionId:B9AE7703A6489DF0044DA8DE17C11754
* Connection #0 to host localhost left intact

6.1.3 Session 生命周期

后端 Session 也不是永久有效的,超时会销毁。

Session 超时配置,在 tomcat 配置文件 web.xml 中默认值设置为 30 分钟,要是想为项目中单独指定超时时间可以向我们项目 web.xml 的 <web-app> 内配置 <session-config>

<session-config>
    <!--30 分钟自动销毁 session-->
    <session-timeout>30</session-timeout>
</session-config>

另一种设置方法更为灵活,是调用 HttpSession 对象的 setMaxInactiveInterval(int) 方法,指定 Session 最长能存活几秒。需要注意,手动调方法设置存活时长,就会覆盖掉 <session-config>,其次配置设置的有效期是指后端 Session,而不是前端 Cookie 的有效期。

// 创建 Session
HttpSession session = req.getSession();

// 设置 Session 有效期 20 秒
session.setMaxInactiveInterval(20);

而有效期可以通过 getMaxInactiveInterval() 获得最长能存活多少秒。

// 获取 Session,获取不到返回 null
HttpSession session = req.getSession(false);

// 向页面上输出获取到 Session 信息
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
if (session != null) {
    writer.println("获取到的 SessionId:" + session.getId());
    writer.println("Cookie 有效期:" + session.getMaxInactiveInterval());
} else {
    writer.println("Session 为 null");
}

如果向提前主动销毁 Session,需要调用 HttpSession 对象的 invalidate() 方法。需要注意这个方法没法指定 SessionId 进行销毁。

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/del")
public class useSession03 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession(false);

        PrintWriter writer = resp.getWriter();
        resp.setContentType("text/html;charset=utf-8");

        if (session != null) {
            // 主动销毁 Session
            session.invalidate();
            writer.println("已经销毁 Session:" + session.getId());
        } else {
            writer.println("Session 为 null");
        }
    }
}

重新获取自定义的 SessionId 进行销毁,成功。

C:\Users\gbb>curl localhost/set -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /set HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200
< Set-Cookie: TEST=23B8F30DC123DDFBAC243C13448760AC; Path=/; HttpOnly
< Content-Type: text/html;charset=utf-8
< Content-Length: 132
< Date: Wed, 12 Jun 2024 13:27:32 GMT
<
响应的 SessionId:23B8F30DC123DDFBAC243C13448760AC
响应的 Cookie:TEST=23B8F30DC123DDFBAC243C13448760AC; Path=/; HttpOnly
* Connection #0 to host localhost left intact

C:\Users\gbb>curl localhost/del -H "Cookie: TEST=23B8F30DC123DDFBAC243C13448760AC" -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /del HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
> Cookie: TEST=23B8F30DC123DDFBAC243C13448760AC
>
< HTTP/1.1 200
< Content-Type: text/html;charset=UTF-8
< Content-Length: 57
< Date: Wed, 12 Jun 2024 13:27:51 GMT
<
已经销毁 Session:23B8F30DC123DDFBAC243C13448760AC
* Connection #0 to host localhost left intact

6.1.4 会话域属性

这里的 Session 叫会话域和前面 ServletRequest 请求域,ServletContext 应用域一起并称三大作用域。

void setAttribute(String name, Object value) 这里是可以在会话里添加属性,这个属性内容可以是一个 Object 类型的对象,相当于可以放任何内容。

最常见的一个场景是用户登陆后创建的 Session,为了防止越权产生,我们在这个 Session 设置一个用户 id,后续访问其他功能时检查 Session 对象与请求参数中的 id 是否一致判断有没越权行为。

获取属性对象,一般通过属性名称获取 Object getAttribute(String name),要想获取所有的属性对象执行 Enumeration<String> getAttributeNames() 得到属性的枚举对象再 getAttribute()

既然可以设置属性,也可以删除属性 void removeAttribute(String name)

6.2 Cookie

jakarta.servlet.http.Cookie 用来操作 Cookie。

Cookie 分两种:

  • 一次性 Cookie,创建会话后没有设置时长,响应的 Cookie 有效期被标注成 Session,表明只在当前浏览器运行内存中临时存储,关闭浏览器后就不再保存,因此下次请求应用任然会创建新的会话。在前面创建 Session 返回的就是这种。
  • 永久性 Cookie,创建会话后设置了时长,因此响应的 Cookie会带上多久失效,浏览器会把这中设置了时效的 Cookie 储存到硬盘上,下次访问依旧存在,只有过期后关闭浏览器自动清除。

6.2.1 获取 Cookie

获取 Cookie 需要用到 HttpServletRequest 的 getCookies() 方法,获取到返回 jakarta.servlet.http.Cookie 类型的数组,没有获取到返回 null。

创建 useCookie02.java

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/getCookie")
public class useCookie02 extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
        // 获取 Cookie
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            PrintWriter writer = res.getWriter();
            writer.println("客户端发送的 Cookie 是:");
            for (Cookie cookie : cookies) {
                writer.println("Name: "+ cookie.getName() + ", Value: " + cookie.getValue() +
                        ", Domain: " + cookie.getDomain() + ", Path: " + cookie.getPath() +
                        ", MaxAge: " + cookie.getMaxAge() + ", Secure:" + cookie.getSecure() +
                        ", HttpOnly: " + cookie.isHttpOnly());
            }
        }
    }
}

运行成功获取所有 Cookie 中的信息。

C:\Users\gbb>curl http://localhost/getCookie -H "Cookie: test1=v1; test2=v2; test3=v3" -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /getCookie HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
> Cookie: test1=v1; test2=v2; test3=v3
>
< HTTP/1.1 200
< Content-Length: 319
< Date: Wed, 12 Jun 2024 08:14:57 GMT
<
客户端发送的 Cookie 是:
Name: test1, Value: v1, Domain: null, Path: null, MaxAge: -1, Secure:false, HttpOnly: false
Name: test2, Value: v2, Domain: null, Path: null, MaxAge: -1, Secure:false, HttpOnly: false
Name: test3, Value: v3, Domain: null, Path: null, MaxAge: -1, Secure:false, HttpOnly: false
* Connection #0 to host localhost left intact

6.2.2 创建 Cookie

创建 useCookie01.java

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/setCookie")
public class useCookie01 extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
        // 创建 Cookie 对象
        Cookie cookie = new Cookie("name", "value");

        // 将 Cookie 输出到响应体
        res.addCookie(cookie);
        PrintWriter writer = res.getWriter();
        writer.println("返回的 Cookie 是:" + res.getHeader("set-cookie"));
    }
}

访问 /setCookie 确实通过 Set-Cookie 返回。

C:\Users\gbb>curl http://localhost/setCookie -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /setCookie HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200
< Set-Cookie: name=value
< Content-Length: 35
< Date: Wed, 12 Jun 2024 07:53:31 GMT
<
返回的 Cookie 是:name=value
* Connection #0 to host localhost left intact

6.2.2 设置 Cookie 有效期

前面设置的 Cookie 每次关闭浏览器都会被清空,可以用 setMaxAge(int expiry) 方法设置多少秒后 Cookie 过期,让其长时间存储。

这个值有讲究,不能随便设,当 Cookie 有效期小于零,Cookie 会存在内存中,重启浏览器失效。有效期设置为零表示浏览器不接收 Cookie,无法使用,相当于删除此 Cookie。

大于零的情况,就在指定秒后过期。

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/setCookieLifecycle")
public class useCookie01 extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
        // 创建 Cookie 对象
        Cookie cookie = new Cookie("name", "value");

        // 设置 Cookie 有效期为两小时
//        cookie.setMaxAge(60 * 120);

        // 设置为零浏览器不会存储,无法使用,相当于让浏览器删除此 Cookie。
//        cookie.setMaxAge(0);

        // 设置为负数,标识一次性使用,浏览器重启失效
        cookie.setMaxAge(-123123123);

        // 将 Cookie 输出到响应头
        res.addCookie(cookie);
        PrintWriter writer = res.getWriter();
        writer.println("返回的 Cookie 是:" + res.getHeader("set-cookie"));
    }
}

访问 /setCookieLifecycle 后发现成功设置有效期微 7200 秒,而 Expires 中的时间不准是因为这个时间是 UTC。

Set-Cookie: name=value; Max-Age=7200; Expires=Wed, 12 Jun 2024 10:36:36 GMT

当然这里 Cookie 作用域是可以设置的,使用 setPath(String uri) ,这在前面获取 Cookie 中都有看到 getter 方法,对应他们也有 setter,不再赘述。

7. Filter

Filter 是 Servlet 2.3 新增功能,是一个检查机制,启用 Filter 的情况下所有请求都优先经过它,具体来说可以控制请求能否访问到后面的资源,还可以修改响应内容。

// 请求
Client -> Filter -> WebResource

// 响应
Client <- Filter <- WebResource

这个请求响应有点像数据结构栈,先入后出。再提一嘴这个 Filter 是一种责任链设计模式开发的。

7.1 创建 Filter 三步骤

创建 testServlet.java,访问 /test 就向页面输出文字 Test Servlet。

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(value = "/test", name = "testaz")
public class testServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter writer = resp.getWriter();
        writer.println("Test Servlet");
    }
}

此时有个需求是,访问 /test 必须有请求头 Secure,没有不让访问 /test 的资源,并页面上输出 “请求头 Secure 缺失”。这可以用 Filter 完成请求检查。

创建 useFilter.java

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;

public class useFilter implements Filter {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 1.请求处理
        HttpServletRequest request1 = (HttpServletRequest) request;
        String secureValue = request1.getHeader("secure");
        if (secureValue != null) {
            System.out.println(secureValue);

            // 2.请求放行
            // 将请求转给下一个过滤器,要是没有过滤器把请求转到资源。
            chain.doFilter(request, response);
        } else {
            // 3.响应处理
            response.setContentType("text/html;charset=utf-8");
            response.getWriter().println("请求头 Secure 缺失");
        }
    }
}

要编写 Filter 有个三步骤,首先要实现 Tomcat 实现的 Filter 接口 jakarta.servlet.Filter,其次重写过滤器 doFilter 方法来具体实施过滤逻辑,最后是配置过滤器让其生效。

由于所有请求和响应都会经过 Filter,这里 doFilter 方法有三个参数,前两个是请求和响应,最后一个是 FilterChain 对象,用来把请求转发到下一个 Filter 去,因为可能有很多 Filter 串起来所以需要转发,Filter-1 检查完了转给 Filter-2 继续检查,所有都检查完了,就把请求转发给具体资源,这点在后面 Filter 链小节再聊。

请求到了 Filter,处理也很简单,在 doFilter 直接检查有没有请求头 Secure,有就直接把请求通过 doFilter 转给资源。

chain.doFilter(request, response);

要是没有请求头 Secure,Filter 直接向页面上写 "请求头 Secure 缺失"。

response.setContentType("text/html;charset=utf-8");
response.getWriter().println("请求头 Secure 缺失");

逻辑写完了 Filter 可不会生效,像 Servlet 一样需要主动配置过滤器,先看 web.xml 如何配。

<filter>
    <!--filter 别名-->
    <filter-name></filter-name>
    <!--编写的 filter 全限定名-->
    <filter-class></filter-class>
</filter>
<filter-mapping>
    <!--filter 想对哪些资源起作用-->
    <url-pattern></url-pattern>
    <servlet-name></servler-name>
</filter-mapping>

<filter><filter-name> 是为这个 Filter 起个名称,方便标识,<filter-class> 是你编写的 Filter 类全限定名。

<filter-mapping> 则是要配置哪些资源需要经过 Filter 检查,<url-pattern> 是 url-pattern,比如 /test 就只过滤指定资源,或者 /test/* 是过滤 /test 下所有资源,星号就是所有的意思,在 filter-mapping 里 url-pattern 标签只能写一个。<servlet-name> 很直观,就是通过 ServletName 找到 url-pattern 对这个 url-pattern 进行过滤,同样在 filter-mapping 中也只能也写一个。它两可以在 filter-mapping 里一起出现,比如,既通过 url-pattern 拦截路径也能指定 servlet-sname 拦截 servlet 指定的 url-pattern,也可以只单独使用 url-pattern 或者 servlet-name,如果想要拦截多个可以编写多个 filter-mapping。

这里需要注意的是 filter 和 filter-mapping 并没有先后顺序的要求,挨在一起只是方便识别它两是有一对一关系。

根据上面原则编写 Filter 配置如下。

<?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_4_0.xsd"
         version="4.0">
    <filter>
        <!--filter 别名-->
        <filter-name>test</filter-name>
        <!--编写的 filter 全限定名-->
        <filter-class>useFilter</filter-class>
        <init-param>
            <param-name>paramName</param-name>
            <param-value>paramValue</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <!--filter 想对哪些资源起作用-->
        <filter-name>test</filter-name>
        <servlet-name>testaz</servlet-name>
        <url-pattern>/test</url-pattern>
    </filter-mapping>
</web-app>

实际运行发现,过滤逻辑成功实现。

C:\Users\gbb>curl localhost/test -H "secure: test" -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /test HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
> secure: test
>
< HTTP/1.1 200
< Content-Length: 14
< Date: Thu, 13 Jun 2024 05:43:29 GMT
<
Test Servlet
* Connection #0 to host localhost left intact

C:\Users\gbb>curl localhost/test -H "secure1: test" -vv
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET /test HTTP/1.1
> Host: localhost
> User-Agent: curl/8.4.0
> Accept: */*
> secure1: test
>
< HTTP/1.1 200
< Content-Type: text/html;charset=utf-8
< Content-Length: 25
< Date: Thu, 13 Jun 2024 05:43:38 GMT
<
请求头 Secure 缺失
* Connection #0 to host localhost left intact

7.2 Filter 生命周期

生命周期和 Servlet,只不过处理的是 Filter 相关内容。

创建 FilterLifecycle.java

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;

import java.io.IOException;

public class FilterLifecycle implements Filter {
    public FilterLifecycle() {
        System.out.println("FilterLifecycle 被实例化");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init 方法被执行");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("被请求");
    }

    @Override
    public void destroy() {
        System.out.println("FilterLifecycle 被销毁");
    }
}
  1. 实例化 Filter。Tomcat 启动自动创建实例,只被执行一次。
  2. 初始化。Tomcat 启动后创建完 Filter 对象后紧跟着执行初始化方法。 获取参数要先在 web.xml 中设置参数。
    <filter>
        <init-parm>
            <parm-name>参数名</parm-name>
            <parm-value>参数值</parm-value>
        </init-parm>
    </filter>
    
  3. 处理请求和响应。每次发送的请求都会被调用,执行多次。
  4. 销毁。Filter 对象被关闭前执行,一般是 Tomcat 正常关闭时执行一次。

运行服务器后首先执行无参构造方法和 init 方法。

FilterLifecycle 被实例化
init 方法被执行
    FilterName: test
    param-name: paramName2
    param-value: paramValue2

随后访问 /test 执行 doFilter 方法。

被请求

正常关闭 Tomcat 服务器,销毁方法正常执行。

FilterLifecycle 被销毁

7.3 注解方式配置 Filter

和 Servlet 一样 Filter 也可以用注解配置,这个注解是 jakarta.servlet.annotation.WebFilter,只能在类上使用,具体来说是在 Filter 类上用。

默认情况下什么都不写,是指定要过滤的 url-pattern

// 配置单个路径
@WebFilter("/test")

另外两个 String[] valueString[] urlPatterns 也是可以配置要过滤的路径。只设置 url-pattern 官方建议优先使用 value 元素。

// 配置单个路径
@WebFilter(urlPatterns = "/test")
@WebFilter(urlPatterns = {"/test"})

// 配置多个路径
@WebFilter(urlPatterns = {"/test", "/tet/*"})
// 配置单个路径
@WebFilter(value = "/test")
@WebFilter(value = {"/test"})

// 配置多个路径
@WebFilter(value = {"/test", "/tet/*"})

当然也可以和 web.xml 一样去限制 <servlet-name> 中的路径,使用 String[] servletNames() 来做到这点。

// 配置单个类
@WebFilter(servletNames = "testaz")
@WebFilter(servletNames = {"testaz"})

// 配置多个类
@WebFilter(servletNames = {"testaz", "testzz"})

<filter-name> 也可以通过 filterName 元素进行配置,不过在使用注解的情况下基本没有用。就配置 web.xml 中 <filter> 需要和 <filter-mapping> 有个映射关系,全靠 <filter-name> 维系,不加就报错。

@WebFilter(value = "/test", filterName = "testaaaa")

<init-param>,可以用 @WebInitParam 元素配置。

// 配置一个参数
@WebFilter(
        value = "/1",
        initParams = @WebInitParam(name = "parmName1", value = "parmValue1")
)

// 配置多个参数
@WebFilter(
        value = "/1",
        initParams = {
                @WebInitParam(name = "parmName1", value = "parmValue1"),
                @WebInitParam(name = "parmName2", value = "parmValue2"),
                @WebInitParam(name = "parmName3", value = "parmValue3"),
        }
)

改造原有 web.xml 为注解。

<?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_4_0.xsd"
         version="4.0">
    <filter>
        <!--filter 别名-->
        <filter-name>test</filter-name>
        <!--编写的 filter 全限定名-->
        <filter-class>useFilter</filter-class>
        <init-param>
            <param-name>paramName</param-name>
            <param-value>paramValue</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <!--filter 想对哪些资源起作用-->
        <filter-name>test</filter-name>
        <servlet-name>testaz</servlet-name>
        <url-pattern>/test</url-pattern>
    </filter-mapping>
</web-app>

改造后简洁许多。

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;


@WebFilter(
        filterName = "test", // 不写默认就是 useFilter 的全限定名,在注解中可有可无。
        value = "/test",
        servletNames = "testaz",
        initParams = @WebInitParam(name = "paramName", value = "paramValue")
)
public class useFilter implements Filter {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 1.请求处理
        HttpServletRequest request1 = (HttpServletRequest) request;
        String secureValue = request1.getHeader("secure");
        if (secureValue != null) {
            System.out.println(secureValue);

            // 2.请求放行
            // 将请求转给下一个过滤器,要是没有过滤器把请求转到资源。
            chain.doFilter(request, response);
        } else {
            // 3.响应处理
            response.setContentType("text/html;charset=utf-8");
            response.getWriter().println("请求头 Secure 缺失");
        }
    }
}

7.4 Filter 链

多个 Filter 对统一个资源进行过滤,这些 Filter 执行先后顺序是怎么样的?其实和单个没区别。

// 请求
Client -> Filter1 -> Filter2 -> Filter3 -> WebResource

// 响应
Client <- Filter1 <- Filter2 <- Filter3 <- WebResource

先创建三个 Filter。

filterChain1.java

import jakarta.servlet.*;

import java.io.IOException;

public class filterChain1 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Filter 1");
        chain.doFilter(request, response);
    }
}

filterChain2.java

import jakarta.servlet.*;

import java.io.IOException;

public class filterChain2 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Filter 2");
        chain.doFilter(request, response);
    }
}

filterChain3.java

import jakarta.servlet.*;

import java.io.IOException;

public class filterChain2 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Filter 3");
        chain.doFilter(request, response);
    }
}

先按 web.xml 配置。

<filter>
    <filter-name>filter-1</filter-name>
    <filter-class>filterChain1</filter-class>
</filter>
<filter-mapping>
    <filter-name>filter-1</filter-name>
    <url-pattern>/t</url-pattern>
</filter-mapping>

<filter>
    <filter-name>filter-2</filter-name>
    <filter-class>filterChain2</filter-class>
</filter>
<filter-mapping>
    <filter-name>filter-2</filter-name>
    <url-pattern>/t</url-pattern>
</filter-mapping>

<filter>
    <filter-name>filter-3</filter-name>
    <filter-class>filterChain3</filter-class>
</filter>
<filter-mapping>
    <filter-name>filter-3</filter-name>
    <url-pattern>/t</url-pattern>
</filter-mapping>

访问 /t 资源成功按照顺序执行 Filter,说明是按照 <filter-mapping> 先后顺序执行,谁在前谁先执行

Filter 1
Filter 2
Filter 3

我们将 filter-2 放到最后面,看看会不会切换顺序。

<filter>
    <filter-name>filter-1</filter-name>
    <filter-class>filterChain1</filter-class>
</filter>
<filter-mapping>
    <filter-name>filter-1</filter-name>
    <url-pattern>/t</url-pattern>
</filter-mapping>

<filter>
    <filter-name>filter-2</filter-name>
    <filter-class>filterChain2</filter-class>
</filter>

<filter>
    <filter-name>filter-3</filter-name>
    <filter-class>filterChain3</filter-class>
</filter>
<filter-mapping>
    <filter-name>filter-3</filter-name>
    <url-pattern>/t</url-pattern>
</filter-mapping>

<filter-mapping>
    <filter-name>filter-2</filter-name>
    <url-pattern>/t</url-pattern>
</filter-mapping>

重新访问 /p,发现 filterChain2 是最后执行。

Filter 1
Filter 3
Filter 2

注解配置过滤器执行顺序按照类名排序来确定先后,没有元素控制先后,具体排序规则是按照类名在字典中排序位置字典 a-z, 0-9确定。但是取个前缀 filter01_xxxx 做表标识可控制先后。由于规则有不确定性,有要求使用多个过滤器的场景最好还是配 web.xml

8. Listener

监听器是应用在应用域、会话域、请求域这三大作用域上的,只要监听到状态改变自动做出响应,状态由 CRUD 事件驱动。比如类加载的时候就优先执行静态代码块,加载就是创建事件,而做出响应的就是静态代码快,这个可以理解为监听器。

应用域:

会话域:

请求域:

三个作用域都有域的创建和销毁,属性的添加、删除、修改操作,而且方法操作都是一样的,学一个等于学三个。这里已应用域为例演示方法使用。

创建 useContextListener.java。

import jakarta.servlet.ServletContextAttributeListener;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.ServletContextAttributeEvent;
import jakarta.servlet.ServletContextEvent;

public class useContextListener implements ServletContextListener, ServletContextAttributeListener {
    /**
     * 应用域销毁时被调用。
     *
     * @param sce Information about the ServletContext that was destroyed
     */
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("应用域被销毁");
    }

    /**
     * 应用域被创建时调用
     *
     * @param sce Information about the ServletContext that was initialized
     */
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("应用域被创建");
    }

    /**
     * 应用域添加属性被调用
     *
     * @param scae Information about the new attribute
     */
    @Override
    public void attributeAdded(ServletContextAttributeEvent scae) {
        System.out.println("应用域添加了属性:" + scae.getName() + ",值是:" + (String) scae.getValue());
    }

    /**
     * 应用域删除属性被调用
     *
     * @param scae Information about the removed attribute
     */
    @Override
    public void attributeRemoved(ServletContextAttributeEvent scae) {
        System.out.println("应用域删除了属性:" + scae.getName() + ",值是:" + (String) scae.getValue());
    }

    /**
     * 应用域替换属性值被调用
     *
     * @param scae Information about the replaced attribute
     */
    @Override
    public void attributeReplaced(ServletContextAttributeEvent scae) {
        System.out.println("应用域属性:" + scae.getName() + "值被替换,新值是:" + (String) scae.getValue());
    }
}

编写完还不行,有些监听器需要注册才能使用,怎么确认哪些需要手动配置启用?只要接口文档中提到 the implementation class must be configured in the deployment descriptor for the web application,都需要手动配置。梳理完以下接口都需要注册:

  • jakarta.servlet.http.HttpSessionAttributeListener
  • jakarta.servlet.http.HttpSessionListener
  • jakarta.servlet.http.HttpSessionIdListener
  • jakarta.servlet.ServletContextAttributeListener
  • jakarta.servlet.ServletContextListener
  • jakarta.servlet.ServletRequestAttributeListener
  • jakarta.servlet.ServletRequestListener

注册的配置方法同样是有 web.xml 和注解两种,先看 web.xml。

<listener>
    <!--填类的全限定名-->
    <listener-class>useContextListener</listener-class>
</listener>

而注解是最简单的,直接在监听器类上添加 @WebListener 即可,在实际编写中推荐替代 web.xml 使用。

@WebListener
public class useContextListener implements ServletContextListener, ServletContextAttributeListener {
......

尝试启动 Tomcat 和停止运行,确实自动执行了监听器代码,而且这个监听器中的方法不需要我们手动调用,全由 Tomcat 自动调,这种调用机制由观察者设计模式也叫监听器设计模式实现。

Connected to server
[2024-06-16 11:28:06,746] Artifact servlet07:Web exploded: Artifact is being deployed, please wait...
应用域被创建
......
应用域被销毁
Disconnected from server

应用域的属性设置这里通过一个 Servlet 实现,通过任意请求方法访问 / 完成属性设置、替换、删除操作。

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;

@WebServlet("/")
public class setContextAttribute extends HttpServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
        ServletContext servletContext = servletRequest.getServletContext();
        
        // 设置属性
        servletContext.setAttribute("tz", "1");
        
        // 替换属性
        servletContext.setAttribute("tz", "1");
        
        // 删除属性
        servletContext.removeAttribute("tz");
    }
}

访问后成功触发 ServletContextAttributeListener 接口中的方法。

应用域添加了属性:tz,值是:1
应用域属性:tz值被替换,新值是:1
应用域删除了属性:tz,值是:1

那监听器中的方法和生命周期中的方法那个优先调用?通过测试 jakarta.servlet.Servlet.Servlet 的 service 方法和 jakarta.servlet.ServletRequestListener 接口的 requestInitialized 方法,发现 Servlet 被请求时监听器会优先触发。

最近更新:

发布时间:

摆哈儿龙门阵