【1】ServletContainerInitializer是什么
在web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册servlet或者filtes
等,servlet规范中通过ServletContainerInitializer
实现此功能。
每个框架要使用ServletContainerInitializer
就必须在对应的jar包的META-INF/services
目录创建一个名为javax.servlet.ServletContainerInitializer
的文件,文件内容指定具体的ServletContainerInitializer
实现类,那么,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作。
一般伴随着ServletContainerInitializer
一起使用的还有HandlesTypes
注解,通过HandlesTypes
可以将感兴趣的一些类
注入到ServletContainerInitializerde的onStartup
方法作为参数传入。
Tomcat容器的ServletContainerInitializer机制的实现,主要交由Context容器和ContextConfig监听器共同实现。
ContextConfig
监听器负责在容器启动时读取每个web应用的WEB-INF/lib
目录下包含的jar
包的META-INF/services/javax.servlet.ServletContainerInitializer
,以及web根目录下的META-INF/services/javax.servlet.ServletContainerInitializer(
即直接在src下建立的META-INF/services/javax.servlet.ServletContainerInitializer
),通过反射完成这些ServletContainerInitializer
的实例化,然后再设置到Context
容器中。
Context
容器启动时就会分别调用每个ServletContainerInitializer的onStartup
方法,并将感兴趣的类作为参数传入。
基本的实现机制如上图
-
首先通过
ContextConfig
监听器遍历每个jar包或web根目录的META-INF/services/javax.servlet.ServletContainerInitializer
文件,根据读到的类路径实例化每个ServletContainerInitializer
。 -
然后再分别将实例化好的
ServletContainerInitializer
设置进Context
容器中。 -
最后
Context
容器启动时分别调用所有ServletContainerInitializer
对象的onStartup
方法。
如下图所示,在org/springframework/spring-web/5.3.1/spring-web-5.3.1.jar!/META-INF/services/
下就配置了文件javax.servlet.ServletContainerInitializer
【2】@HandlesTypes注解与SpringServletContainerInitializer
① @HandlesTypes注解
Servlet 3.0+后,该注解用于声明一组类,这些类将会被传递给javax.servlet.ServletContainerInitializer使用
。注解源码如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SuppressWarnings("rawtypes") // Spec API does not use generics
public @interface HandlesTypes {
Class[] value();
}
即, 容器启动的时候会将@HandlesTypes
指定的这个类型下面的子类(实现类,子接口等)
传递过来。
② SpringServletContainerInitializer
package org.springframework.web;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = Collections.emptyList();
if (webAppInitializerClasses != null) {
initializers = new ArrayList<>(webAppInitializerClasses.size());
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)
ReflectionUtils.accessibleConstructor(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;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
代码解释如下:
- ① 如果
webAppInitializerClasses
不为null,则逐个进行遍历; - ② 是
WebApplicationInitializer
类型&&非接口&&非抽象类,则进行实例化然后将实例放入List<WebApplicationInitializer>
; - ③ 如果initializers为空,也就是没有发现
WebApplicationInitializer
实现,则打印日志No Spring WebApplicationInitializer types detected on classpath
,然后结束方法。 - ④ 如果
initializers
不为空,则进行排序,然后调用每个实例的onStartup
方法;
下面是SpringServletContainerInitializer 的onStartup方法的javadoc
将ServletContext
委派给应用程序中所有WebApplicationInitializer
实现类(类和子接口)。因为SpringServletContainerInitializer
声明了注解@HandlesTypes(WebApplicationInitializer.class)
,在Servlet 3.0+容器下会自动扫描类路径下WebApplicationInitializer
的所有实现,然后将所有实现的类型作为方法的入参webAppInitializerClasses
。
如果没有发现WebApplicationInitializer
的实现,则该方法将毫无作为。仅仅会打印日志No Spring WebApplicationInitializer types detected on classpath
告诉用户ServletContainerInitializer
的确已经指向但是没有发现WebApplicationInitializer
任何实现。
如果有一个或多个WebApplicationInitializer
实现被检测到,其将会按照顺序被实例化(如果声明了@Order
注解或者实现了Ordered
接口)。然后将会按照顺序调用每一个WebApplicationInitializer
实现的onStartup(ServletContext)
方法。把ServletContext委派给WebApplicationInitializer
实例时为了方便其注册配置servlets
诸如springmvc
的DispatcherServlet
,监听器诸如ContextLoaderListener
或者其他 Servlet API组件如filters。
【3】实现ServletContainerInitializer并注册三大组件
① 创建文件
如下图所示:
直接在src下面建立META-INF/services/javax.servlet.ServletContainerInitializer
。
② 实现类
//容器启动的时候会将@HandlesTypes指定的这个类型下面的子类(实现类,子接口等)传递过来;
//传入感兴趣的类型;
@HandlesTypes(value={HelloService.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {
/**
* 应用启动的时候,会运行onStartup方法;
*
* Set<Class<?>> arg0:感兴趣的类型的所有子类型;
* ServletContext arg1:代表当前Web应用的ServletContext;一个Web应用一个ServletContext;
*
* 1)、使用ServletContext注册Web组件(Servlet、Filter、Listener)
* 2)、使用编码的方式,在项目启动的时候给ServletContext里面添加组件;
* 必须在项目启动的时候来添加;
* 1)、ServletContainerInitializer得到的ServletContext;
* 2)、ServletContextListener得到的ServletContext;
*/
@Override
public void onStartup(Set<Class<?>> arg0, ServletContext sc) throws ServletException {
// TODO Auto-generated method stub
System.out.println("感兴趣的类型:");
for (Class<?> claz : arg0) {
System.out.println(claz);
}
//注册组件 ServletRegistration
ServletRegistration.Dynamic servlet = sc.addServlet("userServlet", new UserServlet());
//配置servlet的映射信息
servlet.addMapping("/user");
//注册Listener
sc.addListener(UserListener.class);
//注册Filter FilterRegistration
FilterRegistration.Dynamic filter = sc.addFilter("userFilter", UserFilter.class);
//配置Filter的映射信息
filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
}
}
测试如下: