zoukankan      html  css  js  c++  java
  • Spring MVC启动流程分析

    本文是Spring MVC系列博客的第一篇,后续会汇总成贴子。


    Spring MVC是Spring系列框架中使用频率最高的部分。不管是Spring Boot还是传统的Spring项目,只要是Web项目都会使用到Spring MVC部分。因此程序员一定要熟练掌握MVC部分。本篇博客就简要分析下Spring MVC的启动流程,帮助我们更好的理解这个框架。


    为什么要写这篇博客

    Spring的MVC框架已经出来很久了,网上介绍这部分的博客有很多很多,而且很多肯定比我自己写的好,那我还为什么要写这篇博客呢。一方面我觉得博客是对自己学习过程的一个记录,另一方面写博客的过程能加深自己对相关技术的理解,也方便以后自己回顾总结。

    Spring MVC简介

    什么是Spring MVC

    要回答这个问题,我们先要说说MVC。MVC是一种设计模式,这种设计模式建议将一个请求由M(Module)、V(View)、C(controller)三个部分进行处理。请求先经过controller,controller调用其他服务层得到Module,最后将Module数据渲染成试图(View)返回客户端。Spring MVC是Spring生态圈的一个组件,一个遵守MVC设计模式的WEB MVC框架。这个框架可以和Spring无缝整合,上手简单,易于扩展。

    解决什么问题

    通常我们将一个J2EE项目项目分为WEB层、业务逻辑层和DAO层。Spring MVC解决的是WEB层的编码问题。Spring MVC作为一个框架,抽象了很多通用代码,简化了WEB层的编码,并且支持多种模板技术。我们不需要像以前那样:每个controller都对应编写一个Servlet,请求JSP页面返回给前台。

    优缺点

    用的比较多的MVC框架有Struts2和Spring MVC。两者之间的对比

    • 最大的一个区别就是Struts2完全脱离了Servlet容器,而SpringMVC是基于Servlet容器的;
    • Spring MVC的核心控制器是Servlet,而Struts2是Filter;
    • Spring MVC默认每个Controller是单列,而Struts2每次请求都会初始化一个Action;
    • Spring MVC配置较简单,而Struts2的配置更多还是基于XML的配置。

    总的来说,Spring MVC比较简单,学习成本低,和Spring能无缝集成。在企业中也得到越来越多的应用。所以个人比较建议在项目中使用Spring MVC。

    启动流程分析

    PS:本文的分析还是基于传统的Tomcat项目分析,因为这个是基础。现在非常流行的Spring Boot项目中的启动流程后续也会写文章分析。其实原理差不多...

    要分析Spring MVC的启动过程,要从它的启动配置说起。一般会在Tomcat的 Web.xml中配置了一个ContextLoaderListener和一个DispatcherServlet。其实ContextLoaderListener是可以不配,这样的话Spring会将所有的bean放入DispatcherServlet初始化的上下文容器中管理。这边我们就拿常规的配置方式说明Spring MVC的启动过程。(PS:Spring Boot启动过程已经不使用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应用对应一个ServletContext实例,这个实例是应用部署启动后,servlet容器为应用创建的。
        ServletContext实例包含了所有servlet共享的资源信息。通过提供一组方法给servlet使用,用来
        和servlet容器通讯,比如获取文件的MIME类型、分发请求、记录日志等。
        http://www.cnblogs.com/nantang/p/5919323.html -->
    <web-app>
    	<display-name>Archetype Created Web Application</display-name>
    
    	<context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:beans.spring.xml</param-value>
        </context-param>
    	<context-param>
    		<param-name>webAppRootKey</param-name>
    		<param-value>project.root.path</param-value>
    	</context-param>
    
    	<!-- 将项目的绝对路径放到系统变量中 -->
    	<listener>
    		<listener-class>org.springframework.web.util.WebAppRootListener</listener-class>
    	</listener>
    	<!--初始化Spring IOC容器,并将其放到servletContext的属性当中-->
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener> 
         
    	<!-- 配置SPRINGMVC-->
    	<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.spring.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>
    
    </web-app>
    

    Tomcat启动的时候会依次加载web.xml中配置的Listener、Filter和Servlet。所以根据上面的配置,会首先加载ContextLoaderListener,这个类继承了ContextLoader,用来初始化Spring根上下文,并将其放入ServletContext中。下面就以这个为入口分析下代码。

    Tomcat容器首先会调用调用ContextLoadListener的contextInitialized()方法,这个方法又调用了父类ContextLoader的initWebApplicationContext()方法。下面是这个方法的源代码。

    public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    //如果ServletContext中已经存在Spring容器则报错
    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) {
                //这里创建了webApplicationContext,默认创建的是XmlWebApplicationContext
                //如果想要自定义实现类,可以在web.xml的<context-param>中配置contextClass这个参数
                //此时的Context还没进行配置,相当于只是个"空壳"
    			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);
    				}
                    //读取Spring的配置文件,初始化父上下文环境
    				configureAndRefreshWebApplicationContext(cwac, servletContext);
    			}
    		}
            //将根上下文存入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;
    	}
    }
    

    至此,Spring的父(根)上下文已经初始化完毕,并且已经存在ServletContext中。

    下面开始分析子上下文的初始化过程。这个过程通过Spring MVC的核心Servlet完成,所以我们也有必要讲下Servlet的生命周期。请求过来,判断Servlet有没创建,没有实例化并调用init方法,后面再调用service方法。我们在配置DispatcherServlet的时候,将其设置为启动时创建实例,所以Tomcat在启动的时候就会创建Spring的子上下文。

    下面是DispatcherServlet的继承结构。

    GenericServlet (javax.servlet)
        HttpServlet (javax.servlet.http)
            HttpServletBean (org.springframework.web.servlet)
                FrameworkServlet (org.springframework.web.servlet)
                    DispatcherServlet (org.springframework.web.servlet)
    

    DispatcherServlet继承了FrameworkServlet,FrameworkServlet又继承了HttpServletBean,HttpServletBean又继承HttpServlet并且重写了init方法,所以创建子上下文时的入口就在这个init方法。

    //HttpServletBean的init是入口方法
    @Override
    public final void init() throws ServletException {
    	if (logger.isDebugEnabled()) {
    		logger.debug("Initializing servlet '" + getServletName() + "'");
    	}
    	// Set bean properties from init parameters.
    	try {
            //读取Servlet配置的init-param,创建DispatcherServlet实例
    		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) {
    		if (logger.isErrorEnabled()) {
    			logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
    		}
    		throw ex;
    	}
    	// HttpServletBean的这个方法中没有做任何事情,子类FrameWorkServlet这个类的initServletBean()方法重写了
        // 这个类,所以后续工作会在这个方法中执行。
    	initServletBean();
    	if (logger.isDebugEnabled()) {
    		logger.debug("Servlet '" + getServletName() + "' configured successfully");
    	}
    }
    

    下面是FrameWorkServlet这个类的initServletBean()方法

    //FrameWorkServlet.initServletBean()
    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 {
            //这里是重点,初始化子Spring上下文
    		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()方法的具体代码

    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
            // 这边是重点,创建Spring子上下文,并将设置其父类上下文
    		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) {
    		// 将Spring子上下文存入ServletContext
    		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;
    }
    

    最后看下DispatcherServlet中的onRefresh()方法,这个方法初始化了很多策略:

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

    到此为止,SpringMVC的启动过程结束了。这边做下SpringMVC初始化总结(不是很详细)

    • HttpServletBean的主要做一些初始化工作,将我们在web.xml中配置的参数书设置到Servlet中;
    • FrameworkServlet主要作用是初始化Spring子上下文,设置其父上下文,并将其和ServletContext关联;
    • DispatcherServlet:初始化各个功能的实现类。比如异常处理、视图处理、请求映射处理等。

    简单总结

    传统的Spring MVC项目启动流程如下:

    • 如果在web.xml中配置了org.springframework.web.context.ContextLoaderListener,那么Tomcat在启动的时候会先加载父容器,并将其放到ServletContext中;
    • 然后会加载DispatcherServlet(这块流程建议从init方法一步步往下看,流程还是很清晰的),因为DispatcherServlet实质是一个Servlet,所以会先执行它的init方法。这个init方法在HttpServletBean这个类中实现,其主要工作是做一些初始化工作,将我们在web.xml中配置的参数书设置到Servlet中,然后再触发FrameworkServlet的initServletBean()方法;
    • FrameworkServlet主要作用是初始化Spring子上下文,设置其父上下文,并将其放入ServletContext中;
    • FrameworkServlet在调用initServletBean()的过程中同时会触发DispatcherServlet的onRefresh()方法,这个方法会初始化Spring MVC的各个功能组件。比如异常处理器、视图处理器、请求映射处理等。(注意这个onRefresh方法,这个方法做了很多事情,建议仔细看下)

    博客参考

  • 相关阅读:
    LeetCode 1275. 找出井字棋的获胜者 Find Winner on a Tic Tac Toe Game
    LeetCode 307. 区域和检索
    LeetCode 1271 十六进制魔术数字 Hexspeak
    秋实大哥与花 线段树模板
    AcWing 835. Trie字符串统计
    Leetcode 216. 组合总和 III
    Mybatis 示例之 复杂(complex)属性(property)
    Mybatis 示例之 复杂(complex)属性(property)
    Mybatis 高级结果映射 ResultMap Association Collection
    Mybatis 高级结果映射 ResultMap Association Collection
  • 原文地址:https://www.cnblogs.com/54chensongxia/p/12522804.html
Copyright © 2011-2022 走看看