什么是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的继承关系:
比较主要的是这一条线: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
来存放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技术内幕》 计文柯 著