SpringMVC启动与初始化源码阅读

注:本博客中所用Spring源码都是基于Spring3.1版本


1、SpringMVC的配置

如果我们需要使用SpringMVC,通常我们需要在web.xml中作如下配置:

<servlet>
	<servlet-name>SpringMVC</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:SpringMVC-servlet.xml</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>SpringMVC</servlet-name>
	<url-pattern>/</url-pattern>
</servlet-mapping>

在上面的代码中我们配置了一个Servlet,是SpringMVC提供的DispatcherServlet,这是SpringMVC框架的入口,负责启动SpringMVC并且分发HTTP请求的作用。另我们将其设置为load-on-startup,也就是说它会随着web容器一起启动;init-param元素用于指定SpringMVC的配置文件,如果我们没有指定该文件,框架则会默认找到WEB-INF/[ServletName]-servlet.xml文件,这里的ServletName指定的就是配置DispatcherServlet时所用的名字,在上面的配置代码中是SpringMVC。

在分析源码之前首先来看一下DispatcherServlet类的继承体系结构:

  SpringMVC启动与初始化源码阅读_第1张图片             SpringMVC启动与初始化源码阅读_第2张图片

由上面两图可以得知,DispatcherServlet通过继承FrameworkServlet和HttpServletBean而继承了HttpServlet,因此可以通过使用Servlet的API来对HTTP请求进行响应,而成为SpringMVC的前端处理器。


2、DispatcherServlet的启动

我们知道在Servlet被初始化时,Servlet的init方法会被调用。既然DispatcherServlet是标准的Servlet实现,那么在其初始化时肯定也会有相应的init方法会被调用,分析源码会发现这个init方法就定义在其父类HttpServletBean中,如下所示:

public final void init() throws ServletException {
	// 日志代码省略

	// 将Servlet的初始化参数设置到自身
	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, this.environment));
		initBeanWrapper(bw);
		bw.setPropertyValues(pvs, true);
	} catch (BeansException ex) {}

	// 子类扩展点,调用由子类实现的initServletBean方法进行具体的初始化.
	initServletBean();
	// 日志代码省略
}

从上面的代码中可以看出,在初始化开始时需要获取Servlet的初始化参数并将其设置到自身的属性中,然后调用了抽象方法initServletBean来进行具体的初始化工作,该方法由子类FrameworkServlet实现。如下所示:

protected final void initServletBean() throws ServletException {
	// 日志代码
	long startTime = System.currentTimeMillis();	// 记录上下文启动开始时间
	// 开始初始化上下文
	try {
		this.webApplicationContext = initWebApplicationContext();
		initFrameworkServlet();
	} catch (ServletException ex) {	// 异常处理代码省略
	} catch (RuntimeException ex) {
	}
	if (this.logger.isInfoEnabled()) {
		long elapsedTime = System.currentTimeMillis() - startTime;	// 得出启动所消耗时间,写入日志代码省略
	}
}
protected WebApplicationContext initWebApplicationContext() {
	// 获取根上下文对象,如果我们在web.xml配置了Spring的ContextLoaderListener,这里会得到其创建的上下文对象
	WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
	WebApplicationContext wac = null;
	if (this.webApplicationContext != null) {
		// 如果本DispatcherServlet所持有的上下文已经存在(可以在构造函数中注入),直接使用
		wac = this.webApplicationContext;
		if (wac instanceof ConfigurableWebApplicationContext) {
			ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
			if (!cwac.isActive()) {
				if (cwac.getParent() == null) {
					cwac.setParent(rootContext);	// 将根上下文设置为父上下文
				}
				configureAndRefreshWebApplicationContext(cwac);	// 配置(设置id等)启动该上下文
			}
		}
	}
	if (wac == null) {
		// 如果在构造函数中没有注入上下文对象,查找ServletContext中是否已经注册了一个与本Servlet相关的上下文对象,
		// 如果可以得到一个上下文对象,则说明其已经设置了父上下文对象及已经初始化完成
		wac = findWebApplicationContext();
	}
	if (wac == null) {
		// 上面两步都没有找到上下文对象,则创建一个
		wac = createWebApplicationContext(rootContext);
	}

	if (!this.refreshEventReceived) {
		// 针对上面得到的上下文对象执行初始化操作
		onRefresh(wac);
	}
	if (this.publishContext) {
		// 把当前获取或者建议的上下文对象保存到ServletContext中,使用的索引与当前Servlet名有关
		String attrName = getServletContextAttributeName();
		getServletContext().setAttribute(attrName, wac);
		// 部分日志代码省略
	}
	return wac;
}

