第9章 创建Spring MVC之器

​ 本章将分析Spring MVC自身的创建过程。首先分析Spring MVC的整体结构,然后具体分析每一层的创建过程。

1 整体结构介绍

Spring MVC中核心Servlet的继承结构如图9-1所示。

图9-1 Spring MVC核心Servlet结构图

​ 可以看到在Servlet的继承结构中一共有5个类,GenericServlet和HttpServlet在java中,前面已经讲过,剩下的三个类HttpServletBean、FrameworkServlet和DispatcherServlet是Spring MVC中的,本章主要讲解这三个类的创建过程。

​ 这三个类直接实现三个接口:EnvironmentCapable、EnvironmentAware和Application-ContextAware。XXXAware在spring里表示对XXX可以感知,通俗点解释就是:如果在某个类里面想要使用spring的一些东西,就可以通过实现XXXAware接口告诉spring,spring看到后就会给你送过来,而接收的方式是通过实现接口唯一的方法set-XXX。比如,有一个类想要使用当前的ApplicationContext,那么我们只需要让它实现ApplicationContextAware接口,然后实现接口中唯一的方法void setApplicationContext(ApplicationContext applicationContext)就可以了,spring会自动调用这个方法将applicationContext传给我们,我们只需要接收就可以了!很方便吧!EnvironmentCapable,顾名思义,当然就是具有Environment的能力,也就是可以提供Environment,所以EnvironmentCapable唯一的方法是Environment getEnvironment(),用于实现EnvironmentCapable接口的类,就是告诉spring它可以提供Environment,当spring需要Environment的时候就会调用其getEnvironment方法跟它要。

​ 了解了Aware和Capable的意思,下面再来看一下ApplicationContext和Environment。前者相信大家都很熟悉了,后者是环境的意思,具体功能与之前讲过的ServletContext有点类似。实际上在HttpServletBean中Environment使用的是StandardServletEnvironment(在createEnvironment方法中创建),这里确实封装了ServletContext,同时还封装了ServletConfig、JndiProperty、系统环境变量和系统属性,这些都封装到了其propertySources属性下。为了让大家理解得更深刻,在前面项目的GoController中获取Environment,然后通过调试看一下。首先让GoController实现EnvironmentAware接口,这样spring就会将Environment传给我们,然后在处理请求的方法中加入断点,这样就可以查看spring传进来的内容了。修改后代码如下:

package spring.tutorial.springmvc.controller;@Controller
public class GoController implements EnvironmentAware {private final Log logger = LogFactory.getLog(GoController.class);private Environment environment = null;@RequestMapping(value = {"/"},method = {RequestMethod.HEAD})public String head() {return "go.jsp";}@RequestMapping(value = {"/index", "/"},method = {RequestMethod.GET})public String index(Model model) throws Exception {logger.info("======processed by index=======");// 这里设置断点model.addAttribute("msg", "Go Go Go!");return "go.jsp";}@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;}
}

​ 然后启动调试,并且打开浏览器发送一个根路径的请求。程序会中断到我们设置的断点的地方。这时将鼠标放到变量上就可以看到其内容了,如图9-2所示。
可以看到propertySources中确实包含前面所说的5个属性,然后再来看一下ServletConfig-PropertySource的内部结构,如图9-3所示。

图9-2 Environment结构及内容图

图9-3 ServletConfigPropertySource结构图

​ 从图中可以看到ServletConfigPropertySource的source的类型是StandardWrapperFacade,也就是Tomcat里定义的ServletConfig类型,所以ServletConfigPropertySource封装的就是ServletConfig。在web.xml中定义的contextConfigLocation可以在config下的parameters里看到,这里还可以看到name以及parent等属性。当然,这里的config是私有的,不可以直接调用,config其实是Tomcat中的StandardWrapper——存放Servlet的容器(图9-4)。

2 HttpServletBean

​ HttpSerlvetBean继承自HttpServlet。

​ HttpServletBean覆写了init方法,对初始化过程做了一些处理。 我们来看下init方法到底做了什么:

org.springframework.web.servlet.HttpServletBean:

<servlet><servlet-name>dispatcher</servlet-name>  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  <load-on-startup>1</load-on-startup>  <init-param><param-name>contextConfigLocation</param-name>  <param-value>classpath:springConfig/dispatcher-servlet.xml</param-value>  </init-param>
</servlet><servlet-mapping><servlet-name>dispatcher</servlet-name>  <url-pattern>/</url-pattern>
</servlet-mapping>

​ 比如上面这段配置,传递了contextConfigLocation参数,之后构造BeanWrapper,这里使用BeanWrapper,有2个理由:

