参考 Spring 官方文档:
https://docs.spring.io/spring-framework/docs/5.2.25.RELEASE/spring-framework-reference/web.html#spring-web
1、前置知识:ServletContainerInitializer
在 Servlet3.0 及之后,提供了一个接口:ServletContainerInitializer(Servlet容器的初始化器)
用于提供给第三方组件一个机会,对 Servlet 容器做一些初始化的工作
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
【大白话总结】:
Web容器(Tomcat)会调用 ServletContainerInitializer 的 onStarup 方法对Servlet容器进行初始化,而这些 ServletContainerInitializer 的实现,由第三方提供
问题:
Web容器如何发现这些 ServletContainerInitializer 的实现?-----> 使用的是SPI机制
SPI机制
-
ServletContainerInitializer 服务调用方:Web容器(Tomcat)
利用 ServiceLoader,查找当前项目环境下,所有 jar 包 /META-INF /service 文件夹,是否有一个 javax.servlet.ServletContainerInitializer 文件
-
ServletContainerInitializer 服务提供方:Spring框架
在 org.springframework:spring-web 包下的 /META-INF /service 文件夹下,提供了一个 javax.servlet.ServletContainerInitializer 文件:
【大白话总结】:
利用SPI的发现机制,Web容器会调用SpringServletContainerInitializer的onStarup方法对Servlet容器进行初始化
2、SpringServletContainerInitializer
可以发现,类上@HandlesTypes注解中标注了 WebApplicationInitializer.class
,(感兴趣的的类)
那么 Tomcat 就会将所有 WebApplicationInitializer(Web应用初始化器) 的实现类(包括抽象类),传到 SpringServletContainerInitializer # onStartup() 方法的参数上
@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) {
/*
筛选出满足下面三个条件的 Class 类,实例化后,加入到initializers集合
条件:"不是接口类型" && "不是抽象类" && "是WebApplicationInitializer的实现类"
*/
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
// 实例化,加入到initializers集合
initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
//...
}
}
}
//...
// 遍历这些 WebApplicationInitializer,调用它们的 onStartup() 方法
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
可以发现,在SpringServletContainerInitializer # onStartup()中会调用 WebApplicationInitializer 的 onStartup() 方法
3、 编写自己的 WebApplicationInitializer
Web应用初始化器的实现可以有两种方式:
3.1、方式一:直接实现 WebApplicationInitializer 接口(无父子容器,不推荐):
public class MyAppStarter implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// 创建SpringWeb容器 context
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
// 注册SpringWeb容器的配置类 SpringMVCConfig
context.register(SpringMVCConfig.class);
// 创建并注册 DispatcherServlet 对象
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet); //将 DispatcherServlet 添加到 ServletContext 域中
registration.setLoadOnStartup(1);
registration.addMapping("/"); //设置 DispatcherServlet 的访问路径,这里设置拦截所有请求
}
}
以上的 onStartup() 方法:
①创建了SpringWeb容器,这里创建的 SpringWeb 容器类型是 AnnotationConfigWebApplicationContext
②利用 创建好的SpringWeb容器 构建DispatcherServlet: DispatcherServlet servlet = new DispatcherServlet(context);
那么之后Tomcat在初始化 DispatcherServlet 时,进入 init 逻辑,会在 FrameworkServlet # initServletBean() 中进行 SpringWeb 容器的初始化…(initWebApplicationContext())。详情查看【DispatcherServlet的初始化(二)】
3.2 方式二:继承 AbstractAnnotationConfigDispatcherServletInitializer (有父子容器,推荐)
public class MyQuickAppStarter extends AbstractAnnotationConfigDispatcherServletInitializer {
/*
父容器:Spring根容器的配置
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{ SpringConfig.class }; //SpringConfig(负责Service、Dao层的配置)
}
/*
子容器:SpringWeb容器的配置
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{ SpringMVCConfig.class }; //SpringMVCConfig(负责Controller层的配置)
}
/*
映射路径
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"}; //拦截所有请求
}
}
【方式二的原理】WebApplicationInitializer 体系结构
分别给三个重写方法打断点:观察函数栈:
因为 WebApplicationInitializer 的 onStartup() 方法是调用入口,从 onStartup() 方法开始分析:
一、Spring根容器的创建
1、调用入口: onStartup() 方法最后一次重写发生在 AbstractDispatcherServletInitializer 中
2、进入父类:
3、创建Spring根容器、并利用创建好的根容器构造ContextLoaderListener(【详情查看ContextLoaderListener】)
4、创建Spring根容器
二、SpringWeb容器的创建
创建SpringWeb容器、并用创建好的SpringWeb容器构建DispatcherServlet(【详情查看:DispatcherServlet的初始化(二)】)
创建SpringWeb容器
总结
在使用JavaConfig的方式配置SpringMVC应用时,底层是借助 Spring 提供的 SpringServletContainerInitializer 来介入到Web容器的初始化过程的。
在这个过程中,可以通过实现 WebApplicationInitializer 接口,来完成DispatcherServlet的初始化,以及Spring容器(父与子)的创建