Spring 源码解析——SpringMVC 源码解析(Spring 整合 Spring MVC )(一)

目录

一、概述

二、涉及技术

2.1 SPI 机制

2.2 Servlet 中的 ServletContainerInitializer 接口

2.3 Spring MVC 中的层级容器(Context Hierarchy)及其配置

三、源码解析(基于注解配置)

3.1 Spring Web 容器初始化器(示例代码仅提供思路引导)

3.2 调用 Spring Web 容器初始化器

3.3 创建根容器 

3.4 创建 DispatcherServlet 和子容器

3.5 初始化根容器

3.6 初始化子容器并关联父子容器

四、方法调用时序图(UML 序列图)

4.1 父子容器和 DispatcherServlet 的创建

4.2 父子容器初始化

五、内容总结


写文章不易,时序图均为自研,转载请标明出处。

同时,如果你喜欢我的文章,请关注我,让我们一起进步。

一、概述

在这篇博文中我们首先将大概了解一下 SPI 机制和 Servlet 以及 SpringMVC 中的基础技术,然后通过对于源码的分析来梳理 Spring 整合 SpringMVC 的过程,即探究 Spring 是怎样完成在 Tomcat 启动时完成 Spring web 父子容器的创建和初始化工作,最后会通过两张方法调用时序图来对整篇博文的内容进行梳理。

 

二、涉及技术

2.1 SPI 机制

因为 SPI 机制是 Spring web 容器得以自动创建并初始化的核心所在,所以我们先来大概了解一下什么是 SPI 机制。首先 SPI  机制是一种 服务发现机制,全称为 Service Provider Interface ,它通过在 ClassPath 路径下的 resource \ META-INF \ services 目录中查找符合条件的文件来自动加载文件里所定义的类。这一机制在许多的框架中都有应用比如在 Dubbo、JDBC 等,SPI 机制的使用让框架具备了更强的动态扩展能力,同时在 Tomcat 的后续版本中即 Servlet 3.x 中也增加了对于 SPI 机制的支持,也正因如此 SpringMVC 才能够更加轻易的整合 Spring 和 Servlet 。

对于 SPI 机制的使用有以下几个重点:

(1)首先我们需要定义一个接口,并让我们需要被扫描到类实现此接口;

(2)然后在项目的 resource \ META-INF \ services 路径下创建一个以 接口全限定类名 命名的文件;

(3)在刚刚创建的文件中添加我们需要被扫描到的类的 类全限定类名

(4)最后我们就可以在代码中通过 ServiceLoader.load 或者 Service.providers 方法来获取到我们需要被扫描到的 (两个方法的参数都为接口的类型,并且第一个方法返回的是一个集合包装类,需要通过获取到它的迭代器来对它进行扫描,而第二个方法返回的是一个可以直接使用的 迭代器);

 

2.2 Servlet 中的 ServletContainerInitializer 接口

public interface ServletContainerInitializer {
    public void onStartup(Set<Class<?>> c, ServletContext ctx)
        throws ServletException; 
}

 老规矩还是直接先上代码,这个接口的代码十分的简单,只有一个 onStartup 方法,但这个接口正是 Servlet 3.0 之后提供对 Java SPI 机制扩展支持的核心,首先我还是带大家先来了解一下这个接口和其中的方法。

首先对于这个接口,它允许库或运行时(runtime)在应用程序的启动阶段收到通知,并执行任何需要的代码来响应它对servlet、filter 和 listener 的注册。该接口的实现类必须被声明在 jar 包文件源码(resources)中 META-INF/services 目录下文件名为该接口全限定类名的文件中,这样它才能够被运行时服务根据限定规则所查找到,并且发现这些服务(实现类)的顺序必须遵循应用程序的类加载委托模型。

其次该接口的实现类可以通过使用 @HandlesTypes 注解来指定需要被注入到 Set 集合参数中来执行 onStartup 方法的类型,例如 @HandlesTypes(WebApplicationInitializer.class),就是指定了需要将扫描到的 WebApplicationInitializer 类型的子类或者实现类添加到 onStartup 方法的第一个参数的 Set 集合中。但是如果这个接口的实现没有使用这个注解或者没有任何可以匹配指定类型的实现类的时候,容器必须传递给 onStartup 方法一个空的 Set 集合作为首参。

当应用正在搜索该接口的实现类中 @HandlesTypes 注解指定的类型时,容器可能会因为类加载时缺少相应的 jar 包而出现问题,这主要是因为容器不能够判断当前类的加载失败会不会影响到容器的正常工作,所以它必须忽略掉这些异常,但是同时会通过日志来记录它们。

下面再来说一说 onStartup 方法,该接口的实现类(满足上述规范)可以在应用程序启动时接收到来自 ServletContext 的通知,如果该实现类在应用程序 jar 包的 WEB-INF/lib 目录下,那么它的的 onStartup 方法只会被调用一次,但是如果它位于 jar 包中但位于 WEB-INF/lib 目录之外,那么每次应用程序启动的时候都会调用 onStartup 方法。

onStartup 方法的第一个参数是一个 set 集合,包含所有的实现或继承 @HandlesTypes 注解中所指明类型的类,而当实现类没有使用 @HandlesTypes 注解或者没有找到匹配类的时候该参数值为 null 。而第二个参数代表在其中找到了第一个参数中所包含的类,且正在启动的 Web 应用程序的 ServletContext 。

 

2.3 Spring MVC 中的层级容器(Context Hierarchy)及其配置

在 Spring MVC 当中存在一个层级容器的概念,也可以简单的理解为我们通常所说的父子容器,一般来说在根容器(父容器)中会保存一些相对比较基础的 Bean 实例,比如数据层或业务层等,而在子容器中一般会保存一些比较特殊的 Bean,比如 Controller 或者 ViewResolver 等,而父子容器的关系通过上图我们也比较好理解,子容器可以使用(另一种形式的继承)父容器中的基础 Bean,而同时这两个 WebApplicationContext 又是各自存在于其对应的 DispatcherServlet 中的(DispatcherServlet 会对应持有唯一的 WebApplicationContext),所以从整体来看 WebApplicationContext 是存在于 DispatcherServlet 这个大范围当中的。

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        // 返回根容器的配置类
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        // 返回子容器的配置类
        return new Class<?>[] { App1Config.class };
    }

    @Override
    protected String[] getServletMappings() {
        // 返回 Servlet 的映射关系
        return new String[] { "/app1/*" };
    }
}
<web-app>

    <!-- 监听器配置 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- 根容器配置 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>

    <!-- 子容器配置 -->
    <servlet>
        <servlet-name>app1</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/app1-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- Servlet 映射关系配置 -->
    <servlet-mapping>
        <servlet-name>app1</servlet-name>
        <url-pattern>/app1/*</url-pattern>
    </servlet-mapping>

</web-app>

而对于 SpringMVC 当中实现层级容器的配置方式一般有以上两种,第一种是通过 Java 类来进行配置,在对应的方法中返回对应的配置类,在实际使用中我们一般继承 AbstractAnnotationConfigDispatcherServletInitializer 来进行更多参数的配置(如 filter 或者 listener 等),对于 AbstractAnnotationConfigDispatcherServletInitializer 和 WebApplicationInitializer 的继承关系可以参考下图,而第二种方式就是常用的 xml 文件配置的方式。

需要注意的是当我们不需要适用层级容器时,我们在使用 Java 类进行配置时可以让 getRootConfigClasses 方法返回 null ,而将所有的配置类通过 getServletConfigClasses 方法返回。

 

三、源码解析(基于注解配置)

3.1 Spring Web 容器初始化器(示例代码仅提供思路引导)

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // 加载 Spring web 应用程序配置
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        // 将配置类注册到容器中
        ac.register(AppConfig.class);
        // 刷新容器
        ac.refresh();

        // 创建并注册 DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        // 将创建完成的 DispatcherServlet 添加到 Servlet 容器中
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        // 设置启动顺序
        registration.setLoadOnStartup(1);
        // 添加映射关系
        registration.addMapping("/app/*");
    }
}

在开始对源码的正式分析之前,我们先通过官方文档来了解一下怎样通过代码快速创建一个 Spring Web 容器并完成 DispatcherServlet 的创建和注册。上面的代码是官方文档中给的示例,整体的启动流程非常的简单并且可以分为两大部分,第一部分完成了 Spring Web 容器的创建和注册(类似于 ApplicationContext,只不过这里是 WebApplicationContext),第二部分完成了 DispatcherServlet 的创建、配置并添加到 Servlet 容器中的操作。

虽然仅有这么几段代码却已经实现了最基本的 Spring Web 的容器启动和相关的配置工作,那么接下来我们需要思考的问题就是 Spring 是通过什么样的机制来保证 Tomcat 在启动的时候会自动调用这段代码来进行 Spring Web 容器和 DIspatcherServlet 的创建和初始化工作。在这里其实一般最容易想到的方式就是让 Tomcat 单纯通过反射机制来动态调用 Spring 初始化逻辑,但这样存在的一个问题就是会将 Spring 和 Tomcat 的代码仅仅的耦合在一起(因为反射的逻辑是被写死在代码中的)。换一种思路,我们可以将需要被扫描类型的类信息写到自己 jar 包的特定文件中,然后让 Tomcat 来完成自动扫描并加载相关类,而这也就是 Tomcat 在 Servlet 3.x 实现的一种动态扩展方式(SPI),正是通过这种机制 Spring 可以保证在 Tomcat 启动时自动加载并调用初始化器的相关代码(onStartup 方法),从而完成 Spring Web 容器和 DIspatcherServlet 的创建和初始化工作。

3.2 调用 Spring Web 容器初始化器

首先,对于 Spring 整合 Spring MVC 使用的很关键的一个技术就是我们刚刚介绍过的 SPI 机制,通过 SPI 机制 Spring 得以在 Servlet 容器启动的时候动态的将 Spring Web 容器的初始化器创建出来并完成容器的部分初始化工作,而对于这一点的佐证我们可以直接在 spring-web 的源码模块中找到如下图一这样的一个文件。同时我们也可以发现它的存放路径规则、文件命名以及文件内容(下图二)也完全满足 SPI 的规范(存放在 resources \ META_INF \ services 路径下,文件命名为接口的全限定类名,内容为实现类的全限定类名),因此我们可以更加的确定 Spring 正是通过这种 SPI 的机制完成了与 Servlet 的整合来实现 Spring MVC。

 接下来既然我们已经明确了 Spring 与 Servlet 整合实现 Spring MVC 的机制,因此我们就直切要点直接进入到这个 SpringServletContainerInitializer 类中,这个类实现了 ServletContainerInitializer 接口(这个接口是 Servlet 的一个接口),但这个类中正如我们下面的代码所示其整个类只实现了一个 onStartup 方法。

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

	@Override
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

                // 获取所有的 Spring Web 应用容器初始化器
		List<WebApplicationInitializer> initializers = new LinkedList<>();
                // 当成功获取到所有的初始化器后对它们依次遍历
		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
			        // 过滤掉一些 servlet 容器为我们提供的无效类
                                // 即初始化器不能为接口也不能为抽象类,并且应当是 WebApplicationInitializer 的子类
				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);
                // 对初始化器依次调用它们(父类 AbstractDispatcherServletInitializer)的 onStartup 方法
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

}

public void onStartup(ServletContext servletContext) throws ServletException {
        // 调用父类 AbstractContextLoaderInitializer 中的 onStartup 方法
	super.onStartup(servletContext);
        // 创建并注册 registerDispatcherServlet
	registerDispatcherServlet(servletContext);
}

// AbstractContextLoaderInitializer.java
public void onStartup(ServletContext servletContext) throws ServletException {
        // 创建、初始化根容器并注册容器加载监听器
	registerContextLoaderListener(servletContext);
}

因为上面这两个 onStartup 方法代码逻辑相对来说比较简单,根据注释就可以理解了 ,我们就再大概总结一下两个 onStartup 方法的功能。

首先对于第一个 onStartup 方法因为它满足 Servlet 3.x 中对 SPI 的扩展,因此根据它的 @HandlesTypes 注解中的属性值,Servlet 容器会将扫描到的所有 WebApplicationInitializer 抽象类的子类都添加到第一个参数 webAppInitializerClasses 的 Set 集合中(同时子类具有 @Order 注解或实现了 Order 接口,那么这些子类还会被根据优先级进行排序),并且当如果在类路径下没有找到匹配的实现类时它也会通过打印日志的方式来提醒开发者 ServletContainerInitializer 接口的相关实现类已经确实被调用了,但是没有发现 WebApplicationInitializer 抽象类的子类。在这个方法中会对符合条件的(非接口、非抽象类并且是 WebApplicationInitializer 类型)子类通过反射调用构造方法来创建它们的实例,并为它们注册和配置 Servlet(比如 Spring 当中的 DispatcherServlet)、listener(比如 ContextLoaderListener)和 filter 等。

而第二个 onStartup 方法虽然调用的是父类 WebApplicationInitializer 中的 onStartup 方法,但其实它的最终调用是在子类 AbstractDispatcherServletInitializer 中的实现,而这个方法的实现的功能也比较简单,第一部分就是调用父类的 onStartup 方法来创建并初始化配置根容器(RootWebApplicationContext),而第二部分就是创建并初始化配置 DispatcherServlet 和子容器(ServletWebApplicationContext),并对它们进行关联。但是这里需要注意的是,这两个部分都是仅对容器进行了创建和部分初始化的配置,没有对容器进行刷新操作(refrain 方法,扫描 Bean 等)。

3.3 创建根容器 

// AbstractContextLoaderInitializer.class
protected void registerContextLoaderListener(ServletContext servletContext) {
        // 创建根容器(RootApplicationContext)
	WebApplicationContext rootAppContext = createRootApplicationContext();
        // 如果成功创建根容器(外部传入根容器配置类)
	if (rootAppContext != null) {
                // 为根容器创建一个容器加载监听器(ContextLoaderListener)
                // 保证后面根容器的初始化逻辑得以被调用
		ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
                // 获取并设置根容器初始化器
		listener.setContextInitializers(getRootApplicationContextInitializers());
                // 将创建好的监听器添加到 Servlet 容器中
		servletContext.addListener(listener);
	}
	else {
		logger.debug("No ContextLoaderListener registered, as " +
				"createRootApplicationContext() did not return an application context");
	}
}

 完成了容器初始化器的 onStartup 方法的调用后,会首先调用它父类 AbstractContextLoaderInitializer 中的 registerContextLoaderListener 方法进行根容器的创建容器加载监听器的创建和注册。这里需要注意的是对于 ContextLoaderListener 的注册,因为这个 ContextLoaderListener 直接关乎根容器后面的初始化逻辑调用。

// AbstractAnnotationConfigDispatcherServletInitializer.class
protected WebApplicationContext createRootApplicationContext() {
        // 获取根配置类
	Class<?>[] configClasses = getRootConfigClasses();
        // 如果存在根配置类时
	if (!ObjectUtils.isEmpty(configClasses)) {
                // 创建一个 AnnotationConfigWebApplicationContext 容器
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
                // 将配置类注册到容器中
		context.register(configClasses);
                // 返回配置完成的根容器
		return context;
	}
	else {
                // 如果不存在根配置类则直接返回空
		return null;
	}
}

// 该方法由我们自己实现,将根配置类(具有 @Configuration 的 Java 类)传入
protected abstract Class<?>[] getRootConfigClasses();

// AnnotationConfigWebApplicationContext.class
public void register(Class<?>... annotatedClasses) {
	Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified");
        // 将根配置类全部添加到容器的 annotatedClasses 集合中
	Collections.addAll(this.annotatedClasses, annotatedClasses);
}

 对于根容器的创建工作这里会调用 createRootApplicationContext 方法,方法的逻辑也比较简单,主要就是通过 getRootConfigClasses 方法获取我们从外部传入的 Java 配置类,然后在创建完根 WebApplicationContext 容器后再将相关的配置类注册到容器中即可。再提一点,这里面调用的 context.register 方法其实就是我们在 Spring IOC 源码分析中经常分析到的容器注册流程(容器初始化一般分为 register 和 refresh 两大部分),并且在 createRootApplicationContext 方法中我们可以看到如果外部没有传入配置类,那么它是不会创建根容器的,这里其实对应层次容器结构(父子容器),正如 Spring 官方文档所述当我们不需要层次容器结构时,可以让 getRootConfigClasses 方法返回 null,而将所有的 Servlet 配置类通过 getServletConfigClasses 方法返回,这样产生的 Spring Web 容器都将是平级的关系。

3.4 创建 DispatcherServlet 和子容器

protected void registerDispatcherServlet(ServletContext servletContext) {
        // 获取 DispatcherServlet 将要注册的名称(默认为 dispatcher)
	String servletName = getServletName();
	Assert.hasLength(servletName, "getServletName() must not return null or empty");

        // 创建子容器(ServletApplicationContext)
	WebApplicationContext servletAppContext = createServletApplicationContext();
	Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");

        // 创建一个绑定当前 WebApplicationContext 的 DispatcherServlet 实例
	FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
	Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
        // 添加容器初始化器
	dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

        // 将创建完成的 DispatcherServlet 实例添加到 WebApplicationContext 中
	ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
	if (registration == null) {
		throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
				"Check if there is another servlet registered under the same name.");
	}

        // 设置启动顺序(同 xml 文件中配置)
	registration.setLoadOnStartup(1);
        // 添加映射关系(getServletMappings 方法由我们自己实现,重写方法将 Servlet 映射关系传入)
	registration.addMapping(getServletMappings());
        // 设置是否支持异步
	registration.setAsyncSupported(isAsyncSupported());

        // 获取过滤器(getServletFilters 方法也由我们自己实现,重写方法将 Filter 传入)
	Filter[] filters = getServletFilters();
	if (!ObjectUtils.isEmpty(filters)) {
		for (Filter filter : filters) {
                        // 注册过滤器
			registerServletFilter(servletContext, filter);
		}
	}

        // 可选再进行一次自定义注册(空实现)
	customizeRegistration(registration);
}

// AbstractAnnotationConfigDispatcherServletInitializer.class
protected WebApplicationContext createServletApplicationContext() {
        // 创建一个 AnnotationConfigWebApplicationContext 容器
	AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        // 获取子容器配置类(当不需要层次结构容器时该方法将返回 null)
	Class<?>[] configClasses = getServletConfigClasses();
        // 如果存在子容器配置类就将其注册到容器中
	if (!ObjectUtils.isEmpty(configClasses)) {
		context.register(configClasses);
	}
        // 将创建好的子容器返回
	return context;
}

// 该方法由我们自己实现,将子配置类(具有 @Configuration 的 Java 类)传入
protected abstract Class<?>[] getServletConfigClasses();

protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
        // 创建一个绑定当前 WebApplicationContext 的 DispatcherServlet
	return new DispatcherServlet(servletAppContext);
}

 当 onStartup 方法调用父类中的 registerContextLoaderListener 方法完成根容器的创建、初始化以及容器加载监听器的注册后,接着它会调用 registerDispatcherServlet 方法来创建、初始化子容器和 DispatcherServlet,具体的流程大概如下:

(1)获取 DispatcherServlet 名称;

(2)通过外部传入的配置类(getServletConfigClasses)创建 Spring web 子容器(WebApplicationContext);

(3)创建一个绑定当前 Spring web 子容器的 DispatcherServlet 实例,并让 DispatcherServlet 持有子容器引用

(4)为刚刚创建的 DispatcherServlet 添加容器初始化器(ContextInitializer),完成后将其注册到 Servlet 容器中;

(5)对刚刚创建的 DispatcherServlet 进行相关的参数配置;

(6)获取外部传入的 filter(getServletFilters)并将其注册给 Servlet 容器;

(7)调用 customizeRegistration 方法给外部一次自定义注册的机会;

3.5 初始化根容器

// ContextLoaderListener.calss
public void contextInitialized(ServletContextEvent event) {
	initWebApplicationContext(event.getServletContext());
}

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        // 判断当前 Servlet 容器中是否已经设置了根容器,如果已经设置了则抛异常
	if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
		throw new IllegalStateException(
				"Cannot initialize context because there is already a root application context present - " +
				"check whether you have multiple ContextLoader* definitions in your web.xml!");
	}

	servletContext.log("Initializing Spring root WebApplicationContext");
	Log logger = LogFactory.getLog(ContextLoader.class);
	if (logger.isInfoEnabled()) {
		logger.info("Root WebApplicationContext: initialization started");
	}
	long startTime = System.currentTimeMillis();

	try {
		// 将 Spring web 容器(这里是指根容器)存储在本地实例变量中来保证它在 ServletContext 关闭时可用
		if (this.context == null) {
                        // 如果当前容器不存在则创建
			this.context = createWebApplicationContext(servletContext);
		}
		if (this.context instanceof ConfigurableWebApplicationContext) {
                        // 对父容器进行类型强转并赋值给变量 cwac
			ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
                        // 如果当前容器还未被激活(未被刷新)
			if (!cwac.isActive()) {
				// 如果当前容器未设置父容器
				if (cwac.getParent() == null) {
					// 上下文实例在没有显式设置父实例的情况下被注入则尝试加载它的父容器
                                        // 对于没有特殊需要的根容器一般返回 null
					ApplicationContext parent = loadParentContext(servletContext);
                                        // 为当前容器设置获取到的父容器 
					cwac.setParent(parent);
				}
                                // 配置并刷新当前 Spring Web容器
				configureAndRefreshWebApplicationContext(cwac, servletContext);
			}
		}	    
                // 设置当前 Spring Web 容器为 Servlet 的根容器
                // 当初始化子容器的时候使用到这个值
                servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
                // 获取类加载器
		ClassLoader ccl = Thread.currentThread().getContextClassLoader();
		if (ccl == ContextLoader.class.getClassLoader()) {
			currentContext = this.context;
		}
		else if (ccl != null) {
			currentContextPerThread.put(ccl, this.context);
		}

		if (logger.isInfoEnabled()) {
			long elapsedTime = System.currentTimeMillis() - startTime;
			logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
		}

		return this.context;
	}
	catch (RuntimeException | Error ex) {
		logger.error("Context initialization failed", ex);
                servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
		throw ex;
	}
}

对于根容器的初始化工作主要是通过 ContextLoaderListener 来完成的,刚刚在创建根容器的过程中我们创建了一个 ContextLoaderListener 实例,并将根容器保存在实例当中,最终将其注册到 Servlet 容器中。同时 ContextLoaderListener 实现了 ServletContextListener 接口这也就意味着它能够监听到 Servlet 容器的生命周期,当 Servlet 容器启动 web 应用时会调用到其中的 contextInitialized 方法,也就是在这个方法中通过调用 initWebApplicationContext 完成了对根容器的初始化工作。

上面对于 initWebApplicationContext 方法的注释已经解释的比较清楚了,需要注意的点就是它最终通过调用 configureAndRefreshWebApplicationContext 方法(这个方法最核心的就是其最后调用 refresh 方法来刷新容器,也就是从这里进入到了 Spring IOC 的相关逻辑中,完成了对于根容器中相关 Bean 的实例化等工作)来实现根容器的配置和刷新,同时它在完成根容器的初始化工作后会通过设置 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 的属性值来将根容器的引用进行保存,这主要是用于后面子容器的初始化工作。

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
	if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
		// 如果应用程序上下文 id 仍然设置为其原始默认值
		// 在这里根据可用信息分配更有用的 id
		String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
		if (idParam != null) {
			wac.setId(idParam);
		}
		else {
			// 生成默认id
			wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
					ObjectUtils.getDisplayString(sc.getContextPath()));
		}
	}

        // 将 Spring web 容器与 Servlet 容器进行绑定
	wac.setServletContext(sc);
        // 获取 Servlet 容器的初始化参数
	String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
	if (configLocationParam != null) {
                // 将 Servlet 容器配置参数设置给 Spring Web 容器
		wac.setConfigLocation(configLocationParam);
	}

	// 在任何情况下,当上下文被刷新时,都会调用 Spring Web 环境的 initPropertySources 方法
	// 确保 servlet 属性源在 refresh 方法之前的任何后续处理或初始化中都处于适当的位置
	ConfigurableEnvironment env = wac.getEnvironment();
	if (env instanceof ConfigurableWebEnvironment) {
		((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
	}

        // 处理自定义的 ContextInitializer
	customizeContext(sc, wac);
        // 刷新容器
	wac.refresh();
}

3.6 初始化子容器并关联父子容器

经过上面的几个步骤我们已经完成了根容器、子容器和 DispatcherServlet 的创建工作,并且已经通过 Servlet 监听器完成了根容器的初始化工作,那么接下来要做的就是最后两步:初始化子容器并关联父子容器。首先如果想要搞清楚这部分的调用逻辑,就需要先从 DispatcherServlet 的继承结构说起。

通过上图继承树中的蓝色实现我们可以发现,其实 DispatcherServlet 的本质就是一个 HttpServlet,同时你也可以发现我们上面调用到的很多方法其实也都是在 DIspatcherServlet 的父容器中进行实现的。那么言归正传,如果 DispatcherServlet 的本质就是一个 HttpServlet,那根据 Servlet 的规范就会在初始化这个 Servlet 的时候调用到它的 init 方法,又因为我们在创建 DispatcherServlet 的时候就已经注入了一个 WebApplicationContext 实例(子容器),因此我们就可以推断出对于子容器的初始化工作应当是在 DispatcherServlet 被调用 init 初始化方法时进行的。

但是需要注意的是,对于使用 Java 注解进行配置和使用 xml 文件进行配置都会调用到这个方法中,因为篇幅的原因本文不会过多的涉及 xml 配置的 DIspatcherServlet 进入到该方法时的逻辑,仅会在显著区别的逻辑处进行注释。

// HttpServletBean.class
public final void init() throws ServletException {

	// 使用 init 参数设置 Bean 属性
	PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
	if (!pvs.isEmpty()) {
		try {
			BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
			ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
			bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
			initBeanWrapper(bw);
			bw.setPropertyValues(pvs, true);
		}
		catch (BeansException ex) {
			throw ex;
		}
	}
	// 初始化 ServletBean
	initServletBean();
}

// FrameworkServlet.class
protected final void initServletBean() throws ServletException {
	long startTime = System.currentTimeMillis();
	try {
                // 初始化 WebApplicationContext 容器
		this.webApplicationContext = initWebApplicationContext();
                // 初始化 FrameworkServlet(空实现)
		initFrameworkServlet();
	}
	catch (ServletException | RuntimeException ex) {
		throw ex;
	}
}

当调用到 DispatcherServlet 的 init 方法后,首先会使用 init 中配置的参数(Java 注解或 xml 中设置的 init 属性)对 Bean 的属性进行赋值,然后会调用 initServletBean 方法来初始化 ServletBean,但是需要注意的是这个 init 方法是在 DispatcherServlet 的父类 HttpServletBean 中实现的,而它所调用的 initServletBean 方法又是由 DispatcherServlet 的直接父类、HttpServletBean的子类 FrameworkServlet 来实现的(这里其实是使用了模板方法设计模式,即父类只负责提供规范,具体的内容交由子类来实现)。而进入到 initServletBean 方法后才会调用到关键的 initWebApplicationContext 方法来初始化 Spring web 容器。

// FrameworkServlet.class
protected WebApplicationContext initWebApplicationContext() {
        // 获取 Spring web 根容器
	WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        // 创建子容器变量且初始值为 null
	WebApplicationContext wac = null;

        // 如果当前 DispatcherServlet 已经绑定了 webApplicationContext 容器
        // 那说明在构建时已经向 DispatcherServlet 中注入了 webApplicationContext 容器
        // 这里主要是用来区分注解初始化和 xml 文件初始化(下面详细分析)
	if (this.webApplicationContext != null) {
		// 因为已经绑定,所以直接将绑定的 webApplicationContext 赋给 wac 变量
		wac = this.webApplicationContext;
		if (wac instanceof ConfigurableWebApplicationContext) {
			ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                        // 如果当前 webApplicationContext 容器还未刷新(未调用 refresh 方法)
			if (!cwac.isActive()) {
				if (cwac.getParent() == null) {
					// 容器实例在没有显式父实例的情况下被注入
					// 如果根容器存在,就使用根容器作为其父容器(当然根容器也可能为空)
					cwac.setParent(rootContext);
				}
                                // 调用方法配置容器并调用其 refresh 方法进行刷新
                                // 同时让容器持有 Servlet 容器的引用
				configureAndRefreshWebApplicationContext(cwac);
			}
		}
	}
	if (wac == null) {
		// 如果进入到此判断意味着在构建时没有为其注入 webApplicationContext
		// 那么先查看在 servlet 容器中是否已经注册了 webApplicationContext(检查 contextAttribute 属性值)
		// 如果存在则假定已经设置了父容器(如果存在根容器)
		// 并且已经执行了所有的初始化操作,例如设置容器 id 等
		wac = findWebApplicationContext();
	}
	if (wac == null) {
		// 如果进到这个判断说明还是没有为 DispatcherServlet 注册 webApplicationContext 实例(一般使用 xml 配置 DispatcherServlet 会走到这里)
                // 那么就直接调用 createWebApplicationContext 方法创建一个 webApplicationContext 实例
                // 在这个方法中同时会将传入的根容器作为新创建容器的父容器
		wac = createWebApplicationContext(rootContext);
	}

	if (!this.refreshEventReceived) {
		// 如果当前 Spring web 容器不是具有刷新支持的 ConfigurableApplicationContext
		// 或者在构造时注入的容器已经被刷新
		// 在这里手动触发初始 onRefresh 方法
		synchronized (this.onRefreshMonitor) {
			onRefresh(wac);
		}
	}

	if (this.publishContext) {
		// 将当前 Spring web 容器(webApplicationContext)作为 servlet 容器的属性发布
		String attrName = getServletContextAttributeName();
                // 也就是在这里将 webApplicationContext 暴露给了 Servlet 容器(便于其它的 DispatcherServlet 获取)
		getServletContext().setAttribute(attrName, wac);
	}

	return wac;
}

 这里对于 initWebApplicationContext 方法实现逻辑概括一下:

(1)首先获取 Servlet 容器中的 根 Spring web 容器

(2)判断当前的 DispatcherServlet 中的 webApplicationContext 属性是否已经被赋值,如果被赋值则说明在前面构造 DispatcherServlet 的过程中就已经完成了 webApplicationContext 的创建、初始化和注册工作(即上面 3.2 的情况),换句话说此时的配置方式是使用 Java 注解配置的;

(3)如果已经确定了当前 DispatcherServlet 已经完成了 webApplicationContext 的注册,那么就直接获取已经注册了的 webApplicationContext 实例,首先判断它是否已经进行了容器刷新,如果没有且其当前不存在父容器,那么就先将根容器设置为其父容器(如果根容器存在的话),然后再对它进行必要的配置和回调后,最后调用它的 refresh 方法来刷新激活容器(代码见下方),反之如果存在父容器那么就只进行容器的配置和激活刷新;

(4)那如果当前 DispatcherServlet 没有注册 webApplicationContext 实例的话,就需要分两种情况来讨论(但这两种情况一般都是针对使用 xml 进行配置时),一种是当前 DispatcherServlet 的 contextAttribute 属性值已经被设置了(可以理解为已经预定好了 webApplicationContext ,但还没有进行注册),那么这个时候我们就可以直接通过 WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName) 方法根据容器名来获取指定的 webApplicationContext 实例,然后再返回给调用者就可以了(以上逻辑主要集中在 findWebApplicationContext 方法中,代码如下);

另一种情况是当前 DispatcherServlet 的 contextAttribute 属性也未被赋值(使用 xml 配置就会这样),那么此时就只能现场创建一个 webApplicationContext 实例了(createWebApplicationContext 方法代码实现如下);

(5)然后再进行判断,如果刚刚获取到的 webApplicationContext 实例不是具有刷新支持的 ConfigurableApplicationContext 或者在构造时注入的容器已经被刷新则手动触发初始 onRefresh 方法;

(6)最后如果当前 DispatcherServlet 支持将 webApplicationContext 作为 Servlet 容器属性发布发布的话,那么就将刚刚获取到的 webApplicationContext 实例存储到 Servlet 容器的属性中,且存储时的键为 SERVLET_CONTEXT_PREFIX + servlet name ,值为 webApplicationContext 实例,到这里也就完成了子容器的初始化和父子容器关联的工作了; 

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
	if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
		// 如果容器 id 仍然设置为其原始默认值
		// 则根据可用信息设置更有用的 id
		if (this.contextId != null) {
			wac.setId(this.contextId);
		}
		else {
			// 使用默认 id
			wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
		}
	}
        
        // 将 Servlet 容器注册到当前 Spring web 容器中
	wac.setServletContext(getServletContext());
        // 获取 Servlet 容器中的配置信息
	wac.setServletConfig(getServletConfig());
        // 设置命名空间
	wac.setNamespace(getNamespace());
        // 添加监听器
	wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

	ConfigurableEnvironment env = wac.getEnvironment();
	if (env instanceof ConfigurableWebEnvironment) {
		((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
	}

        // 调用生命周期回调
	postProcessWebApplicationContext(wac);
        // 应用容器的所有初始化器
	applyInitializers(wac);
        // 刷新容器
	wac.refresh();
}

// 查看在 servlet 容器中是否已经注册了 webApplicationContext
protected WebApplicationContext findWebApplicationContext() {
        // 获取 DispatcherServlet 实例的 contextAttribute 属性值
	String attrName = getContextAttribute();
	if (attrName == null) {
		return null;
	}
        // 确定已经注册后直接将注册的 webApplicationContext 返回给调用者
	WebApplicationContext wac =
			WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
	if (wac == null) {
		throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
	}
	return wac;
}

// 创建一个 WebApplicationContext 实例并返回 
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
	Class<?> contextClass = getContextClass();
	if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
		throw new ApplicationContextException(
				"Fatal initialization error in servlet with name '" + getServletName() +
				"': custom WebApplicationContext class [" + contextClass.getName() +
				"] is not of type ConfigurableWebApplicationContext");
	}
	ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

	wac.setEnvironment(getEnvironment());
        // 设置根容器为父容器
	wac.setParent(parent);
	String configLocation = getContextConfigLocation();
	if (configLocation != null) {
		wac.setConfigLocation(configLocation);
	}
        // 配置并激活刷新当前容器
	configureAndRefreshWebApplicationContext(wac);
	
	return wac;
}

四、方法调用时序图(UML 序列图)

4.1 父子容器和 DispatcherServlet 的创建

4.2 父子容器初始化

 

五、内容总结

本文主要完成了对于 Spring 整合 SpringMVC 源码的一些分析,大概探究了 Spring 是怎样在 Tomcat 启动时完成 Spring web 父子容器的创建和初始化工作。

从没有白费的努力,也没有碰巧的成功。只要认真对待生活,终有一天,你的每一份努力,都将绚烂成花。

发布了244 篇原创文章 · 获赞 32 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_40697071/article/details/102230862