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方法,这个方法做了很多事情,建议仔细看下)

    博客参考

  • 相关阅读:
    Bridge Design Pattern
    终于写了个自己的简单MVC框架!
    c 第1章的习题 列表 ! :)
    如何创建 linux 下的计划任务运行 php 文件?
    c 的开篇(自学 c 语言)
    一连串问题,都和.net framework有关
    信号与信号量的区别[转]
    永久设置SecureCRT的背景色和文字颜色方案[转]
    do...while(0)的妙用 【转】
    给Ubuntu软件升级命令 [转]
  • 原文地址:https://www.cnblogs.com/54chensongxia/p/12522804.html
Copyright © 2011-2022 走看看