ServletContainerInitializer深入剖析

ServletContainerInitializer深入剖析

 

 

 

思考:

ServletContainerInitizalizer是用来注册那些动态生成的servlet、listener、filter或没配置在web.xml里或jar包下的servlet等吗?

 

官方文档:

https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContainerInitializer.html

 

案例项目地址:

https://gitee.com/LiuDaiHua/servlet3.0

 

了解

文档注释

ServletContainerInitializer注释(自译文)

onStartup注释

HandlesTypes注解注释

Servlet3.0下大背景

如何使用ServletContainerInitializer

题外话

实战

最后一个疑问

 

 

 

了解

文档注释

public interface ServletContainerInitializer {

    /**
     * Receives notification during startup of a web application of the classes
     * within the web application that matched the criteria defined via the
     * {@link javax.servlet.annotation.HandlesTypes} annotation.
     *
     * @param c     The (possibly null) set of classes that met the specified
     *              criteria
     * @param ctx   The  of the web application in which the
     *              classes were discovered
     *
     * @throws ServletException If an error occurs
     */
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

 

 

ServletContainerInitializer注释(自译文)

该接口将在Web应用程序的启动阶段通知 ”库/运行时“,并允许这些”库或运行时“ 在启动阶段动态注册WEB三大组件

可以使用HandlesTypes对该接口的实现进行注释,该注解只有一个value属性,该属性接收的值是类数组,你可以使用该注解将需要进行扩展的应用程序集传递进来,这些传递进来的应用程序集将在onStartup方法的第一个参数中被接收,你可以在onStartup中对进行一些操作。例如像SpringMVC那样。

@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
     

 

   

当然如果该接口的实现没有使用HandlesTypes注解,或者没有一个应用程序类与注解中指定的类相匹配,那么容器会将一个空的类集传递给onStartup的第一个参数。当在检查应用程序类中是否存在该注解指定的类时,如果没找到,容器可能会遇到类加载问题。

该接口的实现必须在JAR文件中META-INF/services目录中声明,并以此接口的全限定类名称命名。例如:

 

在容器启动时将使用运行时服务发现机制(SPI)或语义上等同于SPI机制的特定于容器的机制来发现,无论是哪种情况,被排除在绝对顺序之外的 WEB片段JAR文件中的ServletContainerInitializer服务 必须被忽略,并且发现这些服务的顺序必须遵循应用程序的类加载委托模型。

可以通过WEB片段排序来控制每个JAR文件的ServletContainerIntializer服务执行顺序。如果定义了绝对排序,那么只有包含在排序中的jar才会被处理。要完全禁用该服务,可以定义一个空的绝对顺序。

 

思考:这个绝对排序在哪定义?如果有多个JAR都实现了ServletContainerInitializer先加载谁的?

 

onStartup注释

ServletContainerInitalizer只有一个方法onStartup

它有两个参数:

 第一个参数:满足指定条件的类集(可能为空)。

 第二个参数:web应用程序的ServletContext。

 

这个方法的注释如下:

通知这个ServletContainerInitializer启动。将传入表示应用程序的ServletContext。

如果这个ServletContainerInitializer的实现被绑定到应用程序的WEB-INF/lib目录的JAR文件中【意思是你在自己的JAR文件里META-INF/service/javax.servlet.ServletContainerInitializer中声明的ServletContainerInitializer的实现在自己的JAR文件中】,那么它的onStartup方法只会在绑定应用程序启动期间被调用一次。如果这个ServletContainerInitializer的实现绑定在任何WEB-INF/lib目录之外的JAR文件中【意思是你在自己的JAR文件里声明的ServletContainerInitializer的实现在不在自己的JAR文件中】,但是仍然像上面描述的那样可以发现,那么每次启动应用程序时都会调用它的onStartup方法。

 

 

HandlesTypes注解注释

在来看一下HandlesTypes注解注释:

这个注释用于声明ServletContainerInitializer可以处理的类类型。value值表示一个应用程序类数组,这些应用程序类将被传递给ServletContainerInitializer。

 

 

 

Servlet3.0下大背景

要知道为什么出现ServletContainerInitializer,你需要了解Servlet3.0到底发生了什么,因为这是一个Servlet3.0的接口。在Servlet3.0中一个重要的特征就是允许在容器启动的时候动态注册WEB三大组件,而在Servlet3.0文章中我们是使用监听器动态注册WEB三大组件的。但是如果我有一个web片段作为一个组件想提供其他人的项目使用,我们直接使用注解的方式注册能不能被其他人的项目扫描到呢?

如下:

假设我现在某一个web片段项目servlet-container-initializer-one,在这个项目(使用Servlet4.0)里,我只提供一个Servlet如下:

package com.wonders.servlet;

@WebServlet("/tankServlet")
public class TankServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("I'am Tank!");
    }
}

 

现在我将我的WEB项目打成一个JAR供他人使用。

 

假设A用户要使用我的WEB片段项目,他自己的WEB项目中只有一个HelloServlet如下:

@WebServlet("/HelloServlet")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("123");
    }
}

 

现在A用户要使用我的WEB片段,他需要在他项目的WEB-INF/lib目录下引入我的JAR即可【Servlet3.0新特性之组件可插性】,项目结构如下图:

 

 

现在测试,启动servlet-container-initializer-test看看能不能访问我WEB片段项目里的tankServlet。

 

 

启动信息里说扫描到了这个JAR,先访问A用户自己的Servlet:

 

如上图是没问题的,在访问我的WEB片段中的servlet:

 

可以看到也是没问题的,也就是说,只要我的WEB片段里使用注解方式注册的(或者是在web-fragment.xml里注册的,这两个本质是一样的),都会被扫描到,并加入容器之中,不管是注解方式还是在web-fragment.xml声明方式这两种注册WEB三大组件都称不上是动态注册,因为它们是运行时就已经存在了的,动态注册指的是那些运行时不存在,在运行时加入的WEB组件。对于运行时已经存在的只要把我的JAR包放入classpath下用户A就必须加载我JAR里注册的三大组件【强迫性让容器加载所有运行时已存在的WEB三大组件】。如果我不能使用注解和web-fragment.xml这两种方式提前写死,那怎么办?

Servlet3.0也早已考虑到这种情况,既然必须在运行时动态注册,那Servlet3.0就提供了动态注册WEB三大组件的方式,动态注册WEB三大组件的前提是必须在启动时注册。在Servlet3.0文章中我们是使用一个监听器去动态注册我们的WEB三大组件的,使用监听器监听容器启动事件,然后在注册所需的WEB组件或执行其他事情。到此貌似我们根本不需要这个ServletContainerInitializer,因为使用监听器就完全也可以做到Web应用程序的启动阶段通知 ”库/运行时“,并允许这些”库或运行时“ 在启动阶段动态注册WEB三大组件。那为什么Servlet设计者们又专门设计一个ServletContainerInitializer呢?而且还专门把它限定在JAR文件中使用?

到目前理解来看,我想大概是为了使整个软件的设计更加灵活吧,使用这么一个接口的好处是:在一个统一的入口处动态的注册WEB组件以及其他相关任务,避免了在xml中书写的烦杂,又避免了都使用注解时对开发者来说难以一眼概览整个项目,不使用Listener大概是因为解耦和单一职责。

 

 

 

如何使用ServletContainerInitializer

题外话

你要知道ServletContainerInitializer是一个规范,它规定了容器启动的时候必须扫描JAR包里去查找该接口的实现并执行该实现的onStartUp方法。也就是说像Tomcat、WebLogic、Jetty等这些servlet容器必须遵循这一规范。这个规范一边约定容器的开发商必须这么干,还约定开发人员必须在JAR文件的META-INF/service下创建javax.servlet.ServletContainerInitializer文件并在该文件内写上对该接口实现类的全限定名称。这样两边都对起来后,你使用Tomcat容器启动你的WEB项目时你lib目录下的JAR文件里该接口的实现才会被发现,然后tomcat会执行该实现类的onStartup方法。

 

实战

1、在刚才创建的servlet-container-initializer-one项目中先创建一个等待被动态注册的Servlet(注意它没有加注解,也没在web-fragment.xml中声明),我们马上要在ServletContainerInitializer的实现类的动态注册它:

package com.wonders.servlet;

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("one-MyServlet");
    }
}

 

2、然后创建ServletContainerInitializer的实现类:

package com.wonders;

@HandlesTypes({})
public class OneContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        System.out.println("OneContainerInitializer被调用了");
        ServletRegistration.Dynamic dynamic = ctx.addServlet("my-servlet", "com.wonders.servlet.MyServlet");
        dynamic.addMapping("/myServlet");
    }
}

 

在这个HandlesTypes中我们没有传入任何类。

 

3、然后开始重要的一步,创建META-INF/services/javax.servlet.ServletContainerInitializer文件:

 

注意是在src目录下,在该文件内写上对该接口的实现类的全限定名称:

 

 

com.wonders.OneContainerInitializer

 

4、打成JAR包,File -> Project Structure -> Artifacts -> + -> 选中JAR -> From Module ... -> 选中我们的servlet-container-initializer-one -> ok

 

 

注意在Output Layout列表下如果有很多和该jar包的依赖,但是在你将要导入的WEB项目里这些依赖已经有了比如说servlet-api.jar,jsp-api.jar等你可以删掉,然后点击Applay。

