Servlet 3.0 -- 用示例验证主要特性

249 阅读16分钟

Servlet3.0 是 Java Servlet 规范的一个版本,它引入了许多新特性和改进,使得开发者能够更轻松地构建灵活、高效的 Java Web 应用程序。以下是 Servlet 3.0 的一些主要特性:

  1. 注解支持:Servlet 3.0 引入了对注解的支持,开发者可以使用注解来配置 Servlet、Filter 和 Listener,而不再需要在 web.xml 文件中进行繁琐的配置。
  2. 异步处理:Servlet 3.0 支持异步处理请求,这意味着一个请求可以在一个线程中开始,然后在另一个线程中完成。这可以提高服务器的性能和吞吐量,特别是在处理长时间操作(如文件上传、长时间计算等)时。
  3. 动态注册:Servlet 3.0 允许在应用程序运行时动态地注册 Servlet、Filter 和 Listener,而不需要在部署描述符(web.xml)中配置。
  4. 文件上传 API:Servlet 3.0 提供了一个标准地文件上传 API,使得处理文件上传变得更加简单。开发者可以通过 HttpServletRequest 对象直接获取上传的文件内容。
  5. Servlet 容器初始化:Servlet 3.0 引入了 ServletContainerInitializer 接口,允许开发者在容器启动时执行一些初始化逻辑,而不需要在 web.xml 中配置。

war 包部署 Java Web 项目

在嵌入式 Servlet 容器还没有出现之前,我们还需要以 war 包的形式将 Java Web 项目部署到 Servlet 容器如 Tomcat 中,如今已经很少看到这种部署方式了,但是了解它的部署流程对我们理解嵌入式 Servlet 应用程序非常有帮助。

下面介绍在 idea 中使用 maven 构建 java web 项目的步骤,该示例的 gitee 地址如下:gitee.com/monkey12/se…

第一步,使用 maven archetype 提供的模板来创建 web 项目。

File -> New -> Project 创建项目,选择 Maven 选项,勾选 Create from archetype,选择 webapp 的项目模板,然后点击 next 创建项目即可。

image.png

创建的项目就是 Maven 构建工具下 Web 项目的标准目录结构,你会发现相比于普通的 Maven 项目,在 main 目录下多了一个 webapp 目录,其中部署描述符 web.xml 位于 src/main/webapp/WEB-INF 目录下。

image.png

第二步,在 pom.xml 文件中添加 servlet-api 的坐标,由于 maven archetype 生成的项目会自动添加一些初始化配置,我们可以删除掉。注意,pom.xml 文件中的顶级标签 <packaging> 是用来指定打包的类型的,这里我们需要指定成 war。最终的 pom.xml 长这样:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
​
  <groupId>org.example</groupId>
  <artifactId>servlet-webapp</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>
​
​
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
​
    <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.15.2</version>
    </dependency>
​
​
    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
​
  </dependencies>
​
  <build>
    <finalName>servlet-webapp</finalName>
  </build>
</project>

第三步,编写自己的 Servlet 类。

public class MyServlet extends HttpServlet {
​
​
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 设置响应内容类型
        resp.setContentType("text/html");
​
        // 获取输出流对象
        PrintWriter out = resp.getWriter();
​
        // 输出响应内容
        out.println("<html><body>");
        out.println("<h1>Hello, World!</h1>");
        out.println("</body></html>");
    }
}

第四步,在 web.xml 注册自定义 Servlet,最终的 web.xml 长这样:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app>
  <display-name>Archetype Created Web Application</display-name>
​
  <!--
    - Servlet that dispatches requests to the Spring managed block servlets
    -->
  <servlet>
    <servlet-name>myServlet</servlet-name>
    <servlet-class>com.niuma.MyServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
​
  <!-- URL space mappings ============================================= -->
​
  <servlet-mapping>
    <servlet-name>myServlet</servlet-name>
    <url-pattern>/myServlet</url-pattern>
  </servlet-mapping>
</web-app>

第五步,在 Project Structure 的 Mudules 选项卡中检查是否添加了 Web Framework,如果没有要手动添加,检查 web.xml 的路径。

image.png

第六步,点击 IDEA 右边 Maven 选项卡中的 package 生命周期或者使用命令 mvn package 将项目打成 war 包,加入打成的 war 包名为 servlet-webapp.war。

第七步,将 servlet-webapp.war 复制到 Tomcat 安装目录的 webapps 目录下,双击 startup.bat 启动 tomcat,然后在浏览器中访问项目中的 Servlet,你可以看到如下效果,注意,默认的上下文路径是 war 包的名字。

image.png

