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注释(自译文)
如何使用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先加载谁的?
ServletContainerInitalizer只有一个方法onStartup
通知这个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方法。
这个注释用于声明ServletContainerInitializer可以处理的类类型。value值表示一个应用程序类数组,这些应用程序类将被传递给ServletContainerInitializer。
要知道为什么出现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!"); } }
假设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片段里使用注解方式注册的(或者是在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"); } }
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目录下,排序结果如下图:
由于我没找到官方给的相关这个绝对排序的说明(这个绝对排序有可能需要在哪里配置),不过我现在可以大胆的猜测到,如果我们没有指定绝对排序,那么默认排序加载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,有疑问就去追寻答案,望能与读者共勉。