Spring系列-Spring MVC初始化流程

什么是Spring MVC?
Spring MVC是Spring提供的的web框架,类似于Struts。不过在说Spring MVC之前,先说下MVC和三层架构吧,我们在web开发中,经常用到MVC模式,这个MVC指的是:
Model(数据模型) + View(视图) + Controller(控制器),MVC模式在UI设计中使用非常普遍,主要特点是分离了模型,视图,控制器三种角色,将业务处理从UI设计中独立出来,使得相互之间解耦,可以独立扩展而不需要彼此依赖。
三层架构:
Presentation tier(展现层) + Application tier(应用层) + Data tier(数据访问层)。

MVC和三层架构并不是同一回事,三层架构是整个应用角度的架构,应用层对应我们Service类,执行业务流程,数据访问层对应我们的Dao层,进行数据库的操作。展现层,相当于Spring MVC,也就是说,Spring MVC中的Model,View,Controller都是属于三层架构中的展现层。

做Web开发的人,应该都知道SSH架构,就是Struts + Spring + Hibernate,这是Web应用中最常用的技术架构之一,Struts作为Web框架,帮助构建UI,Spring作为应用平台,Hibernate作为数据持久化实现。
不过其实Spring也有Spring MVC,作为Web框架为用户开发Web应用提供支持,而且现在Struts的使用也越来越少了,使用Spring MVC的用户越来越多。

既然知道了Spring MVC是Web框架,那就需要知道,Spring MVC是如何起作用的,和Spring本身有什么关系,请求发送到Spring MVC之后,经过了什么流程,最后返回了Response。这篇文章就先分析下Spring MVC是怎么建立起来的,下一篇再来分析Spring MVC处理Http请求的流程。

Spring MVC是建立在Spring IoC基础上的,Spring IoC的启动和Web容器的启动关联起来,Web容器启动的时候,就把Spring IoC载入到Web容器,并完成初始化。这样,Web容器就有了Spring MVC运行的基础。

Spring IoC容器的初始化
web.xml文件是Java web项目中的一个配置文件,主要用于配置欢迎页、Filter、Listener、Servlet等,但并不是必须的,一个java web项目没有web.xml文件照样能跑起来。Tomcat容器/conf目录下也有作用于全局web应用web.xml文件,当一个web项目要启动时,Tomcat会首先加载项目中的web.xml文件,然后通过其中的配置来启动项目,只有配置的各项没有错误时,项目才能正常启动。
我们在配置Tomcat的web.xml时,一般会这么配置:


        DispatcherServlet
        org.springframework.web.servlet.DispatcherServlet
        1


     DispatcherServlet
     /


    contextConfigLocation
    classpath:spring/applicationContext.xml


    org.springframework.web.context.ContextLoaderListener

在web.xml中,会看到,最先配置了一个DispatcherServlet,使用Spring MVC,配置DispatcherServlet是第一步,DispatcherServlet是前置控制器,拦截匹配的请求。这里还能看到配置了ContextLoaderListener,这个是一个监听器。这个监听器是和Web服务器的生命周期相关的,通过监听Web服务器的生命周期,来完成Spring IoC容器的启动和销毁。Spring IoC容器启动之后,就会把DispatchServlet作为Spring MVC处理Web请求的转发器,完成Http请求的转发和响应。

接下来,就看下ContextLoaderListener的实现:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

	public ContextLoaderListener() {
	}
	
	public ContextLoaderListener(WebApplicationContext context) {
		super(context);
	}

	/**
	 * Initialize the root web application context.
	 */
	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}

	/**
	 * Close the root web application context.
	 */
	@Override
	public void contextDestroyed(ServletContextEvent event) {
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}
}

ContextLoaderListener是Spring提供的类,它实现了ServletContextListener接口:

public interface ServletContextListener extends EventListener {

    /**
     ** Notification that the web application initialization process is starting.
     * All ServletContextListeners are notified of context initialization before
     * any filter or servlet in the web application is initialized.
     * @param sce Information about the ServletContext that was initialized
     */
    public void contextInitialized(ServletContextEvent sce);

    /**
     ** Notification that the servlet context is about to be shut down. All
     * servlets and filters have been destroy()ed before any
     * ServletContextListeners are notified of context destruction.
     * @param sce Information about the ServletContext that was destroyed
     */
    public void contextDestroyed(ServletContextEvent sce);
}

