目录

简介

为 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 举例。

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 静态文件。

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。

第一个 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>

IDEA 开发 Servlet

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

创建空项目。

IDEA JavaWeb 创建项目.png

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

IDEA JavaWeb 项目下创建模块.png

给模块添加框架支持。

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

使用 Servlet 4.0,会自动创建 Servlet 目录结构和 web.xml。由于 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

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。

Instantiation Servlet(实例化 Servlet)

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

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

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

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

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

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

Servlet 被实例化

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

Initialization(初始化)

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

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

Tomcat 控制台输出。

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

Request Handling(处理请求)

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

Tomcat 控制台输出。

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

End of Service(结束服务)

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

Tomcat 控制台输出。

Web Container 关闭时会执行 destroy 方法

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 直接重写重载方法即可。

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 已经实现,也是它实现的思路。

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 对象。

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

输出。

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

ServletContext

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

ServleContext 对象在 init 阶段创建,存放所有在 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 配置有什么区别?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/ 下的目录,就是应用的文件夹名称。

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());
    }
}

输出。

WebApp 路径:/ServerletContext

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。

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 执行流程或者说是核心逻辑是固定的。

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

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

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方法

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> 即可。

HttpServletRequest

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

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

req 对象是 org.apache.catalina.connector.RequestFacade 对象,而 RequestFacade 类实现了 HttpServletRequest 接口。

public class RequestFacade implements HttpServletRequest

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

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

最近更新:

发布时间:

摆哈儿龙门阵