点击Build -> Build Artifacts -> 选中servlet-container-initializer:jar -> build即可。然后到刚才我们配置的Output directory目录下找这个JAR。

如上我导入到servlet3-java里了,找到这个jar包

 

5、在需要导入该JAR的WEB项目中创建WEB-INF/lib,将该jar包放入lib目录下,我的WEB项目是servlet-container-initializer-test如下:

 

6、启动servlet-container-initializer-test查看控制台是否输出我们OneContainerInitializer实现类onStartup里打印的那一句话:

 

虽然字符编码不对,但是打印出来就说明tomcat容器遵循了ServletContainerInitializer规范把我们的实现类OneContainerInitializer的onStartup执行了。

 

7、访问看看我们在onStartup方法里动态注册MyServlet能不能被访问:

 

可以看到能成功访问,说明我们动态注册的Servlet起效了。上图中我是使用Servlet4.0+Tomcat的HTTP/2协议,所以时https,端口是8443,如果不是在tomcat中配置该协议,请使用http和8080端口访问。

 

 

最后一个疑问

在上面文档注释章节抛出了一个疑问:

思考:这个绝对排序在哪定义?如果有多个JAR都实现了ServletContainerInitializer先加载谁的?

为了探索这个疑问,我又创建了一个WEB片段项目servlet-container-initializer-two

 

其大致内容和servlet-container-initializer-one类似,我们这次主要探索排序问题,将其也打成JAR包丢到servlet-container-initializer-test的lib目录下,新创建的JAR排序在servlet-container-initializer-one.jar之后。启动Tomcat,控制台得到如下图结果:

 

继续在创建了一个WEB片段项目,这次创建的项目名称打成的JAR要排序在lib目录下的servlet-container-initializer-one之前,所以我们创建servlet-container-initializer-before-one,内容也类似servlet-container-initializer-one,最后打成JAR包丢到lib目录下,排序结果如下图:

 

启动Tomcat容器,控制台打印结果如下:

 

由于我没找到官方给的相关这个绝对排序的说明(这个绝对排序有可能需要在哪里配置),不过我现在可以大胆的猜测到,如果我们没有指定绝对排序,那么默认排序加载ServletContainerInitializer实现类的规则是按照JAR包名称加载顺序进行加载的!

最后不甘心到此结束,自己尝试在web.xml输入order标签发现还真有一个<absolute-ordering>标签,一看这个名字我感觉自己肯定找对了,查看该标签可以发现它只有两个子标签name和other,然后查看官方文档找关于该标签的说明name指的是WEB片段项目web-fragment.xml中指定的name属性,它指定了WEB片段的加载顺序以及是否被加载。由于我之前创建的servlet-container-initializer-one、servlet-container-initializer-two、servlet-container-initializer-before-one这三个WEB片段项目都是省略了这个文件(在Servlet3.0中新增了web-fragment.xml,但是可以像省略web.xml一样省略该文件)。那没办法了,我们在之前的三个WEB片段里都指定一个web-fragment.xml吧,并给他们每一个都指定一个名字:分别叫one、tow、beforeOne

 

 

<?xml version="1.0" encoding="UTF-8"?>
<web-fragment id="WebFragment_ID" version="3.1" 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-fragment_3_1.xsd">
    <name>one</name>
</web-fragment>

 

重新打JAR包,然后丢到servlet-container-initializer-test的lib目录下,并在servlet-container-initializer-test的WEB-INF目录下新增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" metadata-complete="true">
    <absolute-ordering>
        <name>two</name>
        <name>one</name>
    </absolute-ordering>
</web-app>

 

这次我故意省略了beforeOne看它会不会被加载,然后故意调换了tow和one的加载顺序,看看它们会不会加载顺序发生变化。再次启动Tomcat容器,得到如下结果输出:

 

 

先加载了two后加载了one,没有在web.xml指定绝对排序的JAR不会被加载,果然如上面文档注释章节所诉。

至此一切真相大白,再回头想Servlet的设计者们为什么要创建ServletContainerInitializer现在也有点恍然大悟的感觉,SPI和API对比下来,SPI进行了解耦,而ServletContainerInitializer又是使用了PI机制的典型案例,当不需要引入这些动态注册的WEB组件时只需删除javax.servlet.ServletContainerInitializer即可,而不需要改变原有项目中的类,亦或许Servlet的设计者们考虑的更多。

关于ServletContainerInitializer的研究就先告一段落了。接下来将继续回到SpringBoot之内嵌式Tomcat源码的研究中去了,回忆走过了路线:springboot-java8-servlet3.0-servlet4.0-ServletContainerInitializer,有疑问就去追寻答案,望能与读者共勉。

 

 

posted @ 2020-12-21 19:26  刘呆哗  阅读(1572)  评论(0编辑  收藏  举报