    1. contextConfigLocation属性在FrameworkServlet中定义,HttpServletBean中未定义
      2. 利用Spring的注入特性,只需要调用setPropertyValues方法就可将contextConfigLocation属性设置到对应实例中,也就是以依赖注入的方式初始化属性。

​ 然后设置DispatcherServlet中的contextConfigLocation属性(FrameworkServlet中定义)为web.xml中读取的contextConfigLocation参数,该参数用于构造SpringMVC容器上下文。

​ 可以看到,在HttpServletBean的init中,首先将Servlet中配置的参数使用BeanWrapper设置到DispatcherServle的相关属性,然后调用模板方法initServletBean,子类就通过这个方法初始化。

多知道点
BeanWrapper是什么,怎么用
BeanWrapper是Spring提供的一个用来操作JavaBean属性的工具,使用它可以直接修改一个对象的属性,示例如下:

public class User {
String userName;
public String getUserName() {return userName;
}
public void setUserName(String userName) {this.userName = userName;
}
}
public class BeanWrapperTest {
public static void main(String[] args) {User user = new User();BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(user);bw.setPropertyValue("userName", "张三");System.out.println(user.getUserName()); //输出张三PropertyValue value = new PropertyValue("userName", "李四");bw.setPropertyValue(value);System.out.println(user.getUserName()); //输出李四
}
}

这个例子首先新建了一个User对象,其次使用PropertyAccessorFactory封装成Bean-Wrapper对象,这样就可以使用BeanWrapper对象对其属性userName进行操作。Bean-Wrapper的使用就是这么简单,理解了这个例子,也就可以理解HttpServletBean中设置属性的方法了。

3 FrameworkServlet

​ 下面看下FrameworkServlet这个类,FrameworkServlet继承自HttpServletBean。

​ 首先来看下该类覆写的initServletBean方法:

org.springframework.web.servlet.FrameworkServlet

​ 可以看到这里的核心代码只有两句:一句用于初始化WebApplicationContext,另一句用于初始化FrameworkServlet,而且initFrameworkServlet方法是模板方法,子类可以覆盖然后在里面做一些初始化的工作,但子类并没有使用它。这两句代码如下:

// org.springframework.web.servlet.FrameworkServlet.initServletBean
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();

​ 可见FrameworkServlet在构建的过程中的主要作用就是初始化了WebApplicationContext。下面来看一下initWebApplicationContext方法。

这里的根上下文是web.xml中配置的ContextLoaderListener监听器中根据contextConfigLocation路径生成的上下文。

<context-param><param-name>contextConfigLocation</param-name>  <param-value>classpath:springConfig/applicationContext.xml</param-value>
</context-param>
<listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

initWebApplicationContext方法做了三件事:

  • 获取spring的根容器rootContext。

  • 设置webApplicationContext并根据情况调用onRefresh方法。

  • 将webApplicationContext设置到ServletContext中。

