WebApplicationInitializer向左,ServletContextInitializer向右

2022-12-01 21:41:13 浏览数 (1)

为什么要写这篇文章呢?因为笔者在读Spring相关源码时,发现WebApplicationInitializerServletContextInitializer拥有相同的方法签名,作用也基本一致,可不明白它俩的使用场景有啥区别,要不Spring Boot怎么会又单独设计一个ServletContextInitializer出来呢?

1 写在前面

web.xml是Servlet规范中用来描述如何在Servlet容器中部署Java Web应用的一种部署描述符文件,它一般位于war包的WEB-INF/目录下。Servlet与Filter是web.xml中最核心的内容,换言之,web.xml的主要作用就是帮助Java Web应用构建URLs与Servlet、Filter的映射关系,web.xml的主要内容如下所示。

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_3_1.xsd" version="3.1">
    <!-- ========================================================== -->
    <!-- Context Parameters -->
    <!-- ========================================================== -->
    <context-param>
        <param-name>debug</param-name>
        <param-value>true</param-value>
    </context-param>

    <!-- ========================================================== -->
    <!-- Servlets -->
    <!-- ========================================================== -->
    <servlet>
        <servlet-name>Simple</servlet-name>
        <servlet-class>com.example.crimson_typhoon.servlet.SimpleServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Simple</servlet-name>
        <url-pattern>/servlet/SimpleServlet</url-pattern>
    </servlet-mapping>

    <!-- ========================================================== -->
    <!-- Filters -->
    <!-- ========================================================== -->
    <filter>
        <filter-name>Set Character Encoding</filter-name>
        <filter-class>com.example.crimson_typhoon.filter.SimpleCharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>Set Character Encoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- ========================================================== -->
    <!-- Listeners -->
    <!-- ========================================================== -->
    <listener>
        <listener-class>com.example.crimson_typhoon.listener.SimpleContextListener</listener-class>
    </listener>
</web-app>

在Servlet API 3.0之前,Java Web应用只能依赖web.xml来定义Servlet、Filter和Listener等组件;但随着Servlet API 3.0的发布,Servlet、Filter和Listener等组件的声明方式朝着声明式这一方向演进,也就是说web.xml可以抛之脑后了。那么Servlet API 3.0究竟作出了哪些改进以支持这种深受开发者喜爱的声明式风格呢?绝非仅仅是引入了@WebServlet@WebFilter@WebListener@WebInitParam等注解接口那么简单,这些声明式注解接口只是表象而已;笔者认为有两个改进比较重要:

  1. ServletContext类中新增了addServlet()addFilter()addListener()等方法;
  2. 新增了javax.servlet.ServletContainerInitializer接口,它有两个实现类:分别是spring-web模块中的SpringServletContainerInitializer和spring-boot模块中的TomcatStarter,如下所示:

关于上述两点,第一点是很容易理解的,因为ServletContext是与Servlet容器交互的门户,通过它才能向Servlet容器存取数据,要想以硬编码的方式向Servlet容器添加Servlet、Filter和Listener等组件,添加这些方法是必须的,否则一切免谈。关于第二点,也许大家觉得直接通过ServletContext对象调用addServlet()等方法就可以了,没必要再引入一个ServletContainerInitializer接口;非也非也!一个ServletContext对象往往对应着一个Java Web应用,它是由Servlet容器创建的,开发者要想获取这个全局对象并不是很方便,于是才有ServletContainerInitializer接口一说,它的内容如下:

代码语言:javascript复制
package javax.servlet;

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

ServletContainerInitializer不仅是一个函数式接口还是一个标准的服务供应商拓展接口(SPI),其onStartup()方法会由Servlet容器调用。如果第三方实现了ServletContainerInitializer接口,并且在其META-INF/目录下的services文件中声明了该实现类,那么Servlet容器可以借助JDK的ServiceLoader探测到所有第三方实现类,然后Servlet容器将ServletContext对象依次传递给第三方实现类的onStartup()方法(不用头疼ServletContext对象的获取问题了,Servlet容器直接传给你,前提是你要实现我的SPI拓展接口)。关于ServletContainerInitializer接口中onStartup()方法的第一个参数是本文的重点,主要有两种,分别是:

  • org.springframework.web.WebApplicationInitializer
  • org.springframework.boot.web.servlet.ServletContextInitializer

