Spring 上下文与Spring MVC 上下文

此文章基于Spring 和Spring MVC 的4.3.22.RELEASE版本

源码地址:https://gitee.com/Big_Xin/spring-learn/tree/master/spring-springmvc-demo

先看一下web.xml

<!--Spring的上下文不是必须的,可以只有一个Spring MVC的上下文-->
<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--这边servlet的名称xxx对应配置文件/WEB-INF/xxx-servlet.xml,也可以指定init-param参数contextConfigLocation设置指定文件-->
<servlet>
	<servlet-name>mvc</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<!--这边设置成1,则容器启动时就会初始化这个servlet,否则在第一次访问这个servlet处理的url时才会进行初始化-->
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>mvc</servlet-name>
	<url-pattern>*.do</url-pattern>
</servlet-mapping>

Spring 的上下文

Spring的上下文是由ServletContextListener进行初始化的。

1、由于实现了ServletContextListener,会先执行方法contextInitialized()

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

2、其调用方法initWebApplicationContext()创建上下文。

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!");
	}

	Log logger = LogFactory.getLog(ContextLoader.class);
	servletContext.log("Initializing Spring root WebApplicationContext");
	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.
		if (this.context == null) {
		    //默认会走到这边,进行创建Spring的上下文
			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.
					//这边虽然由给Spring的上下文设置父上下文,但是默认情况下是空的
					ApplicationContext parent = loadParentContext(servletContext);
					cwac.setParent(parent);
				}
				//配置并刷新Spring的上下文
				configureAndRefreshWebApplicationContext(cwac, servletContext);
			}
		}
		//把Spring上下文放入ServletContext中,方便Spring MVC中获取Spring的上下文
		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.isDebugEnabled()) {
			logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
					WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
		}
		if (logger.isInfoEnabled()) {
			long elapsedTime = System.currentTimeMillis() - startTime;
			logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
		}

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

3、由于第一次调用,context属性为空,则调用方法createWebApplicationContext()进行创建上下文。

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);
}

4、需要确认上下文类型,调用determineContextClass()

protected Class<?> determineContextClass(ServletContext servletContext) {
    //这边是扩展点,可以在初始值中设置上下文类
	String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
	if (contextClassName != null) {
		try {
			return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
		}
		catch (ClassNotFoundException ex) {
			throw new ApplicationContextException(
					"Failed to load custom context class [" + contextClassName + "]", ex);
		}
	}
	else {
		contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
		try {
			return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
		}
		catch (ClassNotFoundException ex) {
			throw new ApplicationContextException(
					"Failed to load default context class [" + contextClassName + "]", ex);
		}
	}
}

这边有个初始化参数可以设置上下文类型,如果没有设置则从默认的策略里面找。默认的在静态块中初始化

//默认策略加载文件
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";


private static final Properties defaultStrategies;

static {
	// Load default strategy implementations from properties file.
	// This is currently strictly internal and not meant to be customized
	// by application developers.
	try {
		ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
		defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
	}
	catch (IOException ex) {
		throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
	}
}

它又从ContextLoader.properties文件中读取。

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

5、确认上下文类型后进行实例化,并最后设置相关属性,最后调用refresh()方法刷新容器

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
	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);
	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);
	}

	customizeContext(sc, wac);
	wac.refresh();
}

6、刷新完容器后放入servletContext的属性中。

Spring MVC上下文

Spring MVC的上下文是由DispatcherServlet进行初始化的

1、由于DispatcherServlet的父类实现了Servlet接口,则会调用init()方法进行初始化(这是有入参的init方法哦,千万别看错)。

    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }

2、再调用DispatcherServlet的父类(org.springframework.web.servlet.HttpServletBean)的init()方法,它会根据ServletConfig中的参数进行初始化,最后初始化ServletBean

public final void init() throws ServletException {
	if (logger.isDebugEnabled()) {
		logger.debug("Initializing servlet '" + getServletName() + "'");
	}

	// Set bean properties from init parameters.
	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) {
			if (logger.isErrorEnabled()) {
				logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
			}
			throw ex;
		}
	}

	// Let subclasses do whatever initialization they like.
	//在这边创建SpringMVC的上下文
	initServletBean();

	if (logger.isDebugEnabled()) {
		logger.debug("Servlet '" + getServletName() + "' configured successfully");
	}
}

3、初始化ServletBean时(调用org.springframework.web.servlet.FrameworkServlet的initServletBean()方法),会先初始化WEB上下文(即Spring MVC的上下文)。

protected final void initServletBean() throws ServletException {
	getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
	if (this.logger.isInfoEnabled()) {
		this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
	}
	long startTime = System.currentTimeMillis();

	try {
		this.webApplicationContext = initWebApplicationContext();
		initFrameworkServlet();
	}
	catch (ServletException ex) {
		this.logger.error("Context initialization failed", ex);
		throw ex;
	}
	catch (RuntimeException ex) {
		this.logger.error("Context initialization failed", ex);
		throw ex;
	}

	if (this.logger.isInfoEnabled()) {
		long elapsedTime = System.currentTimeMillis() - startTime;
		this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
				elapsedTime + " ms");
	}
}

4、初始化Spring MVC的上下文之前,会先去servletContext中获取根上下文(即Spring 的上下文)。获取到Spring的上下文之后,在去找Spring MVC的上下文,我这边没有做什么特殊配置,也就是会去创建默认的上下文。

protected WebApplicationContext initWebApplicationContext() {
	//获取之前放在ServletContext中的Spring的上下文
	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
				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);
				}
				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();
	}
	if (wac == null) {
		// No context instance is defined for this servlet -> create a local one
		//默认是走这边创建Spring MVC的上下文
		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.
		onRefresh(wac);
	}

	if (this.publishContext) {
		// Publish the context as a servlet context attribute.
		String attrName = getServletContextAttributeName();
		getServletContext().setAttribute(attrName, wac);
		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
					"' as ServletContext attribute with name [" + attrName + "]");
		}
	}

	return wac;
}

5、创建完默认的XmlWebApplicationContext上下文之后,会把Spring的上下文放入Spring MVC上下文的parent属性中,然后再对Spring MVC上下文进行设值以及refresh。

protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
	Class<?> contextClass = getContextClass();
	if (this.logger.isDebugEnabled()) {
		this.logger.debug("Servlet with name '" + getServletName() +
				"' will try to create custom WebApplicationContext context of class '" +
				contextClass.getName() + "'" + ", using parent context [" + parent + "]");
	}
	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);
	wac.setConfigLocation(getContextConfigLocation());

	configureAndRefreshWebApplicationContext(wac);

	return wac;
}

结合本人平时做项目的一些经验,有以下几点注意:

1、没有Spring的上下文,Spring MVC的上下文也是可以单独存在的。

2、Spring的上下文也是可以有父上下文。

3、如果web.xml中有配置多个DispatcherServlet也就是可以有多个Spring MVC上下文的。

;