    获取spring的根容器rootContext
    获取根容器的原理是,默认情况下spring会将自己的容器设置成ServletContext的属性,默认根容器的key为org.springframework.web.context.WebApplicationContext.ROOT,定义在org.springframework.web.context.WebApplicationContext中。

String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";

所以获取根容器只需要调用ServletContext的getAttribute就可以了。

ServletContext#getAttribute(“org.springframework.web.context.WebApplicationContext.ROOT”)

设置webApplicationContext并根据情况调用onRefresh方法

​ 设置webApplicationContext一共有三种方法。
​ 第一种方法是在构造方法中已经传递webApplicationContext参数,这时只需要对其进行一些设置即可。这种方法主要用于Servlet3.0以后的环境中,Servlet3.0之后可以在程序中使用ServletContext.addServlet方式注册Servlet,这时就可以在新建FrameworkServlet和其子类的时候通过构成方法传递已经准备好的webApplicationContext。
​ 第二种方法是webApplicationContext已经在ServletContext中了。这时只需要在配置Servlet的时候将ServletContext中的webApplicationContext的name配置到contextAttribute属性就可以了。比如,在ServletContext中有一个叫haha的webApplicationContext,可以这么将它配置到Spring MVC中:

<!--Web.xml -->
<servlet><servlet-name>let'sGo</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextAttribute</param-name><param-value>haha</param-value></init-param><load-on-startup>1</load-on-startup>
</servlet>

​ 第三种方法是在前面两种方式都无效的情况下自己创建一个。正常情况下就是使用的这种方式。创建过程在createWebApplicationContext方法中,createWebApplicationContext内部又调用了configureAndRefreshWebApplicationContext方法,代码如下:

// org.springframework.web.servlet.FrameworkServlet
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);
//将设置的contextConfigLocation参数传给wac,默认传入WEB-INFO/[ServletName]-Servlet.xml
wac.setConfigLocation(getContextConfigLocation());configureAndRefreshWebApplicationContext(wac);return wac;
}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 informationif (this.contextId != null) {wac.setId(this.contextId);}else {wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +ObjectUtils.getDisplayString(getServletContext().getContextPath()) + "/" + getServletName());}}wac.setServletContext(getServletContext());wac.setServletConfig(getServletConfig());wac.setNamespace(getNamespace());//添加监听ContextRefreshedEvent的监听器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 #refreshConfigurableEnvironment env = wac.getEnvironment();if (env instanceof ConfigurableWebEnvironment) {((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());}postProcessWebApplicationContext(wac);applyInitializers(wac);wac.refresh();
}

​ 这里首先调用getContextClass方法获取要创建的类型,它可以通过contextClass属性设置到Servlet中,默认使用org.springframework.web.context.support.Xml-WebApplication-Context。然后检查属不属于ConfigurableWebApplicationContext类型,如果不属于就抛出异常。接下来通过BeanUtils.instantiateClass(contextClass)进行创建,创建后将设置的contextConfigLocation传入,如果没有设置,默认传入WEB-INFO/[ServletName]-Servlet.xml,然后进行配置。其他内容基本上都很容易理解,需要说明的是,在configureAndRefreshWebApplicationContext方法中给wac添加了监听器。

wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

​ SourceFilteringListener可以根据输入的参数进行选择,所以实际监听的是ContextRefresh-Listener所监听的事件。ContextRefreshListener是FrameworkServlet的内部类,监听Context-RefreshedEvent事件,当接收到消息时调用FrameworkServlet的onApplicationEvent方法,在onApplicationEvent中会调用一次onRefresh方法,并将refreshEventReceived标志设置为true,表示已经refresh过,代码如下:

// org.springframework.web.servlet.FrameworkServlet
private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {FrameworkServlet.this.onApplicationEvent(event);}
}public void onApplicationEvent(ContextRefreshedEvent event) {this.refreshEventReceived = true;onRefresh(event.getApplicationContext());
}

​ 再回到initWebApplicationContext方法,可以看到后面会根据refreshEventReceived标志来判断是否要运行onRefresh。

// org.springframework.web.servlet.FrameworkServlet.initWebApplicationContext
if (!this.refreshEventReceived) {onRefresh(wac);
}

​ 当使用第三种方法初始化时已经refresh,不需要再调用onRefresh。同样在第一种方式中也调用了configureAndRefreshWebApplicationContext方法,也refresh过,所以只有使用第二种方式初始化webApplicationContext的时候才会在这里调用onRefresh方法。不过不管用哪种方式调用,onRefresh最终肯定会而且只会调用一次,而且DispatcherServlet正是通过重写这个模板方法来实现初始化的。

将webApplicationContext设置到ServletContext中

​ 最后会根据publishContext标志判断是否将创建出来的webApplicationContext设置到ServletContext的属性中,publishContext标志可以在配置Servlet时通过init-param参数进行设置,HttpServletBean初始化时会将其设置到publishContext参数。之所以将创建出来的webApplicationContext设置到ServletContext的属性中,主要是为了方便获取,在前面获取RootApplicationContext的时候已经介绍过。
前面介绍了配置Servlet时可以设置的一些初始化参数,总结如下:

  • contextAttribute:在ServletContext的属性中,要用作WebApplicationContext的属性名称。
  • contextClass:创建WebApplicationContext的类型。
  • contextConfigLocation:Spring MVC配置文件的位置。
  • publishContext:是否将webApplicationContext设置到ServletContext的属性。

