spring mvc的启动流程(精简版)

165 阅读7分钟

前言

前面介绍了: spring的启动流程bean的生命周期常见的扩展接口等知识,还提供了spring的简单使用示例方便读者阅读和理解spring源码。不过目前为止,spring给人的感觉是:

  1. 功能强大却又好像没啥用。
  2. 不知道如何将spring运用到实际项目中。

spring mvc就回应了上述疑问。它是spring在web开发上的运用,它能极大简化了web应用的开发工作量。在学习springmvc的启动流程前,先提供一个springmvc的简单使用示例,方便读者阅读和理解springmvc启动流程源码。

通过资料得知spring mvc的大致启动流程是:

  1. web容器(例如tomcat)启动时会加载web.xml中Servlet、Filter、Listener等组件,也就是会加载spring mvc自定义的DispatchServlet、ContextLoaderListener组件。
  2. web容器启动过程中,当ServletContext初始化之后,会调用ContextLoaderListener的contextInitialized(),spring mvc在contextInitialized()中会初始化一个根web应用上下文
  3. DispatchServlet初始化过程中,会调用DispatchServlet的init(),spring mvc在init()会初始化一个子web应用上下文,并加载处理器映射、处理器适配、视图解析器等组件。

源码解析

目前已经知道ContextLoaderListener、DispatcherServlet的大致作用,接下来将深入这两个类的源码,知晓它们在spring mvc启动过程中的执行细节。

ContextLoaderListener

ContextLoaderListener的继承结构

  1. EventListener接口没定义接口方法,仅是标识的作用。

  2. ServletContextListener是Servlet规范中定义的接口,定义了两个接口方法:

    • contextInitialized:ServletContext初始化之后会调用该方法。
    • contextDestroyed:ServletContext销毁之后会调用该方法。
  3. ContextLoader类是spring定义的类,其作用是加载root application context,也就是根web应用上下文。至于ContextLoaderListener加载根web应用上下文的能力就依托ContextLoader类。

ContextLoaderListener的执行流程

  1. 调用了父类ContextLoader的initWebApplicationContext()。
	/**
	 * Initialize the root web application context.
	 */
	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}
  1. ContextLoader的initWebApplicationContext()主要干了3件事:

    • 创建根web应用上下文(如果不存在则创建)。
    • 配置并刷新根web应用上下文。
    • 将根web应用上下文和ServletContext进行绑定。

	/**
	 * Initialize Spring's web application context for the given servlet context,
	 * using the application context provided at construction time, or creating a new one
	 * according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
	 * "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
	 * @param servletContext current servlet context
	 * @return the new WebApplicationContext
	 * @see #ContextLoader(WebApplicationContext)
	 * @see #CONTEXT_CLASS_PARAM
	 * @see #CONFIG_LOCATION_PARAM
	 */
	public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
		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 {
			// Store context in local instance variable, to guarantee that
			// it is available on ServletContext shutdown.
            // 根web应用上下文不存在则创建
			if (this.context == null) {
				this.context = createWebApplicationContext(servletContext);
			}
			if (this.context instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
				if (!cwac.isActive()) {
					// The context has not yet been refreshed -> provide services such as
					// setting the parent context, setting the application context id, etc
					if (cwac.getParent() == null) {
						// The context instance was injected without an explicit parent ->
						// determine parent for root web application context, if any.
						ApplicationContext parent = loadParentContext(servletContext);
						cwac.setParent(parent);
					}
                    // 配置并刷新根web应用上下文
					configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}
            // 将根web应用上下文和ServletContext进行绑定
			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;
		}
	}


  1. 创建根web应用上下文的createWebApplicationContext()的逻辑是:根据ServletContext确定web应用上下文的class,默认是XmlWebApplicationContext,实例化一个web应用上下文对象。
	/**
	 * Instantiate the root WebApplicationContext for this loader, either the
	 * default context class or a custom context class if specified.
	 * <p>This implementation expects custom contexts to implement the
	 * {@link ConfigurableWebApplicationContext} interface.
	 * Can be overridden in subclasses.
	 * <p>In addition, {@link #customizeContext} gets called prior to refreshing the
	 * context, allowing subclasses to perform custom modifications to the context.
	 * @param sc current servlet context
	 * @return the root WebApplicationContext
	 * @see ConfigurableWebApplicationContext
	 */
	protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
		Class<?> contextClass = determineContextClass(sc);
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
					"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
		}
		return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	}
  1. 配置和刷新根web应用上下文的configureAndRefreshWebApplicationContext()的逻辑是:

    • 设置根web应用上下文的id。
    • 将ServletContext的configLocationParam参数值设置给根web应用上下文。
    • 初始化根山下文的Environment。
    • 执行ApplicationContextInitializer的initialize()
    • 执行ApplicationContext的refresh()。
	protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
		//  设置根web应用上下文的id
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
			// The application context id is still set to its original default value
			// -> assign a more useful id based on available information
			String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
			if (idParam != null) {
				wac.setId(idParam);
			}
			else {
				// Generate default id...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(sc.getContextPath()));
			}
		}

		wac.setServletContext(sc);
        // 将ServletContext中的configLocationParam设置给根web应用上下文
		String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
		if (configLocationParam != null) {
			wac.setConfigLocation(configLocationParam);
		}

		// The wac environment's #initPropertySources will be called in any case when the context
		// is refreshed; do it eagerly here to ensure servlet property sources are in place for
		// use in any post-processing or initialization that occurs below prior to #refresh
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
		}

        // 执行ApplicationContextInitializer的initialize()
		customizeContext(sc, wac);
		wac.refresh();
	}

总结

可见ContextLoaderListener的执行过程是:

画板

DispatcherServlet