由以上可知,DispatcherServlet的初始化首先是获取或者创建一个与该DispatcherServlet相关的上下文对象,并且如果我们通过ContextLoaderListener启动了Spring的根上下文对象,该DispatcherServlet持有的上下文对象会被设置为根上下文对象的子上下文。可以认为,Spring根上下文是和Web应用相对应的一个上下文,而DispatcherServlet持有的上下文是与该Servlet相对应的一个上下文。在SpringMVC中可以有多个DispatcherServlet存在,那么Spring的根上下文也就会作为多个Servlet上下文的双亲上下文。

在取得或者创建完成Servlet上下文后,跟Spring根上下文一样对其进行初始化、命名(这部分代码定义在configureAndRefreshWebApplicationContext方法中,此处省略),然后把它设置到Web容器的上下文中(即ServletContext中);索引名与在web.xml中配置的DispatcherServlet的Servlet名称相关,这样可以保证其在Web上下文中的唯一性。


3、FrameworkServlet建立WebApplicationContext

下面我们跟踪到上一步代码中的createWebApplicationContext(rootContext)方法中去看一下DispatcherServlet持有的上下文是怎样建立起来的。代码如下:

protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
	Class<?> contextClass = getContextClass();	// 取得容器类的Class对象
	// 日志代码省略
	if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
		// 这里判断所要使用的容器类是否为ConfigurableWebApplicationContext类,如果不是则抛出异常
	}
	// 实例化具体的上下文对象,并为该上下文对象设置双亲上下文及配置文位置
	ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	wac.setParent(parent);
	wac.setConfigLocation(getContextConfigLocation());
	// 配置刚创建的上下文并执行初始化
	configureAndRefreshWebApplicationContext(wac);

	return wac;
}

代码逻辑很简单,首先取得需要实例化上下文的Class对象,然后实例化并设置相关属性,再进行其它配置(比如ID、资源、双亲上下文等)及初始化后返回上下文实例。这里需要注意的是getContextClass方法,该方法返回一个Class对象,其实是成员变量contextClass,而该成员变量被赋值成常量DEFAULT_CONTEXT_CLASS的值,那么常量DEFAULT_CONTEXT_CLASS的值又是什么呢?其实就是XmlWebApplicationContext.class,也就是说这里用的上下文也是XmlWebApplicationContext类型。

定义如下:

public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;
private Class<?> contextClass = DEFAULT_CONTEXT_CLASS;
public Class<?> getContextClass() {
	return this.contextClass;
}

分析到这里DispatcherServlet中持有的IoC容器已经建立起来了,而且这个IoC容器是Spring根上下文的子上下文(如果我们启动了Spring根上下文的话)。然后开始SpringMVC其它模块的初始化。


4、SpringMVC的初始化

从第2步DispatcherServlet上下文创建完成之后,随后程序进入了另外一个方法onRefresh的执行,onRefresh在FrameworkServlet中是个protected权限的方法并作了空实现,具体的实现在子类也就是DispatcherServlet中。代码如下:

protected void onRefresh(ApplicationContext context) {
	initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
	initMultipartResolver(context);
	initLocaleResolver(context);
	initThemeResolver(context);
	initHandlerMappings(context);
	initHandlerAdapters(context);
	initHandlerExceptionResolvers(context);
	initRequestToViewNameTranslator(context);
	initViewResolvers(context);
	initFlashMapManager(context);
}

onRefresh直接调用了initStrategies方法,在该方法中,启动了整个SpringMVC框架的初始化,包括对各种MVC框架元素的初始化,比如支持国际化的LocalResovler、视图生成的ViewResolver及支持HTTP映射的HandlerMapping等,这一大串的MVC元素的初始化从调用的一大串方法名字就可以理解。在这里不进行一一跟踪分析,仅拿HandlerMapping的初始化作为例子来分析其初始化过程。

HandlerMapping的作用是为HTTP请求找到相应的Controller控制器,从而可以利用其执行逻辑操作。初始化代码如下:

private void initHandlerMappings(ApplicationContext context) {
	this.handlerMappings = null;

	if (this.detectAllHandlerMappings) {	// 查找所有的HandlerMapping,包括双亲上下文中定义的,也是默认情况
		Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
		if (!matchingBeans.isEmpty()) {
			this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
			OrderComparator.sort(this.handlerMappings);	// 排序,基于Spring提供的Ordered接口,也就是说HandlerMapping实现了Ordered接口
		}
	} else {	// 只获取当前上下文中的HandlerMapping
		try {
			HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
			this.handlerMappings = Collections.singletonList(hm);
		} catch (NoSuchBeanDefinitionException ex) {
			// 忽略异常,如果没有配置,则添加默认的HandlerMapping
		}
	}

	// 如果上面没有找到HandlerMapping配置,则加载默认的实现
	if (this.handlerMappings == null) {
		this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
		// 日志代码省略
	}
}

HandlerMapping的初始化也很简单明了,首先判断是否需要加载所有的HandlerMapping,这是由成员变量detectAllHandlerMappings来决定的,该成员变量默认设置为true,即加载所有的HandlerMapping Bean,这些Bean可以是当前DispatcherServlet上下文中的,也可以是双亲上下文中的Bean,然后将其按配置排序;如果我们在配置文件中指定了detectAllHandlerMappings为false,则只会加载本DispatcherServlet所持有上下文中的HandlerMapping Bean,并且Bean的名字为HANDLER_MAPPING_BEAN_NAME常量所定义的值(HANDLER_MAPPING_BEAN_NAME常量值为handlerMapping)。

最后,如果我们没有配置HandlerMapping,也就是在上下文中找不到HandlerMapping Beans,程序默认会为我们设定默认的HandlerMapping,至于加载何种HandlerMapping实现以及如何加载,那就需要到getDefaultStrategies方法中一探究竟了,首先来看一下这个方法的定义:

protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
	String key = strategyInterface.getName();
	String value = defaultStrategies.getProperty(key);
	if (value != null) {
		String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
		List<T> strategies = new ArrayList<T>(classNames.length);
		for (String className : classNames) {
			try {
				Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
				Object strategy = createDefaultStrategy(context, clazz);
				strategies.add((T) strategy);
			} catch (ClassNotFoundException ex) {
			} catch (LinkageError err) {
			}
		}
		return strategies;
	} else {
		return new LinkedList<T>();
	}
}

这是一个范型化的方法,其实在initStrategies方法中调用的众多MVC元素的初始化方法进行元素初始化过程中,在配置文件中没有指定所用元素实现的情况都会走到这个方法来加载默认的实现,仅以接口的Class对象来区分需要加载何种元素的默认实现。从以上代码也可以看出,MVC元素的默认实现描述是从defaultStrategies这个对象中获取的,而defaultStrategies从这里看貌似是一个Properties对象,而且其中以各MVC元素接口name为key、实现类的全限定类名为value定义了SpringMVC各元素的默认实现策略。我们向上查找其定义的位置,果不其然,在DispatcherServlet的常量声明区域可以看到,这是一个Properties类型的常量,而且该常量是在一个static静态代码块中进行赋值,如下所示:

private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
private static final Properties defaultStrategies;
static {
	try {
		ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
		defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
	} catch (IOException ex) {
		throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage());
	}
}

这段代码显示,defaultStrategies常量加载的是DispatcherServlet.properties配置文件中的内容,这个文件可以在org.springframework.web.servlet-3.1.2.RELEASE.jar文件中找到,里面定义了一些SpringMVC各元素在没有配置的情况下的默认实现,这里不再给出其内容。

SpringMVC其它元素的初始化和HandlerMapping的初始化过程类似,也是从IoC容器中读入配置,在没有配置的情况下会启用默认的配置,这里不再跟踪分析。


5、总结

从以上分析可知,SpringMVC的初始化是建立在IoC容器初始化完成基础之上的,并且DispatcherServlet的初始化过程中主要做了两件事:

其一,初始化SpringMVC持有的上下文实例,此过程有可能指定双亲上下文为ContextLoaderListener加载的Spring根上下文对象;

其二,初始化SpringMVC所要使用的各种元素。

另外,在SpringMVC中可以配置多个DispatcherServlet,并通过配置时指定的Servlet名字进行区分,每个DispatcherServlet有自己的命名空间,也都持有自己的上下文对象(类型为WebApplicationContext)。


你可能感兴趣的:(SpringMVC启动与初始化源码阅读)