JavaEE 笔记02: Spring Web MVC流程分析

Spring Web MVC流程分析




前言

Spring MVC是Spring系列框架中使用频率最高的部分。不管是Spring Boot还是传统的Spring项目,只要是Web项目都会使用到Spring MVC部分。本篇博客将对其启动流程和工作流程进行简要分析。

Spring Mvc启动流程分析

我们以一个常见的简单web.xml配置进行Spring MVC启动过程的分析,web.xml配置内容如下:

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>

	<!--全局变量配置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>

	<!--过滤器-->
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

	<!--监听器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <!--调度器-->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

根据Java Servlet Specification v4.0,Web应用部署的相关步骤如下:
在这里插入图片描述
当一个Web应用部署到容器内时(如Tomcat),在Web应用开始响应执行用户请求前,以下步骤会被依次执行:

  • 部署描述文件中(如tomcat的web.xml)由<listener>元素标记的事件监听器会被创建和初始化
  • 对于所有事件监听器,如果实现了ServletContextListener接口,将会执行其实现的contextInitialized()方法
  • 部署描述文件中由<filter>元素标记的过滤器会被创建和初始化,并调用其init()方法
  • 部署描述文件中由<servlet>元素标记的servlet会根据的权值按顺序创建和初始化,并调用其init()方法

我们可以知道,Web应用的初始化流程是:先初始化Listener,接着初始化Filter,最后初始化Servlet

Listener初始化

根据上面web.xml的配置,首先定义了<context-param>标签,用于配置一个全局变量,其内容读取后会被放进application中,作为Web应用的全局变量使用,接下来创建listener时会使用到这个全局变量。
<context-param>中定义了一个ContextLoaderListener,这个类继承了ContextLoader,并实现了ServletContextListener接口。

在这里插入图片描述

首先来看一下ServletContextListener的源码:
在这里插入图片描述
该接口只有两个方法:contextInitialized()contextDestroyed()。当Web应用初始化或销毁时会分别调用上述两个方法。

回到ContextLoaderListener,当Web应用初始化时,就会调用contextInitialized()方法:
在这里插入图片描述
这个方法又调用了父类ContextLoaderinitWebApplicationContext()方法。下面是该方法的源代码:

/**
 * 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.
		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.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;
	}
}

initWebApplicationContext()方法的主要目的就是创建根WebApplicationContext对象,即根IoC容器。其中需要注意的是,整个Web应用如果存在根IoC容器,则有且只能有一个根IoC容器作为全局变量存储在ServletContext,即application对象中。

根IoC容器放入到application对象之前,进行了IoC容器的配置和刷新操作,调用了configureAndRefreshWebApplicationContext()方法,该方法源码如下:

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

该方法主要就是获取到了web.xml中的<context-param>标签配置的全局变量contextConfigLocation,并在最后调用了refresh()方法。其中ConfigurableWebApplicationContext是一个接口。
在这里插入图片描述
通过IDEA的导航功能,可以发现一个抽象类AbstractApplicationContext实现了refresh()方法,其源码如下:

@Override
public void refresh() throws BeansException, IllegalStateException {
	synchronized (this.startupShutdownMonitor) {
		// Prepare this context for refreshing.
		prepareRefresh();

		// Tell the subclass to refresh the internal bean factory.
		ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

		// Prepare the bean factory for use in this context.
		prepareBeanFactory(beanFactory);

		try {
			// Allows post-processing of the bean factory in context subclasses.
			postProcessBeanFactory(beanFactory);

			// Invoke factory processors registered as beans in the context.
			invokeBeanFactoryPostProcessors(beanFactory);

			// Register bean processors that intercept bean creation.
			registerBeanPostProcessors(beanFactory);

			// Initialize message source for this context.
			initMessageSource();

			// Initialize event multicaster for this context.
			initApplicationEventMulticaster();

			// Initialize other special beans in specific context subclasses.
			onRefresh();

			// Check for listener beans and register them.
			registerListeners();

			// Instantiate all remaining (non-lazy-init) singletons.
			finishBeanFactoryInitialization(beanFactory);

			// Last step: publish corresponding event.
			finishRefresh();
		}

		catch (BeansException ex) {
			if (logger.isWarnEnabled()) {
				logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex);
			}

			// Destroy already created singletons to avoid dangling resources.
			destroyBeans();

			// Reset 'active' flag.
			cancelRefresh(ex);

			// Propagate exception to caller.
			throw ex;
		}

		finally {
			// Reset common introspection caches in Spring's core, since we
			// might not ever need metadata for singleton beans anymore...
			resetCommonCaches();
		}
	}
}

该方法主要用于创建并初始化contextConfigLocation类配置的xml文件中的Bean,因此,如果我们在配置Bean时出错,在Web应用启动时就会抛出异常,而不是等到运行时才抛出异常。

至此,ContextLoaderListener类的启动过程就结束了。可以发现,创建ContextLoaderListener是比较核心的一个步骤,主要工作就是创建了根IoC容器并使用特定的key将其放入到application对象中,供整个Web应用使用。

由于在ContextLoaderListener类中构造的根IoC容器配置的Bean是全局共享的,因此,在<context-param>标识的contextConfigLocation的xml配置文件一般包括:数据库DataSource、DAO层、Service层、事务等相关Bean。

Filter初始化

本文web.xml中设定了一个characterEncodingFilter,用于解决中文乱码的问题,此处仅对其进行简要介绍。
在这里插入图片描述
characterEncodingFilter继承自抽象类OncePerRequestFilter,实现了其doFilterInternal()方法,在其中对请求与响应进行文字编码相关的操作,源代码如下:

@Override
protected void doFilterInternal(
		HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
		throws ServletException, IOException {

	String encoding = getEncoding();
	if (encoding != null) {
		if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
			request.setCharacterEncoding(encoding);
		}
		if (isForceResponseEncoding()) {
			response.setCharacterEncoding(encoding);
		}
	}
	filterChain.doFilter(request, response);
}

其中FilterChain是一个接口,其只有doFilter()一个方法,定义如下:
在这里插入图片描述
在Web程序开始运行时,Listener完成初始化后,各个过滤器的init()方法就会被调用,且只会被调用一次,顺序取决于web.xml中的定义顺序。程序运行过程中,过滤器会对用户的请求、服务器的响应进行过滤,符合规则就放行,不符合规则就进行拦截或修改,期间的数据就是由FilterChain承载并传递的。关于其具体运作方式,此处就不展开了。

Servlet初始化

Servlet是Spring MVC的核心,用于获取、分发用户请求,并返回服务器响应。大部分情况下使用Spring提供的DispatcherServlet即可,其是Spring MVC中唯一的Servlet。DispatcherServlet的继承关系如下:
在这里插入图片描述

Servlet的生命周期分为3个阶段:初始化,运行和销毁。而其初始化阶段可分为:

  • Servlet容器加载Servlet类,把类的.class文件中的数据读到内存中;
  • Servlet容器中创建一个ServletConfig对象。该对象中包含了Servlet的初始化配置信息;
  • Servlet容器创建一个Servlet对象;
  • Servlet容器调用Servlet对象的init()方法进行初始化。

DispatcherServlet本质上还是一个Servlet,所以初始化也是通过init()方法来完成的。但是,在DispatcherServlet类中并没有直接出现init()方法,而是覆盖了FrameworkServlet类的onRefresh()方法,其中调用了initStrategies()方法:

/**
 * This implementation calls {@link #initStrategies}.
 */