至此 war 包部署 Java Web 项目结束,当然,除了手动复制 war 之外,你还可以在 IDEA 中配置 Tomcat,那么第六步之后你还需要执行以下步骤。

在 Project Structure 中配置 Artifacts,如下图所示,选择 Web Application: Exploded,然后选择 From Modules,选择具体的 Modules。

image.png

最终的效果图如下:

image.png

然后还需要在 Idea 中添加 Local Tomcat,在Edit Configutations 中选择 TomEE Server 下的 Local。

image.png

主要的配置有两点,一个是 Tomcat 的安装路径,即 Application server,一个是 Deployment 中添加项目的 Artifacts,注意,在 Deployment 页签中,可以自定义 web 项目的上下文,但是它默认的名字并不是你项目的名字,也不是 war 的名字,在测试的时候需要保证上下文一致,否则会导致测试失败,推荐更改成项目的名字;最终的效果图如下:

image.png

最后点击 Run 按钮就会运行 tomcat,在浏览器输入同样的 url 会得到同样的效果,这里就不重复贴图了。

Web 项目目录结构和 War 包目录结构

我们使用 Maven Archetype 创建的 Web 项目目录结构如下:

project
|
|--pom.xml
|--src
|  |--main
|  |  |--java
|  |  |--resources
|  |  |  |--META-INF
|  |  |--webapp
|  |  |  |--WEB-INF
|  |  |  |  |--web.xml
|  |  |  |--index.html
|  |  |--test
|  |  |  |--java
|  |  |  |--resources

这个是开发者编写源代码时看到的目录结构,它面向开发者,但是经过 Maven 打包之后是一个 war 包,War 包的目录结构是在 Servlet 规范中定义的,War 包必须符合规范中指定的目录结构,Servlet 容器解析并加载该 War 包,所以 war 包目录结构是面向 Servlet 容器的。而程序在运行过程中看到的是 war 包的目录结构,War 包的目录结构如下:

my-web-app.war
|
|--META-INF/
|--WEB-INF/
|  |--web.xml
|  |--classes/
|  |--lib/
|  |--img/
|  |--css/
|  |--js/
|  |--index.html
  • META-INF:存放 WAR 包的元数据信息,例如 MANIFEST.MF 文件。

  • WEB-INF:Web 应用程序的核心目录,包含了 Web 应用程序的配置和类文件

    • web.xml:Web 项目的部署描述符
    • classes:存放 Java 类文件的目录
    • lib:存放应用程序所需的 JAR 文件,包括第三方库和自定义库

Tomcat 通过解析 War 包文件在内部构建一个 Context 对象,一个 Context 对象就是 Web 项目。

注解支持

注解支持是 Servlet3.0 非常重要的特性之一,接下来我们来验证一下这个特性,在上面的 servlet-webapp 项目的基础上,我们使用注解来注册一个 Servlet,注意这个 Servlet 不需要在 web.xml 中注册,它使用注解 @WebServlet 来注册。

@WebServlet(name = "myAnnotationServlet", urlPatterns = "/myAnnotationServlet")
public class MyAnnotationServlet extends HttpServlet {
​
​
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 设置响应内容类型
        resp.setContentType("text/html");
​
        // 获取输出流对象
        PrintWriter out = resp.getWriter();
​
        // 输出响应内容
        out.println("<html><body>");
        out.println("<h1>This is a Annotation Servlet</h1>");
        out.println("</body></html>");
    }
}

重新打包后在浏览器访问 http://localhost:8080/servlet-webapp/myAnnotationServlet,会得到如下页面,说明 Servlet 注册成功,Filter 和 Listener 类似就不演示了。但是我们不在 web.xml 中使用 <servlet> 标签来注册,直接启动 tomcat 后访问 /myAnnotationServlet 会得到如下结果:

image.png

文件上传 API

文件上传我们需要设置 Content-Type=multipart/form-data,在前端使用

上传文件的时候,需要设置属性 enctype=multipart/form-data,它会自动设置 Content-Type 的值,当 Content-Type=multipart/form-data 的时候,表示表单数据以多多部份(multipart)的形式进行提交,在这种情况下,请求体(request body)的格式较为复杂,它由多个部分组成,每个部分包含一个表单字段的数据。

如下所示使用 Postman 创建了一个 Post 请求,Content-Type=multipart/form-data,有两个域字段 name=bob,age=12,一个文件字段 filename。

image.png

它生成的 Request Body 如下所示:

----------------------------972035522375092745254752
Content-Disposition: form-data; name="name"
​
bob
----------------------------972035522375092745254752
Content-Disposition: form-data; name="age"
​
12
----------------------------972035522375092745254752
Content-Disposition: form-data; name="filename"; filename="cookies.txt"
Content-Type: text/plain
​
buvid3=F411E7C4-EA75-5589-E241-56FDABBB346725287infoc; nostalgia_conf=-1; _uuid=110C4D10FE-C42A-167A-D109C-67D1109DB4FAD25361infoc; buvid4=8C6582E8-D86B-82B5-ADA3-B39C92044D9F60926-023033009-bK5ysmawZ6Vmcp4FH1sAKg%3D%3D; CURRENT_FNVAL=4048; b_nut=100; CURRENT_PID=02807740-dc63-11ed-b9de-e17cb111eaac; rpdid=|(k|k)Rlmmkm0J'uY)uYllJJR; buvid_fp_plain=undefined; DedeUserID=60086572; DedeUserID__ckMd5=4d62e4736606c76e; i-wanna-go-back=-1; b_ut=5; FEED_LIVE_VERSION=V_SIDE_CARD_REFRESH; header_theme_version=CLOSE; home_feed_column=5; fingerprint=e5a21ef9590f5a40f7778ce3a05cbab7; buvid_fp=e5a21ef9590f5a40f7778ce3a05cbab7; PVID=1; browser_resolution=2560-1291; bsource=search_google; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTM4MTc5NzEsImlhdCI6MTY5MzU1ODc3MSwicGx0IjotMX0.lyUxHOXVWv9wAovt1EyO_VymvrZq18kEsXYhCQ_Sm9A; bili_ticket_expires=1693817971; SESSDATA=fafd9ee2%2C1709110774%2C1bee3%2A92pnpNHSaj6gEq3haFeUpet-2E0Az23Gr_lB0j1HucVwYB2h5SV5ajfkzkAVdVXeGBQrL9LgAASAA; bili_jct=6e56e5e8a5238b02c43a00ea12407633; sid=8ms3f7fo; b_lsid=23BD8273_18A5124D77A
----------------------------972035522375092745254752--

其中 ----------------------------972035522375092745254752 是边界(boundary),它用于分隔表单数据,每个字段都包含 Content-Disposition 头,name 表示字段的名称,如果是文件上传还会有 filename 字段和 Content-Type 头,filename 表示上传文件的文件名,Content-Type 表示文件的 MIME 类型,如上的 text/plain 表示上传的文件是一个文本文件。

需要注意到是,boundary 是一个随机生成的字符串,它通常附加在 HTTP 请求的 Content-Type 头中,如 multipart/form-data; boundary=972035522375092745254752

Servlet 3.0 上传文件 API 和核心接口是 Part 接口,Servlet 3.0 在 HttpServletRequest 接口中新增了 getParts() 方法来获取 Part 集合,我们创建一个 Servlet 来验证它:

@MultipartConfig(fileSizeThreshold = 1024 * 1024,    // 1 MB
        maxFileSize = 1024 * 1024 * 10,      // 10 MB
        maxRequestSize = 1024 * 1024 * 50)   // 50 MB
@WebServlet(name = "uploadServlet", urlPatterns = "/uploadServlet")
public class UploadServlet extends HttpServlet {
​
​
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
​
        // 获取所有上传的 Part
        Collection<Part> parts = null;
        try {
            parts = req.getParts();
​
            // 处理每个 Part
            for (Part part : parts) {
                // 获取字段名称
                String fieldName = part.getName();
                String submittedFileName = part.getSubmittedFileName();
                String contentType = part.getContentType();
​
                System.out.println("fieldName: " + fieldName);
                System.out.println("submittedFileName: " + submittedFileName);
                System.out.println("contentType: " + contentType);
            }
​
            resp.getWriter().println("Files uploaded successfully!");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ServletException e) {
            e.printStackTrace();
        }
​
    }
}

重新打包后部署 Tomcat,Postman 的配置如下:

image.png

你会得到如下输出:

fieldName: name
submittedFileName: null
contentType: null
fieldName: age
submittedFileName: null
contentType: null
fieldName: filename
submittedFileName: cookies.txt
contentType: text/plain

你可以通过判断 Part 的 Content-Type 是否为 null 来判断它是普通表单项还是文件上传。

值得注意的是,不要忘记给在 Servlet 类上添加 @MultipartConfig 注解,该注解用于指定上传文件的配置信息,包括文件的大小阈值、单个文件大小和整个请求的最大大小等,它是使用 Servlet 3.0 上传文件 API 必要的配置。这样,Servlet 容器就会按照这些配置来处理文件上传请求,否则会抛出 IllegalStateException 异常。当然,你也可以在 web.xml 部署描述符中通过 标签来配置。

异步处理