DispatcherServlet的继承结构

DispatchetServlet的初始化过程

DispatcherServlet初始化过程的核心逻辑在init的方法里。为了方便理解,我将DispacherServlet和其各层级父类区分来,用下图表示init方法的执行过程:

画板

  1. DispacherServlet最终会执行父类FrameworkServelt的initWebApplicationContext(), initWebApplicationContext()常规执行链路是:

    • 将根web应用上下文作为入参调用createWebApplicationContext()创建子web应用上下文。
    • 调用onRefresh方法。
    • 将子web应用上下文和ServletContext绑定。
	/**
	 * Initialize and publish the WebApplicationContext for this servlet.
	 * <p>Delegates to {@link #createWebApplicationContext} for actual creation
	 * of the context. Can be overridden in subclasses.
	 * @return the WebApplicationContext instance
	 * @see #FrameworkServlet(WebApplicationContext)
	 * @see #setContextClass
	 * @see #setContextConfigLocation
	 */
	protected WebApplicationContext initWebApplicationContext() {
        // 获取根web应用上下文
		WebApplicationContext rootContext =
				WebApplicationContextUtils.getWebApplicationContext(getServletContext());
		WebApplicationContext wac = null;

		if (this.webApplicationContext != null) {
			// A context instance was injected at construction time -> use it
			wac = this.webApplicationContext;
			if (wac instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
				if (!cwac.isActive()) {
					// The context has not yet been refreshed -> provide services such as
					// setting the parent context, setting the application context id, etc
                    // web应用上下文还未刷新,则为其设置父应用上下文
					if (cwac.getParent() == null) {
						// The context instance was injected without an explicit parent -> set
						// the root application context (if any; may be null) as the parent
						cwac.setParent(rootContext);
					}
                    // 3. 刷新应用上下文
					configureAndRefreshWebApplicationContext(cwac);
				}
			}
		}
		if (wac == null) {
			// No context instance was injected at construction time -> see if one
			// has been registered in the servlet context. If one exists, it is assumed
			// that the parent context (if any) has already been set and that the
			// user has performed any initialization such as setting the context id
			wac = findWebApplicationContext();
		}
        // 将根web应用上下文作为入参调用createWebApplicationContext()创建子web应用上下文。
		if (wac == null) {
			// No context instance is defined for this servlet -> create a local one
			wac = createWebApplicationContext(rootContext);
		}
        
        //
		if (!this.refreshEventReceived) {
			// Either the context is not a ConfigurableApplicationContext with refresh
			// support or the context injected at construction time had already been
			// refreshed -> trigger initial onRefresh manually here.
			synchronized (this.onRefreshMonitor) {
				onRefresh(wac);
			}
		}

		if (this.publishContext) {
			// Publish the context as a servlet context attribute.
			String attrName = getServletContextAttributeName();
			getServletContext().setAttribute(attrName, wac);
		}

		return wac;
	}
  1. createWebApplicationContext()作用是:

    • 创建子web应用上下文。
    • 将根web应用上下文设置成子web应用上下文的parent。
    • 配置并刷新子web上下文。
	/**
	 * Instantiate the WebApplicationContext for this servlet, either a default
	 * {@link org.springframework.web.context.support.XmlWebApplicationContext}
	 * or a {@link #setContextClass custom context class}, if set.
	 * <p>This implementation expects custom contexts to implement the
	 * {@link org.springframework.web.context.ConfigurableWebApplicationContext}
	 * interface. Can be overridden in subclasses.
	 * <p>Do not forget to register this servlet instance as application listener on the
	 * created context (for triggering its {@link #onRefresh callback}, and to call
	 * {@link org.springframework.context.ConfigurableApplicationContext#refresh()}
	 * before returning the context instance.
	 * @param parent the parent ApplicationContext to use, or {@code null} if none
	 * @return the WebApplicationContext for this servlet
	 * @see org.springframework.web.context.support.XmlWebApplicationContext
	 */
	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");
		}
        // 创建子web应用上下文
		ConfigurableWebApplicationContext wac =
				(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

		wac.setEnvironment(getEnvironment());
        // 将根web应用上下文设置成子web应用上下文的parent
		wac.setParent(parent);
		String configLocation = getContextConfigLocation();
		if (configLocation != null) {
			wac.setConfigLocation(configLocation);
		}
        // 配置并刷新子web上下文
		configureAndRefreshWebApplicationContext(wac);

		return wac;
	}
  1. configureAndRefreshWebApplicationContext()做的事情是:

    • 设置子web应用上下文的id。
    • 加载子web应用上下文的Environment。
    • 先执行全局的ApplicationContextInitializer、再执行当前DispathcetServlet指定的ApplicationContextInitializer。
    • 调用ApplicationContext的refresh()。
	protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    		// 设置子web应用上下文的id
            if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
			// The application context id is still set to its original default value
			// -> assign a more useful id based on available information
			if (this.contextId != null) {
				wac.setId(this.contextId);
			}
			else {
				// Generate default id...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
			}
		}

		wac.setServletContext(getServletContext());
		wac.setServletConfig(getServletConfig());
		wac.setNamespace(getNamespace());
		wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

		// The wac environment's #initPropertySources will be called in any case when the context
		// is refreshed; do it eagerly here to ensure servlet property sources are in place for
		// use in any post-processing or initialization that occurs below prior to #refresh
		// 加载子web应用上下文的Environment
        ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
		}
        postProcessWebApplicationContext(wac);
        //先执行全局的ApplicationContextInitializer、再执行当前DispathcetServlet指定的ApplicationContextInitializer
		applyInitializers(wac);
        //调用ApplicationContext的refresh()
		wac.refresh();
	}

总结

因此,前面的图可以扩展成:

画板