@Override
protected void onRefresh(ApplicationContext context) {
	initStrategies(context);
}

/**
 * Initialize the strategy objects that this servlet uses.
 * <p>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);
}

可以看到,initStrategies()方法对不同功能模块进行了初始化,各模块功能如下:

  • initMultipartResolver():初始化MultipartResolver,用于处理文件上传服务。如果有文件上传,那么就会将当前的HttpServletRequest包装成DefaultMultipartHttpServletRequest,并且将每个上传的内容封装成CommonsMultipartFile对象。需要在dispatcherServlet-servlet.xml中配置文件上传解析器。
  • initLocaleResolver():用于处理应用的国际化问题,本地化解析策略。
  • initThemeResolver():用于定义一个主题。
  • initHandlerMapping():用于定义请求映射关系。
  • initHandlerAdapters():用于根据Handler的类型定义不同的处理规则。
  • initHandlerExceptionResolvers():当Handler处理出错后,会通过此将错误日志记录在log文件中,默认实现类是SimpleMappingExceptionResolver。
  • initRequestToViewNameTranslators():将指定的ViewName按照定义的 RequestToViewNameTranslators替换成想要的格式。
  • initViewResolvers():用于将View解析成页面。
  • initFlashMapManager():用于生成FlashMap管理器。

顺藤摸瓜,可以发现,onRefresh()方法被initWebApplicationContext()方法调用了。
initWebApplicationContext()方法在FrameworkServlet类中:

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

和之前Listener初始化时有点类似,initWebApplicationContext()的主要作用同样是创建一个WebApplicationContext对象,即Ioc容器,不过前文讲过每个Web应用最多只能存在一个根IoC容器,这里创建的实际上是特定Servlet拥有的子IoC容器IoC容器存在父与子的关系,类似于类的继承,父容器不能访问子容器定义的Bean,但子容器访问父容器定义的Bean。对于DispatcherServlet,其父容器就是根IoC容器。只被特定Servlet使用的Bean,一般在其对应的子容器中声明,所以Controller, HandlerAdapter, ViewResolver等Spring MVC组件,就由DispatcherServlet对应的子IoC容器管理。Spring MVC IoC容器关系如下:

在这里插入图片描述
说回方法的调用关系,initWebApplicationContext()initServletBean()中被调用了,而FrameworkServlet继承自HttpServletBean,覆盖了其initServletBean()方法。

来到了HttpServletBean,其initServletBean()方法内容为空,在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 {

	// 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.
	initServletBean();
}

终于找到init()方法了。HttpServletBean继承了HttpServlet,而HttpServlet又继承了GenericServlet,通过调用查找发现init()GenericServlet中被其init()方法调用:

/**
 * Called by the servlet container to indicate to a servlet that the
 * servlet is being placed into service.  See {@link Servlet#init}.
 *
 * <p>This implementation stores the {@link ServletConfig}
 * object it receives from the servlet container for later use.
 * When overriding this form of the method, call 
 * <code>super.init(config)</code>.
 *
 * @param config 			the <code>ServletConfig</code> object
 *					that contains configuration
 *					information for this servlet
 *
 * @exception ServletException 	if an exception occurs that
 *					interrupts the servlet's normal
 *					operation
 * 
 * @see 				UnavailableException
 */