下面分别对它们进行介绍。

2 WebApplicationInitializer

WebApplicationInitializer接口位于spring-web模块中,内容如下:

代码语言:javascript复制
package org.springframework.web;

public interface WebApplicationInitializer {
 void onStartup(ServletContext servletContext) throws ServletException;
}

当前Java Web应用有两种部署模式,一是将Java Web应用打成war包,然后将其置于外部Servlet容器中运行,这种模式在SSH时代较为常用;另一种是将Java Web应用打成jar包,其内嵌Servlet容器,直接通过java -jar命令来启动,如基于Spring Boot开发的Java Web应用常常会内嵌Tomcat这一Servlet容器。WebApplicationInitializer接口是Spring为第一种部署模式量身打造的一个接口,即它只能应用于外置Servlet容器中,大家可以在Intellj IDEA中DEBUG运行一个Spring Boot应用试试,压根执行不到它。

WebApplicationInitializer由谁调用呢?答案是SpringServletContainerInitializer,SpringServletContainerInitializer会配合javax.servlet.annotation.HandlesTypes注解接口收集所有WebApplicationInitializer的实现类,然后将其传给自己的onStartup()方法;此外,无需将WebApplicationInitializer接口的实现类声明为Bean哈。

代码语言:javascript复制
package org.springframework.web;

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        List<WebApplicationInitializer> initializers = Collections.emptyList();
        if (webAppInitializerClasses != null) {
            initializers = new ArrayList<>(webAppInitializerClasses.size());
            for (Class<?> waiClass : webAppInitializerClasses) {
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer) ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    } catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }
}

3 ServletContextInitializer

ServletContextInitializer接口位于spring-boot模块中,内容如下:

代码语言:javascript复制
package org.springframework.boot.web.servlet;

public interface ServletContextInitializer {
 void onStartup(ServletContext servletContext) throws ServletException;
}

既然spring-web模块中已经有了WebApplicationInitializer接口,那Spring Boot为什么还要另起炉灶转而又搞一个ServletContextInitializer出来呢?笔者在Spring Boot官方文档中找到了相关描述,如下:

Embedded servlet containers do not directly execute the servlet 3.0 ServletContainerInitializer interface or Spring’s WebApplicationInitializer interface. This is an intentional design decision intended to reduce the risk that third party libraries designed to run inside a war may break Spring Boot applications. If you need to perform servlet context initialization in a Spring Boot application, you should register a bean that implements the ServletContextInitializer interface.

从官方文档的描述看,基于Spring Boot的Java Web应用会内嵌Servlet容器,如果沿用外置容器(Servlet容器 ==> ServletContainerInitializer ==> WebApplicationInitializer)那一套会给Spring Boot应用带来一定风险,至于啥风险,咱就不知道了。注意,与WebApplicationInitializer不同,必须将ServletContextInitializer接口的实现类声明为Bean哈。

Spring Boot所内嵌的Servlet容器并不会以SPI这种方式去加载ServletContainerInitializer,而是间接通过TomcatStarter触发,具体如下:

代码语言:javascript复制
package org.springframework.boot.web.embedded.tomcat;

class TomcatStarter implements ServletContainerInitializer {
    private final ServletContextInitializer[] initializers;

    TomcatStarter(ServletContextInitializer[] initializers) {
        this.initializers = initializers;
    }

    @Override
    public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
        try {
            for (ServletContextInitializer initializer : this.initializers) {
                initializer.onStartup(servletContext);
            }
        } catch (Exception ex) {
            if (logger.isErrorEnabled()) {
                logger.error("Error starting Tomcat context. Exception: "   ex.getClass().getName()   ". Message: "   ex.getMessage());
            }
        }
    }
}

在上一小节提到:SpringServletContainerInitializer可以在HandlesTypes的配合下收集所有WebApplicationInitializer的实现类,然后将其传给自己的onStartup()方法;那TomcatStarter的是如何收集ServletContextInitializer实现类的呢?从上述源码来看,TomcatStarter暴露了一个含参构造方法,期望外部通过该含参构造方法将ServletContextInitializer的实现类传进来;TomcatStarter的调用者会传进来一个ServletWebServerApplicationContext$lambda,然后TomcatStarter在执行其onStartup()方法时,会触发ServletWebServerApplicationContext的selfInitialize()方法进行ServletContextInitializer实现类的收集,具体内容如下:

代码语言:javascript复制
package org.springframework.boot.web.servlet.context;

public class ServletWebServerApplicationContext extends GenericWebApplicationContext
        implements ConfigurableWebServerApplicationContext {
    private ServletContextInitializer getSelfInitializer() {
        return this::selfInitialize;
    }

    private void selfInitialize(ServletContext servletContext) throws ServletException {
        prepareWebApplicationContext(servletContext);
        registerApplicationScope(servletContext);
        WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
        for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
            beans.onStartup(servletContext);
        }
    }

    protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
        return new ServletContextInitializerBeans(getBeanFactory());
    }
}

分析上述源码后,可以断定ServletContextInitializer实现类的收集工作由ServletContextInitializerBeans的构造方法承担,具体如下:

代码语言:javascript复制
package org.springframework.boot.web.servlet;

public class ServletContextInitializerBeans extends AbstractCollection<ServletContextInitializer> {
    private final MultiValueMap<Class<?>, ServletContextInitializer> initializers;

    private final List<Class<? extends ServletContextInitializer>> initializerTypes;

    private List<ServletContextInitializer> sortedList;

    public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
                                          Class<? extends ServletContextInitializer>... initializerTypes) {
        this.initializers = new LinkedMultiValueMap<>();
        this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
                : Collections.singletonList(ServletContextInitializer.class);
        addServletContextInitializerBeans(beanFactory);
        addAdaptableBeans(beanFactory);
        List<ServletContextInitializer> sortedInitializers = this.initializers.values()
                .stream()
                .flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
                .collect(Collectors.toList());
        this.sortedList = Collections.unmodifiableList(sortedInitializers);
    }
}

其中,addServletContextInitializerBeans()方法负责收集ServletContextInitializer的直系实现类,比如:ServletRegistrationBean、DispatcherServletRegistrationBean、FilterRegistrationBean和开发者自定义的实现类等;addAdaptableBeans()方法负责收集Servlet、Filter和Listener本体,然后将其适配为对应的ServletRegistrationBean、FilterRegistrationBean和ServletListenerRegistrationBean。这些RegistrationBean后缀的类都是ServletContextInitializer的子类,这一点可以回过头看一下本节开头贴出的关于ServletContextInitializer的继承关系图;另外,addServletContextInitializerBeans()和addAdaptableBeans()这俩方法的参数都是一个BeanFactory,这说明无论是Servlet、Filter和Listener本体还是ServletContextInitializer的直系实现类都需要是一个Bean才行,否则不会被找到。

最后,总结下Spring Boot中注册Filter的几种方式:方式一

代码语言:javascript复制
@Component
public class Filter1 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
    }
}

方式二

代码语言:javascript复制
@WebFilter   启动类@ServletComponentScan
public class Filter2 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
    }
}

方式三

代码语言:javascript复制
@Component
public class CustomServletContextInitializer implements ServletContextInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        Filter3 filter3 = new Filter3();
        FilterRegistration.Dynamic dynamic = servletContext.addFilter("filter3", filter3);
        dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, StringUtils.toStringArray(Lists.newArrayList("/crimson_typhoon/v1/*")));
    }
}

方式四

代码语言:javascript复制
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<Filter4> filterRegistration() {
        FilterRegistrationBean<Filter4> filterRegistrationBean = new FilterRegistrationBean<Filter4>();
        filterRegistrationBean.setFilter(new Filter4());
        filterRegistrationBean.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST));
        filterRegistrationBean.setUrlPatterns(Lists.newArrayList("/crimson_typhoon/v1/*"));
        return filterRegistrationBean;
    }
}

这里强烈推荐方式和方式,因为可以显示地指定Filter的url patterndispatcher type属性!!!其中在方式三和方式四中,方式四更贴近Spring风格,而方式三是Servlet API 3.0引入的一种原生方式;如果大家深挖方式四的话,其实底层原理依然是通过方式三实现的。

4 总结

WebApplicationInitializer与ServletContextInitializer虽然都用于以一种硬编码风格向Servlet容器注册Servlet、Filter和Listener组件,但却是Spring系Java Web应用的两种部署模式下的不同产物。

5 参考文档

  1. https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.servlet.embedded-container.context-initializer

0 人点赞