4 DispatcherServlet

最后看下DispatcherServlet。

DispatcherServlet覆写了FrameworkServlet中的onRefresh方法:

onRefresh方法是DispatcherServlet的入口方法。onRefresh中简单地调用了initStrategies,在initStrategies中调用了9个初始化方法:

org.springframework.web.servlet.DispatcherServlet

很明显,initStrategies方法内部会初始化各个策略接口的实现类。

比如异常处理初始化initHandlerExceptionResolvers方法:

视图处理初始化initViewResolvers方法:

请求映射处理初始化initHandlerMappings方法:

​ 可能有读者不理解为什么要这么写,为什么不将initStrategies的具体实现直接写到onRefresh中呢?initStrategies方法不是多余的吗?其实这主要是分层的原因,onRefresh是用来刷新容器的,initStrategies用来初始化一些策略组件。如果把initStrategies里面的代码直接写到onRefresh里面,对于程序的运行也没有影响,不过这样一来,如果在onRefresh中想再添加别的功能,就会没有将其单独写一个方法出来逻辑清晰,不过这并不是最重要的,更重要的是,如果在别的地方也需要调用initStrategies方法(如需要修改一些策略后进行热部署),但initStrategies没独立出来,就只能调用onRefresh,那样在onRefresh增加了新功能的时候就麻烦了。另外单独将initStrategies写出来还可以被子类覆盖,使用新的模式进行初始化。

小结

总结一下各个Servlet的作用:

1. HttpServletBean

主要做一些初始化的工作,将web.xml中配置的参数设置到Servlet中。比如servlet标签的子标签init-param标签中配置的参数。

2. FrameworkServlet

将Servlet与Spring容器上下文关联。其实也就是初始化FrameworkServlet的属性webApplicationContext,这个属性代表SpringMVC上下文,它有个父类上下文,既web.xml中配置的ContextLoaderListener监听器初始化的容器上下文。

3. DispatcherServlet

初始化各个功能的实现类。比如异常处理、视图处理、请求映射处理等。

5 DispatcherServlet处理请求过程

在分析DispatcherServlet处理请求过程之前,我们回顾一下Servlet对于请求的处理。

HttpServlet提供了service方法用于处理请求,service使用了模板设计模式,在内部对于http get方法会调用doGet方法,http post方法调用doPost方法…

进入processRequest方法看下:

其中注册的监听器类型为ApplicationListener接口类型。

继续看DispatcherServlet覆写的doService方法:

终就是doDispatch方法。

doDispatch方法功能简单描述一下:

​ 首先根据请求的路径找到HandlerMethod(带有Method反射属性,也就是对应Controller中的方法),然后匹配路径对应的拦截器,有了HandlerMethod和拦截器构造个HandlerExecutionChain对象。HandlerExecutionChain对象的获取是通过HandlerMapping接口提供的方法中得到。有了HandlerExecutionChain之后,通过HandlerAdapter对象进行处理得到ModelAndView对象,HandlerMethod内部handle的时候,使用各种HandlerMethodArgumentResolver实现类处理HandlerMethod的参数,使用各种HandlerMethodReturnValueHandler实现类处理返回值。 最终返回值被处理成ModelAndView对象,这期间发生的异常会被HandlerExceptionResolver接口实现类进行处理。

