DispatcherServlet的初始化原理

首先咱先研究一下 DispatcherServlet 的初始化阶段,都干了哪些工作,下一章我们再研究程序运行期间,DispatcherServlet 如何处理请求并响应的。

1. 基于web.xml的DispatcherServlet初始化

前面我们在刚刚开始学习 WebMvc 的时候,我们只配置了两样东西:一个 web.xml ,一个 spring-mvc.xmlweb.xml 中注册了 DispatcherServletspring-mvc.xml 负责初始化 DispatcherServlet 。那问题就来了,DispatcherServlet 在初始化的时候都干了什么,这是我们要研究的东西了。

1.1 init

我们都知道,一切 Servlet 的生命周期,都是从 init 方法开始,DispatcherServlet 也不例外。借助 IDE 往上翻,可以发现 DispatcherServletinit 方法在父类 HttpServletBean 中重写了:

@Override
public final void init() throws ServletException {
    // Set bean properties from init parameters.
    // ......
    // Let subclasses do whatever initialization they like.
    initServletBean();
}

上面的初始化逻辑我们不关心了,关键的点在于下面的这个模板方法 initServletBean ,这个才是关键!

initServletBean 方法要来到 DispatcherServlet 的直接父类 FrameworkServlet 中,这里有初始化的逻辑:

@Override
protected final void initServletBean() throws ServletException {
    // log ......
    long startTime = System.currentTimeMillis();

    try {
        this.webApplicationContext = initWebApplicationContext();
        initFrameworkServlet();
    } // catch ......

    // logger ......
}

刨去超多的日志打印不看,它的核心就两句话:一个初始化 IOC 容器,一个初始化 Servlet 本身。由于 initFrameworkServlet 方法中没有任何逻辑,DispatcherServlet 也没有重写它,所以核心还是初始化 IOC 容器。下面咱进入 initWebApplicationContext 方法。

1.2 initWebApplicationContext

初始化 DispatcherServlet 之前,先把属于它的 IOC 容器先初始化出来,以此来形成父子容器,这部分的源码比较长,小册只截取最关键的部分:

纵观整段源码,在第一次初始化的时候很明显上面的乱七八糟分支都是不走的,它会走中间偏下部分的 createWebApplicationContext 方法,在此初始化 IOC 容器。我们继续跟进源码:

1.2.1 createWebApplicationContext

进来 createWebApplicationContext 方法,发现它继续往下调用重载的方法:

继续往下,发现这里面有正儿八经的逻辑了,咱先把这段源码大体浏览一下,这里面有一些关键的环节,我们需要着重去看。

有了注释,看起来这段代码是不是非常简单了,核心就三步:实例化 IOC 容器、设置配置文件路径配置、刷新 IOC 容器

不过这里面有很多细节,我们逐一来看。

1.2.2 IOC容器的真正落地实现

首先上面的第一行,getContextClass 方法,它会获取到 WebApplicationContext 的真正落地实现,跳转进该方法,会发现它引用的是一个叫 XmlWebApplicationContext 的家伙:

是不是感觉有点似曾相识?之前,我们在 IOC 进阶,介绍 BeanFactory 的实现类中,提到过一个 XmlBeanFactory ,咱当时在那里说,XmlBeanFactory 这个东西不要去看了,因为已经没有在用了。但这个 XmlWebApplicationContext 不一样,它跟 ClassPathXmlApplicationContext 是差不多的,只是一个在 Web 环境下使用,一个主要在普通项目环境中使用。

接下来的配置文件路径的获取,就是利用 XmlWebApplicationContext 的东西,咱继续往下来看。

1.2.3 获取配置文件的路径

String configLocation = getContextConfigLocation(); 这一句代码中,获取的就是我们之前在 web.xml 中配置的那个 **contextConfigLocation** 属性,配置了它,这里就能拿到配置文件的路径;如果不配置的话,它也有一套默认规则,这个默认规则我们可以在 XmlWebApplicationContext 中看到一个重写的 getDefaultConfigLocations 方法:

可见这个默认的规则,就是我们在 68 章,SpringWebMvc 的入门中提到的 /WEB-INF/{servlet-name}-servlet.xml ,这里提到的 getNamespace() 获取到的,是刚才在 FrameworkServlet 中定义的一个方法:

所以,看到了这个套路了吧,把 Servlet 的 name 拿过来,后缀拼上 -servlet ,这样就形成配置文件的路径了。

至于这个配置文件在哪里用,咱下面会遇到的,莫慌莫着急。