www.cnblogs.com/davenkin/p/…

在 Servlet 3.0 之前,Servlet 采用 Thread-Per-Request 的方式处理请求,即每一次 HTTP 请求都由某个线程从头到尾负责处理。如果一个请求需要进行 IO 操作,比如访问数据库、调用第三方服务接口等,那么其所对应的线程将同步地等待 IO 操作完成,而 IO 操作是非常满地,所以此时的线程并不能及时地释放回线程池以供后续使用,在并发量越来越大的情况下,这将带来非常严重的性能问题。比如 Tomcat 的线程池大小是 200,每个请求耗时越久线程就被占用越久,当线程被耗尽之后,新的请求就会被阻塞,如果我们把耗时的处理逻辑另起一个线程来执行,让 Tomcat 的线程及时的被放到线程池中去,那么就可以缓解这种情况下的发生。

需要注意的时,异步处理看起来缓解了 HTTP 请求被阻塞的风险,但是并没有提高应用程序的性能,因为新起的线程仍然会占用处理器资源,而且过多的线程还会增加上下文切换。

和之前一样,我们通过示例来验证一下异步处理,耗时处理逻辑的任务类:

public class MyAsyncTask implements Runnable {
​
    private AsyncContext asyncContext;
​
    public MyAsyncTask(AsyncContext asyncContext) {
        this.asyncContext = asyncContext;
    }
​
    public void run() {
        try {
            // 线程停顿 3s,模拟执行耗时操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
            // 完成异步处理
            asyncContext.getResponse().getWriter().println("Async task completed!");
            System.out.println("Async task completed!");
            // 通知 Servlet 容器请求执行完毕了
            asyncContext.complete();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
@WebServlet(name = "asyncServlet", urlPatterns = "/asyncServlet", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
​
​
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 启用异步处理
        AsyncContext asyncContext = req.startAsync(req, resp);
        // 在异步上下文中执行异步任务
        asyncContext.start(new MyAsyncTask(asyncContext));
​
        System.out.println("HTTP thread end...");
    }
}

从上面的例子看来,Servlet 3.0 的异步处理主要分为 1.获取异步上下文 AsyncContext;2.将 AsyncContext 传给新线程;3.请求执行完之后调用 asyncContext.complete(); 通知 Servlet 容器。

通过 Postman 请求 http://localhost:8080/servlet-webapp/asyncServlet,会发现请求在 3 秒之后才返回。

动态注册

www.blogjava.net/yongboy/arc…

www.cnblogs.com/duanxz/arch…

Servlet 3.0 动态注册允许开发者在 Web 应用程序初始化时动态注册 Servlet、Filter 和 Listener,有两种方法可以实现动态注册,分别是使用 ServletContextListener 和 ServletContainerInitializer。

为了演示这两种方式,我们先定义一个 Servlet,然后用这两种方式注册。

public class DynamicRegisterServlet extends HttpServlet {
​
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
​
        // 获取输出流对象
        resp.getWriter().write("Dynamic Register Success!");
​
    }
}

ServletContextListener 是 Listener 的一种,Servlet 规范定义了很多 Listener,如 ServletRequestListener 和 HttpSessionListener 等,它们都实现 EventListener 接口,使用 IDEA 查看 javax.servlet 包下的 EventListener 接口实现类有:

image.png

当使用 ServletContextListener 接口实现动态注册时,需要实现该接口,并在 contextInitialized() 方法中通过 ServletContext#addServlet 方法完成注册,该方法是 Servlet 3.0 新加入的。

@WebListener
public class MyServletContextListener implements ServletContextListener {
​
​
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext servletContext = sce.getServletContext();
​
        ServletRegistration.Dynamic registerServlet = servletContext.addServlet("dynamicRegisterServlet", DynamicRegisterServlet.class);
        registerServlet.addMapping("/dynamicRegisterServlet");
​
    }
}

重新部署后用 postman 访问 http://localhost:8080/servlet-webapp/dynamicRegisterServlet 会看到正确的返回,说明注册成功了。

当使用 ServletContainerInitializer 来实现动态注册时,需要实现该接口,在 onStartup() 方法中使用 ServletContext 来注册,应用程序初始化时会调用 onStartup() 方法,示例如下:

public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
​
        ServletRegistration.Dynamic registerServlet = ctx.addServlet("dynamicRegisterServlet", DynamicRegisterServlet.class);
        registerServlet.addMapping("/dynamicRegisterServlet");
​
    }
}

因为 Servlet 容器是使用 SPI 来发现 ServletContainerInitializer 实现类的,所以你需要在 src/main/resources/META-INF/services 目录下创建名为 javax.servlet.ServletContainerInitializer 的文件,并将实现类的全限定类名写入文件,文件内容如下:

com.niuma.MyServletContainerInitializer

在重新部署之前,不要忘记了注释到 ServletContextListener 实现类上的 @WebListener 注解,避免对验证结果造成干扰。重新部署之后,用 postman 请求 http://localhost:8080/servlet-webapp/dynamicRegisterServlet 会发现返回正确的结果。

你会发现这两种方法实现动态注册的代码都是通过获取 ServletContext 来委托 ServletContext 注册的,我们能想到还有其他获取 ServletContext 的方式,是不是只要能获取 ServletContext 的地方就能够动态注册呢?

除了 ServletContextListener 之外,获取 ServletContext 对象的方法还有:

  1. Servlet 的 getServletContext() 方法
  2. HttpServletRequest 的 getServletContext() 方法
  3. HttpSession 的 getServletContext() 方法
  4. ServletContextAttributeListener 的 attributeAdded() 方法

我们就用最简单的 Servlet#getServletContext() 方法来验证是否可以在 Servlet 中动态注册其他的 Servlet,这里提前剧透一些,是不能的,因为在一开始说的就是在应用程序初始化时动态注册,关于为什么,我也没有找到比较官方的说法,估计是为了安全考虑。

@WebServlet(urlPatterns = "/dynamicRegisterInServlet")
public class DynamicRegisterInServlet extends HttpServlet {
​
​
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext servletContext = getServletContext();
​
        ServletRegistration.Dynamic registerServlet = servletContext.addServlet("dynamicRegisterServlet", DynamicRegisterServlet.class);
        registerServlet.addMapping("/dynamicRegisterServlet");
​
    }
​
}

验证方式是先调用 /dynamicRegisterInServlet 动态注册 /dynamicRegisterServlet,然后调用 /dynamicRegisterServlet 验证结果,但是现实是在调用 /dynamicRegisterInServlet 时就会报错提示你不能在初始化之后动态注册。

ServletContainerInitializer

在之前讲解动态注册的时候就讲到过 ServletContainerInitializer 这个接口了,它是实现动态注册的一种方式,但是它不止动态注册这一种用法,而且 Spring 和 Spring MVC 框架频繁使用了这个接口,所以这个接口是非常重要的,有必要单独说一说它。

关于 ServletContainerInitializer 我们知道它是一个函数式接口,应用程序初始化时会调用它的 onStartup() 方法,该方法有两个参数,第二个参数 ServletContext 我们知道,但是第一个参数 Set<Class<?>> 是干什么的?

在 ServletContainerInitializer 实现类上可以添加 javax.servlet.annotation.HandlesTypes 注解;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlesTypes {
    
    Class<?>[] value();
​
}

该注解只有一个 Class[] 类型的属性 value,如果一个 ServletContainerInitializer 接口的实现类指定了该注解,并在注解中配置了类 Clazz,如果 Clazz 是抽象类/类,那么 Servlet 容器会在应用程序中收集 Clazz 的所有子类,如果 Clazz 是接口,那么 Servlet 容器会在应用程序中收集 Clazz 的所有实现类,如果 Clazz 是注解,那么 Servlet 容器会在应用程序中收集所有标注了 Clazz 的类,并传递给 ServletContainerInitializer#onStartup 方法,也就是该方法的第一个参数。

在动态注册小节中,ServletContainerInitializer 实现类并没有添加 @HandlesTypes 注解,所以 onStartup() 方法的 Set<Class<?>> c 参数将始终为 null。下面我们重新定义一个 ServletContainerInitializer 实现类并添加上 @HandlesTypes 注解来验证一下。

定义一个测试用的接口 MyInitializer.class 和它的实现类 MyInitializerDemo.class

public interface MyInitializer {
​
    void doSomething();
}
​
public class MyInitializerDemo implements MyInitializer {
​
    public void doSomething() {
        System.out.println("do something ....");
    }
​
}

定义 ServletContainerInitializer 的实现类,并将全限定类名添加到 javax.servlet.ServletContainerInitializer 文件中

@HandlesTypes(MyInitializer.class)
public class MyServletContainerInitializerWithHandlersType implements ServletContainerInitializer {
​
    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
​
        if (c != null) {
            for (Class<?> clazz : c) {
                // 在这里可以执行与应用程序初始化相关的逻辑
                if (MyInitializer.class.isAssignableFrom(clazz)) {
                    try {
                        MyInitializer initializer = (MyInitializer) clazz.newInstance();
                        initializer.doSomething();
                    } catch (IllegalAccessException | InstantiationException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

启动 Tomcat 你会发现控制台打印了 do something ....