为什么要写这篇文章呢?因为笔者在读Spring相关源码时,发现WebApplicationInitializer
与ServletContextInitializer
拥有相同的方法签名,作用也基本一致,可不明白它俩的使用场景有啥区别,要不Spring Boot怎么会又单独设计一个ServletContextInitializer出来呢?
web.xml
是Servlet规范中用来描述如何在Servlet容器中部署Java Web应用的一种部署描述符文件,它一般位于war
包的WEB-INF/
目录下。Servlet与Filter是web.xml中最核心的内容,换言之,web.xml的主要作用就是帮助Java Web应用构建URLs与Servlet、Filter的映射关系,web.xml的主要内容如下所示。
<?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
等注解接口那么简单,这些声明式注解接口只是表象而已;笔者认为有两个改进比较重要:
ServletContext
类中新增了addServlet()
、addFilter()
和addListener()
等方法;javax.servlet.ServletContainerInitializer
接口,它有两个实现类:分别是spring-web模块中的SpringServletContainerInitializer
和spring-boot模块中的TomcatStarter
,如下所示:关于上述两点,第一点是很容易理解的,因为ServletContext是与Servlet容器交互的门户,通过它才能向Servlet容器存取数据,要想以硬编码的方式向Servlet容器添加Servlet、Filter和Listener等组件,添加这些方法是必须的,否则一切免谈。关于第二点,也许大家觉得直接通过ServletContext对象调用addServlet()等方法就可以了,没必要再引入一个ServletContainerInitializer接口;非也非也!一个ServletContext对象往往对应着一个Java Web应用,它是由Servlet容器创建的,开发者要想获取这个全局对象并不是很方便,于是才有ServletContainerInitializer接口一说,它的内容如下:
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()
方法的第一个参数是本文的重点,主要有两种,分别是:
下面分别对它们进行介绍。
WebApplicationInitializer接口位于spring-web
模块中,内容如下:
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哈。
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);
}
}
}
ServletContextInitializer接口位于spring-boot
模块中,内容如下:
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
触发,具体如下:
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实现类的收集,具体内容如下:
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的构造方法承担,具体如下:
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的几种方式:方式一
@Component
public class Filter1 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
}
方式二
@WebFilter + 启动类@ServletComponentScan
public class Filter2 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
}
方式三
@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/*")));
}
}
方式四
@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 pattern
和dispatcher type
属性!!!其中在方式三和方式四中,方式四更贴近Spring风格,而方式三是Servlet API 3.0引入的一种原生方式;如果大家深挖方式四的话,其实底层原理依然是通过方式三实现的。
WebApplicationInitializer与ServletContextInitializer虽然都用于以一种硬编码风格向Servlet容器注册Servlet、Filter和Listener组件,但却是Spring系Java Web应用的两种部署模式下的不同产物。