ServletContainerInitializer 是 Tomcat 容器给用户提供的一个可拓展接口,可以往 ServletContext 容器中塞一些东西。
Tomcat 是通过 SPI 机制(最后解释什么了 SPI)加载的此类的实现的,并且还会去解析注解 @HandlesTypes标注的类,获取到这个类所有的实现子类放到一个 Set 集合中,所以说第三方想要扩展此类,就必须尊守这个 SPI 开发规范。
Servlet 其实是一种 Web 规范、Tomcat 其实是遵守 Servlet 规范的一种实现
SpringServletContainerInitializer 类是 Spring 遵守了 Tomcat SPI 规范对 Tomcat 的 ServletContainerInitializer 接口的一个子类实现,该类存在于 spring-web 模块中,可以从这个模块中发现有一个以包路径命名的文件:
META-INF/services/javax.servlet.ServletContainerInitializer
配置内容如下:
org.springframework.web.SpringServletContainerInitializer
如下图示:

接下来看看这个类详细信息如下:
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
/**
* 该 onStartup() 方法会被 tomcat 服务启动调用
* 这个方法主要是为 Web 容器
* 1、添加 WebServlet 请求
* 2、添加 WebFilter 过滤器
* 3、添加 WebListener 监听器
* 参数1:webAppInitializerClasses 表示加载被注解 @HandlesTypes(WebApplicationInitializer.class) 修饰的接口的所有子类实现,
* 主要有一下三类重要的子类:
* 1.AbstractContextLoaderInitializer ---> 对应 ContextLoaderListener
* 2.AbstractDispatcherServletInitializer --> 对应 DispatcherServlet
* 2.1.AbstractAnnotationConfigDispatcherServletInitializer
* 3.AbstractReactiveWebInitializer --> 对应 React
*
* 参数2:servletContext servlet 容器上下文
*/
@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) {
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);
// initializers 里面会包含我们自己定义的 WebApplicationInitializer 子类实现
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
@HandlesTypes 注解还是 Tomcat SPI 规范中的哦,会被 Tomcat 解析,也就是说会解析被注解 @HandlesTypes 修饰的 WebApplicationInitializer 接口的所有子类,注意此时的 WebApplicationInitializer 接口是由 Spring 提供的,这个接口就是 Spring 整合 SpringMVC 的核心入口, Spring 有三个较重要的实现子类:
主要有一下三类重要的子类:
1.AbstractContextLoaderInitializer ---> 对应 ContextLoaderListener
2.AbstractDispatcherServletInitializer --> 对应 DispatcherServlet
2.1.AbstractAnnotationConfigDispatcherServletInitializer --> 对应注解配置版的 DispatcherServlet
3.AbstractReactiveWebInitializer --> 对应 React
实现了 WebApplicationInitializer 接口的所有子类会被 Tomcat 扫描到然后放到 SpringServletContainerInitializer 类中的 onStartup() 方法中的第一个参数,也就是 Set 集合中,Tomcat 在启动时会调用 SpringServletContainerInitializer 的 onStartup() 方法(因为 SpringServletContainerInitializer 实现了 Tomcat ServletContainerInitializer 接口),然后再遍历调用 WebApplicationInitializer 接口的所有子类(上述的三类重要子类在这里就被回调了)的 onStartup() 方法,
总之一句话,你实现了 WebApplicationInitializer 接口就会被 Tomcat 再调用 SpringServletContainerInitializer 类的 onStartup() 方法时遍历调用到每个 WebApplicationInitializer 子类的 onStartup() 方法。
在 WebApplicationInitializer 接口中我们可以把 SpringMVC 的组件准备好,代码如下:
@ComponentScan(value = "com.gwm.demo")
@Configuration
public class SpringConfig {
}
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
/**
* mvc_tag2: 注意这里虽然 new 了一个 IOC 容器,但是此时还没有调用 refresh() 方法哦
* 所以现在 IOC 容器中都是控制,只有一个 AppConfig.class
*/
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringConfig.class);
/**
* mvc_tag3: 创建 DispatcherServlet 并把 context 传进去了,注意这是一个 Servlet 哦,就会遵循 Servlet 生命周期方法
* Servlet 类的5个生命周期方法,那就先来看 init() 方法吧,从 DispatcherServlet 父类中看起吧
*
* Tomcat 启动的时候回去回调 Servlet 五个生命周期方法,然后再 init() 方法中 context.refresh() 启动 IOC 容器
*/
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/*");
}
}
注意下面这行代码只是 new 了一个 Web IOC 容器,此时还是空的里面,就只有一个 SpringConfig.class 类
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
真正启动 Spring IOC 容器是在 DispatcherServlet() 中的 init() 方法被调用的,因为 DispatcherServlet 也是个Servlet 那就是遵守 Servlet 规范,就得经历 Servlet 的生命周期,每个 Servlet 生命周期调用不同的方法做不同的事,Servlet 具体有五个:
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
//public ServletConfig getServletConfig();
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
//public String getServletInfo();
public void destroy();
}
总结:Spring 在整合 SpringMVC 时利用了 Tomcat SPI 可扩展接口,扩展了 ServletContainerInitializer 接口,在 WebApplicationInitializer 类中进行增强处理。
SPI 一种可扩展的接口规范,别人提供统一接口,然后不同的用户端实现,实现的方式都是不一样的,就比如 MySQL 数据库接口 Driver.class。
SPI 可以通过接口或者是类找到所有的子类,下面进行代码演示:
1、先定义一个接口或者说是父类
public interface ServiceLoaderInterface {
public void hello();
}
2、然后定义两个子类实现接口或者继承父类
public class ServiceLoadImpl1 implements ServiceLoaderInterface {
@Override
public void hello() {
System.out.println("我是 ServiceLoadImpl1");
}
}
public class ServiceLoadImpl2 implements ServiceLoaderInterface {
@Override
public void hello() {
System.out.println("我是 ServiceLoadImpl2");
}
}
3、通过 ServiceLoad.load(Class.class) 找到所有子类,但是应该怎么找到这些子类呢?肯定是有一个地方去告诉这个 ServiceLoad 类的,SPI 就是固定写死了一个文件路径(规范),并且文件名称一定是这个接口或者是父类的全路径名称,如下图是:

文件夹名称叫做:
META-INF/services/main.serviceload.demo.ServiceLoaderInterface
然后里面的内容是你的所有子类:
main.serviceload.demo.ServiceLoadImpl1
main.serviceload.demo.ServiceLoadImpl2
然后最后再通过调用 ServiceLoad.load(Class.class) 获取到所有的实现或者继承子类
public class TestDemo {
public static void main(String[] args) {
ServiceLoader<ServiceLoaderInterface> load = ServiceLoader.load(ServiceLoaderInterface.class);
load.forEach(e->{
e.hello();
});
}
}
输出结果如下所示:
我是 ServiceLoadImpl1
我是 ServiceLoadImpl2
Process finished with exit code 0