[Spring mvc 深度解析(三)] 创建Spring MVC之器相关推荐

  1. Spring源码解析之:Spring Security启动细节和工作模式--转载

    原文地址:http://blog.csdn.net/bluishglc/article/details/12709557 Spring-Security的启动加载细节 Spring-Security的 ...

  2. Spring源码解析之:Spring Security启动细节和工作模式

    本文基于spring-security 3.1.1的源码进行分析.本文原文连接: http://blog.csdn.net/bluishglc/article/details/12709557 转载请 ...

  3. Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析

      我们在看Spring Boot源码时,经常会看到一些配置类中使用了注解,本身配置类的逻辑就比较复杂了,再加上一些注解在里面,让我们阅读源码更加难解释了,因此,这篇博客主要对配置类上的一些注解的使用 ...

  4. Spring 源码解析 - Bean创建过程 以及 解决循环依赖

    一.Spring Bean创建过程以及循环依赖 上篇文章对 Spring Bean资源的加载注册过程进行了源码梳理和解析,我们可以得到结论,资源文件中的 bean 定义信息,被组装成了 BeanDef ...

  5. 源码深度解析系列之 Spring IOC

    大家好,我是黎杜,今天和大家聊聊Spring IOC的源码! 历时 4 个多月,终于把 Sping 源码系列写完了,该系列一共 5 篇,后续会整理成 PDF 教程,本文是最后一篇. 这篇文章主要讲解 ...

  6. Spring Boot 2.0(三):Spring Boot 开源软件都有哪些?

    2016年 Spring Boot 还没有被广泛使用,在网上查找相关开源软件的时候没有发现几个,到了现在经过2年的发展,很多互联网公司已经将 Spring Boot 搬上了生产,而使用 Spring ...

  7. [Spring mvc 深度解析(一)] 详解Servlet

    详解Servlet ​ Servlet是Server+Applet的缩写,表示一个服务器应用.通过上面的分析我们知道Servlet其实就是一套规范,我们按照这套规范写的代码就可以直接在Java的服务器 ...

  8. [Spring mvc 深度解析(二)] Tomcat分析

    第7章 Tomcat分析 ​ 前面已经给大家介绍了网站处理请求时所涉及的各种协议和实现方法,不过之前的实现只是为了让大家明白原理而设计的简单示例程序,本章分析一个实际环境中经常使用的具体的实现--To ...

  9. Spring源码解析三

    上一节,我们了解到Spring的基础容器XmlBeanFactory是如果使用的,并且分析了Spring容器的基本原理.那我们要怎样进行Spring的深入分析呢?Spring源码是一个很复杂的框架,因 ...

最新文章

  1. 四川省天然气行业发展战略及投资形势分析报告2021版
  2. 73.JVM内存基础结构,参数分类,推荐的配置项,参数混用问题,常用工具,常用命令
  3. 看别人情侣空间显示服务器繁忙什么意思,调查13000位80后年轻人,他们的感情状态究竟如何?...
  4. 分布式消息中间件rocketmq的原理与实践
  5. ORA-12518: TNS: 监听程序无法分发客户机连接
  6. 循环机换变速箱油教程_汽车变速箱油啥时更换?重力换和循环机换有啥区别?注意啥?...
  7. java 中class相关的问题
  8. C++支持参数个数不确定的函数
  9. Flume中的flume-env.sh和log4j.properties配置调整建议(图文详解)
  10. 计算机408重点知识及其他(面试)
  11. SVN checkout 之后图标没有显示(绿色勾)的问题
  12. 牛客每日练习----mxh与冰红茶的故事,mengxiang000的龙,组合数问题二
  13. 编译和push services.jar的方法
  14. 【复习】Listening and Reading Comprehension
  15. 计算机的应用安全包括哪些方面,计算机软件的分类有哪些
  16. Linux bc命令
  17. Spark-SparkSession.Builder 源码解析
  18. Nova组件源码分析之冷迁移与Resize
  19. javascript怎么清空一个div里面的内容
  20. 多省市成立大数据局,加快大数据产业发展

热门文章

  1. 编者序:初衷、计划、要求、优势、目标和展望
  2. QIIME 2用户文档. 19使用q2-vsearch聚类OTUs(2019.7)
  3. 丰度决定了细菌在复杂群落中的功能作用
  4. java单例模式的实现方法_Java中的五种单例模式实现方法
  5. R语言可视化包ggplot2包改变边距(margin)实战(Modify the Margins)
  6. ValueError: The data property of a figure may only be assigned a list or tuple that contains a .....
  7. GAN(Generative Adversarial Network,GAN)模型之:EBGAN、PGGAN、CGAN、ACGAN模型
  8. Linux aptget 参数,ubuntu下apt-get 命令参数大全
  9. php mysql技术笔试题_PHP面试笔试题--选择题部分(最新整理)
  10. 无人驾驶之车道线检测简易版