Spring MVC启动流程分析

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

Spring MVC启动流程分析

写代码的木公   2020-03-19 我要评论
本文是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。两者之间的[对比](http://www.cnblogs.com/itsource/p/4352963.html): - 最大的一个区别就是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) ```xml Archetype Created Web Application contextConfigLocation classpath:beans.spring.xml webAppRootKey project.root.path org.springframework.web.util.WebAppRootListener org.springframework.web.context.ContextLoaderListener springmvc org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:springmvc.spring.xml 1 springmvc / ``` **Tomcat启动的时候会依次加载web.xml中配置的Listener、Filter和Servlet。所以根据上面的配置,会首先加载ContextLoaderListener,这个类继承了ContextLoader,用来初始化Spring根上下文,并将其放入ServletContext中**。下面就以这个为入口分析下代码。 Tomcat容器首先会调用调用ContextLoadListener的contextInitialized()方法,这个方法又调用了父类ContextLoader的initWebApplicationContext()方法。下面是这个方法的源代码。 ```java 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的中配置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方法。** ```java //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()方法 ```java //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,因为DispatcherServlet实质是一个Servlet,所以会先执行它的init方法。这个init方法在**HttpServletBean**这个类中实现,其主要工作是做一些初始化工作,将我们在web.xml中配置的参数书设置到Servlet中,然后再触发FrameworkServlet的initServletBean()方法; - **FrameworkServlet**主要作用是初始化Spring子上下文,设置其父上下文,并将其放入ServletContext中; - **FrameworkServlet**在调用initServletBean()的过程中同时会触发**DispatcherServlet**的onRefresh()方法,这个方法会初始化各个功能的实现类。比如异常处理器、视图处理器、请求映射处理等。 ## 博客参考 - [springmvc源码分析系列文章](http://blog.csdn.net/columnhttps://img.qb5200.com/download-x/details/13023.html?&page=3) - [开涛的博客](https://www.iteye.com/blogs/subjects/kaitao-springmvc) - [系列博客](https://www.cnblogs.com/fangjian0423/p/springMVC-directory-summary.html)

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们