public void init(ServletConfig config) throws ServletException {
	this.config = config;
	this.init();
}

这里稍微有点绕圈子,此处的this.init()是在HttpServletBean中被覆盖的那个方法,而调用它的init(),则是实现了Servlet接口的init()方法。

至此,梳理一下顺序,我们可以得到一张DispatcherServlet初始化的调用逻辑图:
在这里插入图片描述

在执行完onRefresh()方法后,DispatcherServlet就初始化完成了。

Spring MVC工作流程分析

在这里插入图片描述

图中涉及到一些Spring MVC组件:

  • DispatcherServlet:前端控制器(不需要程序员开发)
    用户请求到达前端控制器,它相当于MVC模式中的Controller,DispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求。
  • HandlerMapping:处理器映射器(不需要程序员开发)
    HandlerMapping负责根据用户请求找到Handler,Spring MVC提供了不同的映射器以实现不同的映射方式。
  • Handler:处理器(需要程序员开发)
    Handler是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下,Handler对具体的用户请求进行处理。由于Handler设计到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发Handler。这里区分一下和Spring Controller的关系:Spring Controller是Handler,但Handler不一定是Spring Controller。
  • HandlerAdapter:处理器适配器
    通过HandlerAdapte按照特定的规则对执行Handler,通过扩展适配器可以对更多类型的Handler进行执行。
  • ViewResolver:视图解析器(不需要程序员开发)
    ViewResolver负责将处理结果生成View视图。ViewResolver首先根据逻辑视图名解析成物理视图名,即具体的页面地址,再生成View视图对象,最后对View进行渲染后将处理结果通过页面的展示给用户。Spring MVC框架提供了很多View视图类型,包括:JSTLView、freemarkerView、pdfView等等。
  • View:视图 (需要程序员开发 )
    View是一个接口,实现类支持不同的View类型(jsp、freemarker等),一般情况下需要通过页面标签或者页面模板技术将模型数据通过页面展示给用户,需要由程序员根据业务需求开发具体的页面。
  • Model:模型
    简单来说,模型包括了数据模型(如POJO)和业务模型(如登陆,注册操作等),在MVC模式中凭借Controller层进行操作以在View层表示出来。

流程具体步骤说明:

  1. 用户发送请求至前端控制器(DispatcherServlet)。
  2. 前端控制器(DispatcherServlet)请求处理器映射器(HandlerMapping)查找处理器(Handler)。
  3. 处理器映射器(HandlerMapping)根据配置(如注解或xml)找到相应处理器(Handler),其中可能还包含多个拦截器(Interceptor),返回给前端控制器(DispatcherServlet)。
  4. 前端控制器(DispatcherServlet)请求处理器适配器(HandlerAdapter)去执行相应的处理器(Handler)。
  5. 处理器适配器(HandlerAdapter)交由对应处理器(Handler)执行。
  6. 处理器(Handler)执行完成后返回ModelAndView对象给处理器适配器(HandlerAdapter)。
  7. 处理器适配器(HandlerAdapter)接受处理器(Handler)的返回结果,并将该结果返回给前端控制器(DispatcherServlet)。
  8. 前端控制器(DispatcherServlet)接收处理器适配器(HandlerAdapter)返回的数据和视图信息,请求视图解析器(ViewResolver)解析对应的视图(View)。
  9. 视图解析器(ViewResolver)根据信息解析出具体的视图(View),返回给前端控制器(DispatcherServlet)。
  10. 前端控制器(DispatcherServlet)接收具体视图(View),进行渲染,并将模型(Model)数据填充进去,生成最终的视图 (View)。
  11. 将最终的视图(View)返回给前端控制器(DispatcherServlet)。
  12. 前端控制器(DispatcherServlet)向用户请求作出响应。

参考文章

SpringMVC 启动流程及相关源码分析
Listener、Filter、Servlet的创建及初始化顺序
【Spring MVC】DispatcherServlet详解(容器初始化超详细过程源码分析)

猜你喜欢

转载自blog.csdn.net/Yiang0/article/details/105548595
今日推荐