Servlet3.0研究之ServletContainerInitializer接口

之所以对这个接口感兴趣, 主要是因为最近在研究Spring-Session. 在网上查找了相关的配置方式之后, 发现基本都是对Servlet3.0环境下ServletContainerInitializer(简称SCI)接口的使用.

1. 定义

// 完整命名: javax.servlet.ServletContainerInitializer
public interface ServletContainerInitializer {
    public void onStartup(Set<Class<?>> c, ServletContext ctx)
        throws ServletException; 
}

2. 概述

  1. ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册FilterServlet以及Listener,以取代通过web.xml配置注册。这样就利于开发内聚的web应用框架.
  2. 我们以SpringMVC举例, servlet3.0之前我们需要在web.xml中依据Spring的规范新建一堆配置。这样就相当于将框架和容器紧耦合了。而在3.x后注册的功能内聚到Spring里,Spring-web就变成一个纯粹的即插即用的组件,不用依据应用环境定义一套新的配置。

3. 原理

  1. ServletContainerInitializer接口的实现类通过java SPI声明自己是ServletContainerInitializer 的provider.
  2. 容器启动阶段依据java spi获取到所有ServletContainerInitializer的实现类,然后执行其onStartup方法.
  3. 另外在实现ServletContainerInitializer时还可以通过@HandlesTypes注解定义本实现类希望处理的类型,容器会将当前应用中所有这一类型(继承或者实现)的类放在ServletContainerInitializer接口的集合参数c中传递进来。如果不定义处理类型,或者应用中不存在相应的实现类,则集合参数c为空.
  4. 这一类实现了 SCI 的接口,如果做为独立的包发布,在打包时,会在JAR 文件的 META-INF/services/javax.servlet.ServletContainerInitializer 文件中进行注册。 容器在启动时,就会扫描所有带有这些注册信息的类(@HandlesTypes(WebApplicationInitializer.class)这里就是加载WebApplicationInitializer.class类)进行解析,启动时会调用其 onStartup方法——也就是说servlet容器负责加载这些指定类, 而ServletContainerInitializer的实现者(例如Spring-web中的SpringServletContainerInitializer对接口ServletContainerInitializer的实现中,是可以直接获取到这些类的)
    这里以spring-web举例

4. 与类加载的区别

  1. 类加载是根据限定的名称去加载,并没有相关的标准去加载未知的内容.
  2. 而SCI(全称 ServletContainerInitializer)则是根据约定的标准,扫描META-INF中包含注册信息的 class 并在启动阶段调用其onStartup.

5. SpringMVC中的应用

通过查看ServletContainerInitializer继承层级, 可以发现spring-web中的SpringServletContainerInitializer正是实现了ServletContainerInitializer接口(观察上面那张图也说明了这一点);

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

        List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

        // webAppInitializerClasses 就是servlet3.0规范中为我们收集的 WebApplicationInitializer 接口的实现类的class
        // 从webAppInitializerClasses中筛选并实例化出合格的相应的类
        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                // Be defensive: Some servlet containers provide us with invalid classes,
                // no matter what @HandlesTypes says...
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer) waiClass.newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }

        // 这行代码说明我们在实现WebApplicationInitializer可以通过继承Ordered, PriorityOrdered来自定义执行顺序
        AnnotationAwareOrderComparator.sort(initializers);
        servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers);

        // 迭代每个initializer实现的方法
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }

}
  1. SpringServletContainerInitializer 由支持Servlet3.0+的Servlet容器实例化并调用.
  2. Servlet容器还会查询classpath下SpringServletContainerInitializer类上修饰的@HandlesTypes注解所标注的WebApplicationInitializer接口的实现类. 这一步也是容器帮我们完成的.
  3. SpringServletContainerInitializer通过实现ServletContainerInitializer将自身并入到Servlet容器的生命周期中, 并通过自身定义的WebApplicationInitializer将依赖于Spring框架的系统初始化需求与Servlet容器解耦. 即依赖于spring的系统可以通过实现WebApplicationInitializer来实现自定义的初始化逻辑. 而不需要去实现ServletContainerInitializer

6. Tomcat调用SCI的时机

现在还有一个疑问, 就是 ServletContainerInitializer 的调用时机?, 因为servlet容器除了会回调SCI之外, 还有回调诸如servlet, listener等. 搞清楚这些先后顺序可以帮助我们快速定位和理解某些奇怪的问题.

这里我们就以Tomcat举例, 以下逻辑总结于Tomcat7.x, 有兴趣的读者可以去StandardContext类中对startInternal的实现中(第5608行 —— 第5618行, 这也是Tomcat中唯一的调用ServletContainerInitializers接口的onStartup方法的位置)求证下:

  1. 解析web.xml
  2. ServletContext实例中注入<context-param> 参数
  3. 回调Servlet3.0的ServletContainerInitializers接口实现类
  4. 触发 Listener 事件(beforeContextInitialized, afterContextInitialized); 这里只会触发 ServletContextListener 类型的
  5. 初始化 Filter, 调用其init方法
  6. 加载 启动时即加载的servlet
  1. Tomcat 中 的可插拔以及 SCI 的实现原理 — 强烈推荐!

猜你喜欢

转载自blog.csdn.net/zzy7075/article/details/108213722