ServletContextListener接口是在Servlet API中定义的,是ServletContext的监听器,提供了与Servlet生命周期结合的回调,contextInitialized和contextDestroyed方法。从方法名就可以看出,contextInitialized是进行ServletContext初始化时候的回调,contextDestroyed是ServletContext关闭时的回调。
接下来,就看下contextInitialized方法的逻辑,contextInitialized调用了initWebApplicationContext方法:

/**
 * 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) {
	// 判断在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) {
			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);
				}
				configureAndRefreshWebApplicationContext(cwac, 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.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;
	}
}

首先要知道,既然这个方法是ServletContext初始化时候的回调,那这个方法就是为了初始化Spring IoC容器的,方法的注释也说明了,是为ServletContext初始化一个WebApplicationContext。

下面再详细看下这个方法,方法在ContextLoader类中,类中有个private WebApplicationContext context;记录根上下文,一开始先判断了ServletContext中是否已经存在了根上下文。如果根上下文存在的话,就直接抛异常。接下来,会判断如果context为空的话,就调用createWebApplicationContext方法,初始化一个WebApplicationContext。接下来就判断context是否为一个ConfigurableWebApplicationContext实例,如果是的话,就载入根上下文的双亲上下文并设置,还会调用configureAndRefreshWebApplicationContext方法进行上下文的配置。最后还会把这个根上下文作为一个attribute设置到ServletContext中去,设置的路径为ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。

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

createWebApplicationContext方法中,会先调用determineContextClass,判断使用什么用的类在Web容器中作为IoC容器,然后通过反射,创建一个WebApplicationContext实例。下面是determineContextClass的实现:

/**
 * Return the WebApplicationContext implementation class to use, either the
 * default XmlWebApplicationContext or a custom context class if specified.
 * @param servletContext current servlet context
 * @return the WebApplicationContext implementation class to use
 * @see #CONTEXT_CLASS_PARAM
 * @see org.springframework.web.context.support.XmlWebApplicationContext
 */
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);
		}
	}
}

从方法的逻辑和注释中可以看到,一开始,先从servletContext中获取contextClassName,如果contextClassName不为空,则使用该class,如果contextClassName为空,则使用默认的,默认的是XmlWebApplicationContext。

上面分析的就是Spring IoC容器在Web容器环境中的初始化过程。梳理下刚才的逻辑:ContextLoaderListener是Web容器配置的监听器,这个监听器用来启动Spring IoC的初始化,初始化的WebApplicationContext作为根上下文存在,这个根上下文会载入到Web容器(设置到ServletContext中)。

DispatcherServlet初始化
在Spring IoC容器初始化后,Web容器还会对DispatcherServlet初始化。
先看下DispatcherServlet的继承关系:
Spring系列-Spring MVC初始化流程_第1张图片
比较主要的是这一条线:DispatcherServlet继承FrameworkServlet,FrameworkServlet继承HttpServletBean,HttpServletBean继承HttpServlet。
DispatcherServlet作为一个Servlet,启动过程是和Servlet启动过程相联系的,在Servlet初始化过程中,Servlet的init方法会被调用,作为DispatcherServlet的基类,HttpServletBean中的init方法也会被调用:

/**
 * Map config parameters onto bean properties of this servlet, and
 * invoke subclass initialization.
 * @throws ServletException if bean properties are invalid (or required
 * properties are missing), or if subclass initialization fails.
 */
@Override
public final void init() throws ServletException {
	if (logger.isDebugEnabled()) {
		logger.debug("Initializing servlet '" + getServletName() + "'");
	}

	// Set bean properties from init parameters.
	try {
		PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
		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) {
		logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
		throw ex;
	}

	// Let subclasses do whatever initialization they like.
	initServletBean();

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

在方法中,可以看到会先获取ServletConfig,设置PropertyValues和BeanWrapper,接下来会调用initServletBean方法,进行Servlet Bean的初始化操作。这个initServletBean会调用子类的实现,也就是FrameworkServlet中的实现:

/**
 * Overridden method of {@link HttpServletBean}, invoked after any bean properties
 * have been set. Creates this servlet's WebApplicationContext.
 */
@Override
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");
	}
}

