前置准备
0.Servlet规范
servlet规范规定,在servlet启动时,要通过ServletContainerInitializer处理所有@HandlesTypes中标注的类,而ServletContainerInitializer是一个接口。
而ServletContainerInitializer的实现类是Tomcat通过SPI机制加载的。
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;
}
1.SPI机制
JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。
根据以上的知识,我们可以找到SpirngMVC实现了ServletContainerInitializer的类:SpringServletContainerInitializer
//可以看到SpringServletContainerInitializer 对WebApplicationInitializer感兴趣,它只处理WebApplicationInitializer相关的类
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public SpringServletContainerInitializer() {
}
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
//暂时省略
}
}
}
再来看看WebApplicationInitializer长什么样。
public interface WebApplicationInitializer {
void onStartup(ServletContext var1) throws ServletException;
}
可以看到它和ServletContainerInitializer 类似,都是一个接口,且都有一个onStartup方法,区别在于WebApplicationInitializer的方法参数有两个:一个class集合,一个servlet上下文。而WebApplicationInitializer的参数只有一个即servlet上下文。
这里不难猜到SpringServletContainerInitializer的实现类会遍历class集合中的所有类,并调用它们的onStartup方法。
3.环境准备
实验采用Spring及SpringMVC版本均为5.3.1
1.首先写一个配置类,用来配置包扫描路径
@ComponentScan("com.example")
@Configuration
public class WebConfig {
}
2.根据spring官方文档的写法,写一个WebApplicationInitializer的实现类
public class MyApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//新建一个IOC容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
//注册配置类
context.register(WebConfig.class);
//新建DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/*");
}
}
正文
通过前文我们知道Tomcat在启动时会通过SPI机制加载实现了ServletContainerInitializer的类,所以我们的探究就从SpringServletContainerInitializer开始。
打上断点,我们可以发现传入的类的集合里有了我们自己写的实现类。它可能是通过与@HandlesTypes 的某种机制实现的,这里先不探究,接着来看SpringServletContainerInitializer的源码
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public SpringServletContainerInitializer() {
}
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
//准备一个空集合用来存放WebApplicationInitializer的实现类
List<WebApplicationInitializer> initializers = Collections.emptyList();
Iterator var4;
if (webAppInitializerClasses != null) {
//实例化
initializers = new ArrayList(webAppInitializerClasses.size());
//迭代
var4 = webAppInitializerClasses.iterator();
while(var4.hasNext()) {
//得到webAppInitializerClasses中的一个class对象
Class<?> waiClass = (Class)var4.next();
//若该对象若满足 1.不是接口 2. 不是抽象类 3.继承/实现自waiClass
//这三个条件,则将它放入提前准备的List中。
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
//通过反射将class实例化并放入list中
((List)initializers).add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());
} catch (Throwable var7) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
}
}
}
}
//所有的WebApplicationInitializer实现类均已被放入list中
if (((List)initializers).isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
} else {
servletContext.log(((List)initializers).size() + " Spring WebApplicationInitializers detected on classpath");
//按照AnnotationAwareOrderComparator来进行排序,这里不细看
AnnotationAwareOrderComparator.sort((List)initializers);
var4 = ((List)initializers).iterator();
//遍历所有的WebApplicationInitializer实现类,调用它们的onStartUp方法
while(var4.hasNext()) {
WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
initializer.onStartup(servletContext);
}
}
}
}
紧接着来到我们自己写的实现类
public class MyApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//新建一个IOC容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
//注册配置类
context.register(WebConfig.class);
//新建DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/*");
}
}
按照官方的写法,我们首先自己准备了一个AnnotationConfigWebApplicationContext 从名字和继承树我们可以看到它是一个IOC容器。
在 之前的文章中我们知道了在容器实例化的时候会进行容器刷新的12大步,但这里似乎什么都没有做?
这里先留个坑,我们接着往下看。紧接着是进行DispatcherServlet 的配置,就像我们之前在web.xml文件中配置类似,只不过现在将其用编码的方式实现了。
我们注意到在实例化DispatcherServlet时把IoC容器传过去了,我们可以推测容器刷新的十二大步一定与DispatcherServlet有关。
换个角度来思考,DispatcherServlet归根结底是个Servlet,既然是servlet就必然要遵守servlet的init->service->destroy的生命周期。显然,容器的初始化最有可能发生在初始化阶段。我们可以从这个角度切入,看看DispatcherServlet做了什么事情。首先来看DispatcherServlet的继承树
从树结构一个个向上找,我们在HttpServletBean中找到了init()方法。
public final void init() throws ServletException {
//首先会读取配置,当我们通过web.xml形式来配置SpringMVC时,就是在这里读取到了我们配置的
//SpringMVC的配置文件相关的信息。
PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
this.initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
} catch (BeansException var4) {
if (this.logger.isErrorEnabled()) {
this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
}
throw var4;
}
}
//模板方法留给子类实现
this.initServletBean();
}
来到HttpSevletBean的子类FrameworkServlet
我们可以看到,该方法的前后都是打印了一些相关日志,真正重要的是中间两行
this.webApplicationContext = this.initWebApplicationContext();
this.initFrameworkServlet();
protected final void initServletBean() throws ServletException {
this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = this.initWebApplicationContext();
this.initFrameworkServlet();
} catch (RuntimeException | ServletException var4) {
this.logger.error("Context initialization failed", var4);
throw var4;
}
if (this.logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ? "shown which may lead to unsafe logging of potentially sensitive data" : "masked to prevent unsafe logging of potentially sensitive data";
this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
}
if (this.logger.isInfoEnabled()) {
this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
来到initWebApplicationContext()方法,这里涉及一个父子容器的问题,这里先不讨论。
按照我们的写法这里应该是只有一个IOC容器的。
protected WebApplicationContext initWebApplicationContext() {
//拿到根容器,我们这里只有一个容器,根容器为空
WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
WebApplicationContext wac = null;
//这个webApplicationContext 及我们手动实例化的容器
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
//重点 配置并刷新容器
this.configureAndRefreshWebApplicationContext(cwac);
}
}
}
//省略部分代码
return wac;
}
来到configureAndRefreshWebApplicationContext()方法
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
if (this.contextId != null) {
wac.setId(this.contextId);
} else {
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(this.getServletContext().getContextPath()) + '/' + this.getServletName());
}
}
//,可以看到先向容器中配置了一些东西(servle上下文,servlet配置等)
wac.setServletContext(this.getServletContext());
wac.setServletConfig(this.getServletConfig());
wac.setNamespace(this.getNamespace());
wac.addApplicationListener(new SourceFilteringListener(wac, new FrameworkServlet.ContextRefreshListener()));
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment)env).initPropertySources(this.getServletContext(), this.getServletConfig());
}
//模板方法,我们可以实现该方法来在容器刷新前对容器做一些操作。
this.postProcessWebApplicationContext(wac);
//我们可以实现ApplicationContextInitializer类在容器刷新之前对容器做一些操作
this.applyInitializers(wac);
//熟悉的refresh方法
wac.refresh();
}
web容器的刷新与之前提到过的容器刷新略有不同,下一篇文章再来详解。对容器刷新没有概念的读者可以看看这篇文章。