前言

  之前有看过Spring源码,最近在看Spring MVC源码,感觉Spring源码里面更多是基础组件,并没有太多暴露给开发者的入口,但是Spring MVC不一样, 可配置的地方很多,可灵活自定义的地方也更多,源码理解起来更贴近于应用。在查看源码过程中也有蛮多心得,一直记在心里也很容易就忘了,后面我会将源码中的一些心得都分享出来,希望能让更多感兴趣的同学一起参与进来,一起讨论。

  理解Spring MVC源码不是一蹴而就的事情,需要慢嚼细咽,在这中间理解作者的意图和其中的奥妙,特定的设计模式、更好的编码规范、代码风格,那也是我看源码的初衷。好了,废话不多说了,今天主要讲一下MVC应用的初始化,简单的说Spring MVC在tomcat或其它webserver启动时从哪一行代码开始,知道了开始的位置可以更方便于我们后续的调试、理解应用加载过程细节。

springmvc 配置 RabbitTemplate springmvc 配置路由_初始化


spring-web被tomcat发现过程

🎍WebApplicationInitializer作用

  这个类是Spring Web核心类,顾名思义,它的作用即web mvc应用初始化。我做了一个简单的UML图用来描述该类:

springmvc 配置 RabbitTemplate springmvc 配置路由_MVC_02


相信看着这个继承关系图,很多人应该还是很蒙的状态,上图要表示什么意思?不着急,我来慢慢解释。我们知道,Spring MVC核心组件只有一个servlet,后续所有请求路径发现、路由全部交给这一个servlet,这个servlet就是DispatcherServlet那这个servlet如何被servlet容器发现呢?它又是如何注册到servlet容器中的呢?如何在Spring容器中生成的呢? 有了这些疑问我们就可以更好的往下走了。

还是先回到正题,实现该接口的类主要作用是用于在Java代码中配置ServletContext。这是啥意思呢?简单的说,我们web应用大部分采用的是web.xml来初始化servlet上下文,也即ServletContext,但是这种配置文件又非常繁杂,手动写?反正我是写不出来,但是这个文件还必须要定义出来,所以JEE标准制定的那些人在servlet 3.0中增加了程序内配置servlet上下文API来取代web.xml配置文件。简单例子如下:

<servlet>
    <servlet-name>springServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>springServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

上面是原始web.xml配置方式,如果程序中手动注册DispatcherServlet可以采用如下方式:

public class MyWebAppInitializer implements WebApplicationInitializer {
	@Override
	public void onStartup(ServletContext container) {
		XmlWebApplicationContext appContext = new XmlWebApplicationContext();
		appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
		ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(appContext));
		dispatcher.setLoadOnStartup(1);
		dispatcher.addMapping("/");
	}
}

通过上面手动注册DispatcherServlet的方式,我们已经基本上摆脱了烦人的web.xml文件。那为什么WebApplicationInitializer能替换web.xml配置文件,它的作用原理是什么呢?在这里我就不继续往下延伸了,感觉后面可能更多牵扯到JEE API,不是本节重点,如果对实现原理感兴趣的同学可以看这个类javax.servlet.ServletContainerInitializerjavax.servlet.annotation.HandlesTypes
继续回到上面问题,DispatcherServlet如何被servlet容器发现呢?它又是如何注册到servlet容器中的呢? webserver在启动servet容器时会触发上面WebApplicationInitializer的onStartup方法,在子类AbstractDispatcherServletInitializer中重写了刚方法,有如下这段代码:

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
	super.onStartup(servletContext);
	registerDispatcherServlet(servletContext);
}

可以看到在调用onStartup期间就new了一个DispatcherServlet并将改servlet通过ServletContext#addServlet方法注册到servlet容器中。

AbstractContextLoaderInitializer作用

  该类是一个抽象类,定义了一个createRootApplicationContext抽象方法,该方法返回一个WebApplicationContext实例,也可以理解为mvc应用根容器(Root WebApplicationContext),不明白什么是根容器的同学可以先转到下方Spring MVC双容器。

  该类是WebApplicationInitializer实现类,应用启动时会触发onStartup方法,该方法会创建一个ContextLoaderListener并注册到servlet容器中,我对该类的理解是web应用启动、关闭事件监听器,ContextLoaderListener的层级关系如下图:

springmvc 配置 RabbitTemplate springmvc 配置路由_spring_03


监听到servlet容器启动时间时,会触发contextInitialized方法,然后执行ContextLoader(上下文加载器)的initWebApplicationContext方法去进行根容器初始化。不明白上下文加载器的同学可以移步下面关于ContextLoader介绍部分。

AbstractDispatcherServletInitializer作用

  看了上面AbstractContextLoaderInitializer部分我们知道它的作用是创建根容器(Root WebApplicationContext)并进行初始化,并知道了MVC双容器。所以父容器初始化完了后就轮到servlet上下文(Servlet ApplicationContext)初始化了,也就是子容器初始化。
  由于AbstractDispatcherServletInitializer是继承AbstractContextLoaderInitializer,在调用onStartup方法是会首先调用父类onStartup方法去初始化根容器,初始化完成后会调用它自己的createServletApplicationContext方法创建子容器,子容器也是一个WebApplicationContext,注意:目前所说的所有MVC容器都是WebApplicationContext实现类。 子容器创建完成后,下一步创建核心Servlet–DispatcherServlet并将该Servlet通过servletContext#addServlet()注册到J2EE容器中。那如果我要注册自定义的过滤器怎么做?如何将指定URL映射到DispatcherServlet上? 当然如果用web.xml很方便,可以通过filter标签和url-pattern来解决上面问题,那纯Java配置的web应用如何处理呢?这些问题在下面这一节会一一解释如何处理。

AbstractAnnotationConfigDispatcherServletInitializer作用

  上面说了AbstractContextLoaderInitializer作用是初始化根容器并注册servlet启动、关闭监听器,也说了AbstractDispatcherServletInitializer作用是初始化servlet子容器并创建DispatcherServlet,将该Servlet添加到J2EE容器中。他们作用都比较完善,但抽象方法都比较多,特别是createRootApplicationContext和createServletApplicationContext都是抽象方法,如果我们继承其中任何一个类都要实现上面两个抽象方法去创建父、子容器,很容易出错,spring这个时候给我们做了一个默认子类实现,同时把必要的几个方法抽象出来以供开发者重写,这个类就是AbstractAnnotationConfigDispatcherServletInitializer,先来看看该类的几个方法:

springmvc 配置 RabbitTemplate springmvc 配置路由_MVC_04


上面圈圈内是它暴露给开发者的两个抽象方法,至于根容器、servlet容器创建在该类中全做了默认实现,具体WebApplicationContext实现类都采用的是AnnotationConfigWebApplicationContext,这里我截取一段关于root applicationContext生成代码,对应上面第一个方法:

@Override
protected WebApplicationContext createRootApplicationContext() {
	Class<?>[] configClasses = getRootConfigClasses();
	if (!ObjectUtils.isEmpty(configClasses)) {
	    // 创建根容器
		AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
		rootAppContext.register(configClasses);
		return rootAppContext;
	}
	else {
		return null;
	}
}

我们如果要采用全注解方式实现mvc应用,换句话说摆脱web.xml文件的web应用就需要继承该类并重写它的几个抽象方法。总共有三个抽象方法,下面分别对三个抽象方法做详细讲解:

  • getRootConfigClasses方法返回的类就是将要被根容器扫描的配置类,也就是含有@Configuration注解的类。定义在这些注解类中的bean都将保存在根容器中
  • getServletConfigClasses方法返回的类就是将要被servlet容器扫描的配置类,也就是含有@Configuration注解的类。定义在这些注解类中的bean都将保存在servletApplicationContext中
  • getServletMappings抽象方法用于定义哪些URL能映射到DispatcherServlet上,如果该方法返回"/abc"单元素数组,那我们整个应用只能映射类型为"http://localhost:8080/cx/abc/xxx"样式URL,不能映射"http://localhost:8080/cx/a/xxx"等非/abc开头的URL。当然大部分情况下,我们处理该方法返回的结果都是"/"。

🙉Spring MVC双容器

  两个容器分别为RootApplicationContext和ServletApplicationContext,当然他们并不是两个具体类,只是这个称呼,RootApplicationContext在框架中具体类为WebApplicationContext。后者可以访问前者容器内的bean,前者不能访问存放在后者容器中的bean,如下图:

springmvc 配置 RabbitTemplate springmvc 配置路由_初始化_05


上面访问不可逆其实本质上还是spring对不同范围的容器做了隔离,在ApplicationContext的基础接口ConfigurableApplicationContext中定义了setParent方法,用来设置父容器。这里我写了一个很简单的demo,代码全部在autowire包里,代码地址:https://github.com/Crabime/coding/tree/master/spring/spring-basic/src/main/java/autowire。当然这里我也不会讨论spring为什么设置这个父子容器?它的作用?有时间后面会更新它的实现原理并附上链接。

  关于双容器废话说了很多,最后还是要补充一点,Root WebApplicationContext是Servlet ApplicationContext的父容器,所以后者可以访问前者bean,但是前者无法访问后者容器内的bean,之前在工作中有发生过相关诡异bug,数据通过后台线程在运行时存到一个注入的map中,但是后续请求到来是发现map为空,调试了很久才发现数据存到另外一个容器中,这块我会尽快补充demo(todo)。

同一个bean被两个容器都装载了怎么办?

🐻上下文加载器ContextLoader

  执行Spring MVC根上下文(Root WebApplicationContext)的初始化,注意这里是根容器而不是servlet容器,那哪些类处于根容器中呢?这个后面我会继续介绍。我们知道ApplicationContext也被称为bean容器,常见上下文类型有:

  1. 基于xml配置文件的spring应用(非web)ClassPathXmlApplicationContext
  2. 基于注解的应用上下文AnnotationConfigApplicationContext 那在web容器中呢?spring mvc默认为XmlWebApplicationContext,它存放在ContextLoader.properties配置文件中,该文件存放在spring-web.jar包中,如下图:
  3. springmvc 配置 RabbitTemplate springmvc 配置路由_MVC_06

  4. 在ContextLoader初始化时将该properties文件加载到内存,后面创建Root WebApplicationContext时先查找web.xml中是否定义了contextClass属性,也即如下:
<context-param>
   <param-name>contextClass</param-name>
   <param-value>ConfigurableWebApplicationContext子类</param-value>
</context-param>

  如果定义了contextClass属性,那么根容器(Root WebApplicationContext)就是param-value值了,这是自定义根容器情况下。如果不自定义,也就是spring发现web.xml文件中没有定义该属性,它会获取ContextLoader.properties文件中定义的默认根容器,也就是XmlWebApplicationContext。
  好了,既然根容器已经创建完成后,那就要开始最顶层bean的初始化了,也就回到上面那个问题,“哪些bean创建在根容器中呢?”,这里又要引入了另外一个web.xml全局属性contextConfigLocation,它的值是我们应用中的xml配置文件,里面定义的bean就是将要存放在根容器的,举个例子:

<context-param>
   <param-name>contextConfigLocation</param-name>
	<!-- 多个配置文件通过逗号、分号或空格分割 -->
   <param-value>classpath:spring-mvc.xml,xxx.xml</param-value>
</context-param>

  上面spring-mvc.xml,xxx.xml配置文件中的bean将会加载到Root容器中。那如果我没有定义contextConfigLocation属性呢?还有bean会加载到根容器中吗? 这个问题是个很不错的问题,spring mvc默认会加载哪些配置文件,答案很简单,前面我们知道spring mvc根容器在我们没有指定根容器情况下默认XmlWebApplicationContext,而该类的getDefaultConfigLocations方法实现可以看到,默认情况下加载的是applicationContext.xml配置文件。也就是说spring mvc默认情况下会将applicationContext.xml加载到根容器中,但是如果我们指定了contextConfigLocation,那根容器只会加载该属性值对应的配置文件,不再会去加载applicationContext.xml配置文件。