1.2.4 刷新WebAppliCationContext

当上面的 WebAppliCationContext 都配置好之后,下面的最关键步骤,就是刷新 IOC 容器了。来到 configureAndRefreshWebApplicationContext 方法,这里面又是比较长,不过不太值得分段看了,咱总体扫一眼吧,看看里面的关键步骤:

看这里面的逻辑,总体还都是挺容易理解的吧!很明显,对于一个 WebAppliCationContext 而言,它应该持有 Servlet 相关的东西,所以上面的部分很理所当然。

中间它添加了一个 ContextRefreshListener ,注意它来自 spring-webmvc 包,它就是监听我们之前在事件的章节中学习的 ContextRefreshedEvent 事件!至于这个监听器都干了什么,接下来咱马上就会说到。

最底下,它执行 IOC 容器的 refresh 方法,那它就会去加载、解析配置文件,以及初始化的逻辑等等等等。

注意一点!此时 **WebAppliCationContext** 中是没有 **DispatcherServlet** !所以 IOC 容器的初始化,是无法控制和操作 DispatcherServlet 的,加上咱上面说到了,initFrameworkServlet 方法里面没有任何初始化逻辑,所以如果 DispatcherServlet 还需要执行额外的初始化逻辑,那就不得不使用其他的方案来触发。

那咋触发呢?刚才不是刚刚说到一个监听器嘛,这个 ContextRefreshListener ,就是让 IOC 容器重新跟 DispatcherServlet 打交道的 “桥梁” 。

1.3 ContextRefreshListener

在进入监听器的源码之前,先想一个问题。

1.3.0 前置思考:此时 IOC 容器初始化到哪一步了?

此时正在执行的,是 ContextRefreshedEvent 事件的发布动作吧!此时第 11 步 **finishBeanFactoryInitialization** 方法已经执行完毕了吧,IOC 容器中的 bean 也都初始化完成了吧!( **ContextRefreshedEvent** 事件的发布,是在第 12 步 **finishRefresh** 方法中广播的)

所以,当 ContextRefreshedEvent 事件广播的时候,我们就应该知道,此时所有的 bean 都准备就绪。但是!因为我们在之前的初体验中,根本什么东西都没有注册,甚至连一个 <mvc:annotation-driven /> 标签,或者 @EnableWebMvc 注解都没有写,所以此时 IOC 容器中应该只有一个 **InternalResourceViewResolver** ,其余的什么 **HandlerMapping** **HandlerAdapter** 统统都没有!

那问题就来了,这些东西都没有注册,咋就好使了呢?

好,带着这个问题,我们可以来看 ContextRefreshListener 的源码了。

1.3.1 ContextRefreshListener的源码

这个 ContextRefreshListener 定义在 FrameworkServlet 的内部,而正是因为定义在内部,所以刚好能拿到 DispatcherServlet 的实例。这个监听器的触发调用的逻辑,就是 FrameworkServletonApplicationEvent 方法:

很明显,接下来又是模板方法,跳转到 DispatcherServlet 了。

1.3.2 DispatcherServlet的onRefresh

注意这个地方到了非常非常重要的点了!!!高能预警!!!

可以发现,这个刷新逻辑中, **DispatcherServlet** 需要的所有组件,全部都初始化了一遍!所以各位,能猜到里面干了什么了吧!肯定是我们没有初始化哪些组件,这个 **initStrategies** 方法就会帮我们初始化

下面我们来挑一个比较重要的组件来看看吧,小册就挑 HandlerMapping 吧。

1.3.3 initHandlerMappings

首先我们来看 HandlerMapping 的初始化:

首先上面的逻辑中,它会先从 IOC 容器中,寻找那些类型为 HandlerMapping 的 bean ,借助 BeanFactoryUtils.beansOfTypeIncludingAncestors 方法,可以从子容器一路爬到最顶层父容器,来搜寻所有指定类型的 bean 。如果全部搜寻完毕后,发现没有任何 HandlerMapping ,则会调用最底下的 getDefaultStrategies 方法,使用默认的策略初始化 HandlerMapping

1.3.4 【默认策略】getDefaultStrategies

这个默认策略相当重要,一起来看源码是如何设计的:

可以发现!中间 for 循环中初始化了一组 Class<?> ,而对应的 classNames 是由 value 分割而来,而 value 又是根据上面的一个 **Properties** 类型的 **defaultStrategies** 变量而来!而这个 defaultStrategies 的初始化,是走的静态代码块:

注意看!它加载了 classpath 下的一个名为 “DispatcherServlet.properties” 的文件!而这个文件,来自于 spring-webmvc 的 jar 包:

img

打开这个 properties 文件,发现里面定义了刚才在 initStrategies 方法中初始化的所有 WebMvc 的核心组件的落地实现!

可见,即便我们什么也不注册,也能使用默认的策略,加载这些 SpringWebMvc 认为必需的组件。

由 properties 文件可知,默认的 HandlerMapping 会初始化以下三个实现:

  • org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping

  • **org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping**

  • org.springframework.web.servlet.function.support.RouterFunctionMapping

刚好有我们之前说的最重要的 RequestMappingHandlerMapping ,所以我们不需要任何配置,就可以实现 @Controller + @RequestMapping 的注解式 WebMvc 开发。

所以,读取完配置文件后,在上面的 getDefaultStrategies 方法中,就可以拿到上面的 3 个实现类,并实例化并注册进 IOC 容器,这样就完成了默认策略的加载。

1.4 小结

使用 **web.xml** 配置 **DispatcherServlet** 的时候,即便我们不配置任何内置组件,SpringWebMvc 可以通过预先定义好的内置组件,在 IOC 容器已经初始化接近完毕的时候,借助事件监听,回调 **DispatcherServlet** 使其初始化默认组件的落地实现,从而达到兜底配置。

2. 基于Servlet3.0规范的DispatcherServlet初始化

基于 Servlet 3.0 规范的 SpringWebMvc 工程,咱之前说过,它的入口是通过 WebApplicationInitializer 来的。我们在初体验中编写了一个 AbstractAnnotationConfigDispatcherServletInitializer 的子类,来声明根容器和 DispatcherServlet 子容器的配置类,并且指定了 DispatcherServletservlet-mapping ,就完成了 WebMvc 的配置。

这其中,我们也是只配置了一个 InternalResourceViewResolver ,其余的肯定都跟上面一样,执行了 DispatcherServletinitStrategies 方法做了兜底初始化。不过我们应该好奇的是,AbstractAnnotationConfigDispatcherServletInitializer 是怎么由 WebApplicationInitializer 来一步一步把根容器和 WebMvc 子容器都初始化好的呢?

2.1 AbstractContextLoaderInitializer

要说到这个,那就必须来看 WebAppliCationInitializer 的第一个抽象实现类了,它首先实现了 WebAppliCationInitializer 接口的 onStartUp 方法:

只不过这看上去,是为了注册之前在 web.xml 中初始化的那个 ContextLoaderListener 监听器,那里面的逻辑一定是初始化根容器咯,但是这似乎跟 DispatcherServlet 没啥关系。。。

不过要注意,IDEA 在此处给了我们提示,这个 onStartUp 方法,还有重写过的子类:

img

好,那咱就点击这个向下的 override 图标,跳转到一个叫 AbstractDispatcherServletInitializer 的子类。

2.2 AbstractDispatcherServletInitializer

这个 AbstractDispatcherServletInitializer 重写的 onStartUp 中,除了调用上面 AbstractContextLoaderInitializer 父类的 onStartUp 初始化根容器之外,还注册了 DispatcherServlet

进入到 registerDispatcherServlet 方法:

可以发现,这整个流程还是很容易理解的吧!首先初始化 WebMvc 的子容器,然后把 DispatcherServlet 创建出来,最后注册进 ServletContext ,完事。而初始化 WebMvc 子容器的逻辑,就定义在我们刚才说的 AbstractAnnotationConfigDispatcherServletInitializer 中了:

这里面我们要重写的方法,就是这个获取配置类的 getServletConfigClasses 方法。拿到配置类,就可以初始化 WebMvc 的子容器了,后面的逻辑也就跟上面分析的一样了。

小结

好,最后我们总结一下。

使用 **web.xml** 配置 **DispatcherServlet** 的时候,即便我们不配置任何内置组件,SpringWebMvc 可以通过预先定义好的内置组件,在 IOC 容器已经初始化接近完毕的时候,借助事件监听,回调 **DispatcherServlet** 使其初始化默认组件的落地实现,从而达到兜底配置。

基于 Servlet 3.0 规范的方式,通过编写继承 **AbstractAnnotationConfigDispatcherServletInitializer** 的子类,分别提供根容器与 WebMvc 子容器的配置类,底层会帮我们初始化对应的容器,并且借助 **ServletContext** 注册 **DispatcherServlet** 。后续的处理逻辑与基于 **web.xml** 的配置方式一致。

最后更新于

这有帮助吗?