在该方法中,可以看到主要就是两个方法:initWebApplicationContext和initFrameworkServlet,关键是initWebApplicationContext这个方法,从方法名上就可以看出来,这个是初始化WebApplicationContext这个上下文的,这个上下文是DispatcherServlet持有的上下文,下面看下initWebApplicationContext方法的实现:

/**
 * Initialize and publish the WebApplicationContext for this servlet.
 * 

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

在方法中,会先通过WebApplicationContextUtils的getWebApplicationContext静态方法,获取根上下文,这个根上下文就是保存在ServletContext中的Spring IoC容器,接下来,对自己持有的webApplicationContext进行处理(根上下文是和Web应用对应的上下文,而DispatcherServlet持有的上下文是和Servlet对应的一个上下文),如果webApplicationContext是一个ConfigurableWebApplicationContext实例,则把根上下文设置为当前上下文的parent,如果webApplicationContext为空的话,则调用findWebApplicationContext查找当前的上下文,如果还为空的话,调用createWebApplicationContext方法创建一个上下文,接下来,会调用onRefresh方法,进行初始化,这个onRefresh方法是调用到子类,也就是DispatcherServlet中。最后,把自己建立的这个上下文,设置到ServletContext中。
接下来看一下createWebApplicationContext方法的实现,这个是用来创建DispatcherServlet的上下文的:

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

这个方法的入参是根上下文,在方法中先获取上下文的Class,然后通过反射来实例化。这个getContextClass方法返回默认的类,也就是XmlWebApplicationContext的class,实例化XmlWebApplicationContext,下面就是一些配置的方法,比如设置这个上下文的parent等,configureAndRefreshWebApplicationContext方法是更详细的设置内容:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
	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
	ConfigurableEnvironment env = wac.getEnvironment();
	if (env instanceof ConfigurableWebEnvironment) {
		((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
	}

	postProcessWebApplicationContext(wac);
	applyInitializers(wac);
	wac.refresh();
}

方法最终会调用refresh方法,完成容器的初始化,这个方法在Spring IoC初始化的文章中有过说明。

到这里,DispatcherServlet持有的IoC容器已经建立起来了,这个IoC容器是根上下文的子容器,接下来再看下刚才说到的那个onRefresh方法,这个方法会调用到DispatcherServlet类中,最终会调用initStrategies方法:

/**
 * Initialize the strategy objects that this servlet uses.
 * 

May be overridden in subclasses in order to initialize further strategy objects. */ protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); }

这个方法入参是刚创建出来的那个上下文,方法中就是各种初始化,比如initHandlerMappings,initViewResolvers等。我们就只看一个initHandlerMappings方法吧:

/**
 * Initialize the HandlerMappings used by this class.
 * 

If no HandlerMapping beans are defined in the BeanFactory for this namespace, * we default to BeanNameUrlHandlerMapping. */ private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; if (this.detectAllHandlerMappings) { // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList(matchingBeans.values()); // We keep HandlerMappings in sorted order. AnnotationAwareOrderComparator.sort(this.handlerMappings); } } else { try { HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerMapping later. } } // Ensure we have at least one HandlerMapping, by registering // a default HandlerMapping if no other mappings are found. if (this.handlerMappings == null) { this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); if (logger.isDebugEnabled()) { logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default"); } } }

首先说明下,这个HandlerMapping是后面映射的时候会用到的,把一个request请求映射到具体的Controller上。

DispatcherServlet中用List handlerMappings来存放HandlerMapping,方法中会分两种情况去初始化handlerMappings,判断detectAllHandlerMappings的值,detectAllHandlerMappings默认是true,所以会在当前IoC容器(DispatcherServlet的容器)以及双亲上下文中取导入所有的HandlerMapping Bean,放入List。或者是只在当前容器中获取HandlerMapping。

分析到这里,关于Spring MVC初始化方面的流程就完成了,再梳理一下思路,Web容器启动的时候,利用ContextLoaderListener的监听,初始化Spring IoC容器,这个IoC容器是根上下文,初始化之后,会把这个根上下文设置到Web容器中。接下来再初始化DispatcherServlet的上下文,DispatcherServlet的上下文是根上下文的子上下文,DispatcherServlet的上下文初始化设置了好多东西,比如HandlerMappings,ViewResolvers等。

参考资料:
1.《Spring技术内幕》 计文柯 著

你可能感兴趣的:(Spring)