Spring MVC 启动过程源码分析详解

今天小编尝试从源码层面上对Spring mvc的初始化过程进行分析,一起揭开Spring mvc的真实面纱,也许我们都已经学会使用spring mvc,或者说对spring mvc的原理在理论上已经能倒背如流。在开始之前,这可能需要你掌握Java EE的一些基本知识,比如说我们要先学会Java EE 的Servlet技术规范,因为Spring mvc框架实现,底层是遵循Servlet规范的。

在开始源码分析之前,我们可能需要一个简单的案例工程,不慌,小编已经安排好了:

样例工程下载地址 : https://github.com/SmallerCoder/spring-mvc-test

那下面就让我们开始吧!

一、前置知识

大家都知道,我们在使用spring mvc时通常会在 web.xml 文件中做如下配置:

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

 <!-- 上下文参数,在监听器中被使用 -->
 <context-param>
 	<param-name>contextConfigLocation</param-name>
 	<param-value>
  	classpath:applicationContext.xml
  </param-value>
 </context-param>

 <!-- 监听器配置 -->
 <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>
 	<init-param>
 		<param-name>contextConfigLocation</param-name>
 		<param-value>classpath:applicationContext-mvc.xml</param-value>
 	</init-param>
 	<load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
 	<servlet-name>dispatcher</servlet-name>
 	<url-pattern>/</url-pattern>
 </servlet-mapping>

</web-app>

上面的配置总结起来有几点内容,分别是:DispatcherServlet

当我们将spring mvc应用部署到tomcat时,当你不配置任何的 context-paramlistener 参数,只配置一个 DispatcherServlet 时,那么tomcat在启动的时候是不会初始化spring web上下文的,换句话说,tomcat是不会初始化spring框架的,因为你并没有告诉它们spring的配置文件放在什么地方,以及怎么去加载。所以 listener 监听器帮了我们这个忙,那么为什么配置监听器之后就可以告诉tomcat怎么去加载呢?因为 listener 是实现了servlet技术规范的监听器组件,tomcat在启动时会先加载 web.xml 中是否有servlet监听器存在,有则启动它们。 ContextLoaderListener 是spring框架对servlet监听器的一个封装,本质上还是一个servlet监听器,所以会被执行,但由于 ContextLoaderListener 源码中是基于 contextConfigLocationcontextClass 两个配置参数去加载相应配置的,因此就有了我们配置的 context-param 参数了, servlet 标签里的初始化参数也是同样的道理,即告诉web服务器在启动的同时把spring web上下文( WebApplicationContext )也给初始化了。

上面讲了下tomcat加载spring mvc应用的大致流程,接下来将从源码入手分析启动原理。

二、Spring MVC web 上下文启动源码分析

假设现在我们把上面 web.xml 文件中的 <load-on-startup>1</load-on-startup> 给去掉,那么默认tomcat启动时只会初始化spring web上下文,也就是说只会加载到 applicationContext.xml 这个文件,对于 applicationContext-mvc.xml 这个配置文件是加载不到的, <load-on-startup>1</load-on-startup> 的意思就是让 DispatcherServlet 延迟到使用的时候( 也就是处理请求的时候 )再做初始化。

我们已经知道spring web是基于 servlet 标准去封装的,那么很明显,servlet怎么初始化, WebApplicationContext web上下文就应该怎么初始化。我们先看看 ContextLoaderListener 的源码是怎样的。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
 // 初始化方法
 @Override
 public void contextInitialized(ServletContextEvent event) {
 	initWebApplicationContext(event.getServletContext());
 }
 // 销毁方法
 @Override
 public void contextDestroyed(ServletContextEvent event) {
 	closeWebApplicationContext(event.getServletContext());
 	ContextCleanupListener.cleanupAttributes(event.getServletContext());
 }
}

ContextLoaderListener 类实现了 ServletContextListener ,本质上是一个servlet监听器,tomcat将会优先加载servlet监听器组件,并调用 contextInitialized 方法,在 contextInitialized 方法中调用 initWebApplicationContext 方法初始化Spring web上下文,看到这焕然大悟,原来Spring mvc的入口就在这里,哈哈~~~赶紧跟进去 initWebApplicationContext 方法看看吧!

initWebApplicationContext() 方法:

// 创建web上下文,默认是XmlWebApplicationContext
if (this.context == null) {
 this.context = createWebApplicationContext(servletContext);
}

if (this.context instanceof ConfigurableWebApplicationContext) {
 ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
 // 如果该容器还没有刷新过
 if (!cwac.isActive()) {
 	if (cwac.getParent() == null) {
 		ApplicationContext parent = loadParentContext(servletContext);
 		cwac.setParent(parent);
 	}
 	// 配置并刷新容器
 	configureAndRefreshWebApplicationContext(cwac, servletContext);
 }
}

上面的方法只做了两件事:

1、如果spring web容器还没有创建,那么就创建一个全新的spring web容器,并且该容器为root根容器,下面第三节讲到的servlet spring web容器是在此根容器上创建起来的

2、配置并刷新容器

上面代码注释说到默认创建的上下文容器是 XmlWebApplicationContext ,为什么不是其他web上下文呢?为啥不是下面上下文的任何一种呢?

我们可以跟进去 createWebApplicationContext 后就可以发现默认是从一个叫 ContextLoader.properties 文件加载配置的,该文件的内容为:

代码如下:

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

具体实现为:

protected Class<?> determineContextClass(ServletContext servletContext) {
 // 自定义上下文,否则就默认创建XmlWebApplicationContext
 String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
 if (contextClassName != null) {
  try {
  	return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
  }
  catch (ClassNotFoundException ex) {
  	throw new ApplicationContextException(
  			"Failed to load custom context class [" + contextClassName + "]", ex);
  }
 }
 else {
  // 从属性文件中加载类名,也就是org.springframework.web.context.support.XmlWebApplicationContext
  contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
  try {
  	return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
  }
  catch (ClassNotFoundException ex) {
  	throw new ApplicationContextException(
  			"Failed to load default context class [" + contextClassName + "]", ex);
  }
 }
}

上面可以看出其实我们也可以自定义spring web的上下文的,那么怎么去指定我们自定义的上下文呢?答案是通过在 web.xml 中指定 contextClass 参数,因此第一小结结尾时说 contextClass 参数和 contextConfigLocation 很重要~~至于 contextConfigLocation 参数,我们跟进 configureAndRefreshWebApplicationContext 即可看到,如下图:

总结:

spring mvc启动流程大致就是从一个叫 ContextLoaderListener 开始的,它是一个servlet监听器,能够被web容器发现并加载,初始化监听器 ContextLoaderListener 之后,接着就是根据配置如 contextConfigLocationcontextClass 创建web容器了,如果你不指定 contextClass 参数值,则默认创建的spring web容器类型为 XmlWebApplicationContext ,最后一步就是根据你配置的 contextConfigLocation 文件路径去配置并刷新容器了。

三、 DispatcherServlet 控制器的初始化

好了,上面我们简单地分析了Spring mvc容器初始化的源码,我们永远不会忘记,我们默认创建的容器类型为 XmlWebApplicationContext ,当然我们也不会忘记,在 web.xml 中,我们还有一个重要的配置,那就是 DispatcherServlet 。下面我们就来分析下 DispatcherServlet 的初始化过程。

DispatcherServlet ,就是一个servlet,一个用来处理request请求的servlet,它是spring mvc的核心,所有的请求都经过它,并由它指定后续操作该怎么执行,咋一看像一扇门,因此我管它叫“闸门”。在我们继续之前,我们应该共同遵守一个常识,那就是-------无论是监听器还是servlet,都是servlet规范组件,web服务器都可以发现并加载它们。

下面我们先看看 DispatcherServlet 的继承关系:

看到这我们是不是一目了然了, DispatcherServlet 继承了 HttpServlet 这个类, HttpServlet 是servlet技术规范中专门用于处理http请求的servlet,这就不难解释为什么spring mvc会将 DispatcherServlet 作为统一请求入口了。

因为一个servlet的生命周期是 init() -> service() -> destory() ,那么 DispatcherServlet 怎么初始化呢?看上面的继承图,我们进到 HttpServletBean 去看看。

果不其然, HttpServletBean 类中有一个 init() 方法, HttpServletBean 是一个抽象类, init() 方法如下:

可以看出方法采用 final 修饰,因为 final 修饰的方法是不能被子类继承的,也就是子类没有同样的 init() 方法了,这个 init 方法就是 DispatcherServlet 的初始化入口了。

接着我们跟进 FrameworkServletinitServletBean() 方法:

在方法中将会初始化不同于第一小节的web容器,请记住,这个新的spring web 容器是专门为 dispactherServlet 服务的,而且这个新容器是在第一小节根ROOT容器的基础上创建的,我们在 <servlet> 标签中配置的初始化参数被加入到新容器中去。

至此, DispatcherSevlet 的初始化完成了,听着有点蒙蔽,但其实也是这样,上面的分析仅仅只围绕一个方法,它叫 init() ,所有的servlet初始化都将调用该方法。

总结:

dispactherServlet 的初始化做了两件事情,第一件事情就是根据根web容器,也就是我们第一小节创建的 XmlWebApplicationContext ,然后创建一个专门为 dispactherServlet 服务的web容器,第二件事情就是将你在web.xml文件中对 dispactherServlet 进行的相关配置加载到新容器当中。

三、每个request调用请求经历了哪些过程

其实说到这才是 dispatcherServlet 控制器的核心所在,因为web框架无非就是接受请求,处理请求,然后响应请求。当然了,如果 dispactherServlet 只是单纯地接受处理然后响应请求,那未免太弱了,因此spring设计者加入了许许多多的新特性,比如说拦截器、消息转换器、请求处理映射器以及各种各样的 Resolver ,因此spring mvc非常强大。

dispatcherServlet 类不做相关源码分析,因为它就是一个固定的执行步骤,什么意思呢?一个request进来,大致就经历这样的过程:

接受请求 -----> 是否有各种各样的处理器 Handler -------> 是否有消息转换器 HandlerAdapter --------> 响应请求

上面每一步如果存在相应的组件,当然前提是你在项目中有做相关的配置,则会执行你配置的组件,最后响应请求。因此明白大致的流程之后,如果你想调试一个request,那么你完全可以在 dispatcherServlet 类的 doDispatch 方法中打个断点,跟完代码之后你就会发现其实大致流程就差不多了。

四、后话

本文的工程是基于传统的web.xml加载web项目,当然在spring mvc中我们也可以完全基于注解的方式进行配置,我们可以通过实现 WebApplicationInitializer 来创建自己的web启动器,也可以通过继承 AbstractAnnotationConfigDispatcherServletInitializer 来创建相应的spring web容器(包括上面说到的根容器和servlet web容器),最后通过继承 WebMvcConfigurationSupport 再一步进行自定义配置(相关拦截器,bean等)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Spring SpringMVC在启动完成后执行方法源码解析

    关键字:spring容器加载完毕做一件事情(利用ContextRefreshedEvent事件) 应用场景:很多时候我们想要在某个类加载完毕时干某件事情,但是使用了spring管理对象,我们这个类引用了其他类(可能是更复杂的关联),所以当我们去使用这个类做事情时发现包空指针错误,这是因为我们这个类有可能已经初始化完成,但是引用的其他类不一定初始化完成,所以发生了空指针错误,解决方案如下: 1.写一个类继承spring的ApplicationListener监听,并监控ContextRefresh

  • Spring MVC深入学习之启动初始化过程

    前言 虽然从学java的第一个程序--helloworld至今,已经有好几个年头了.当时自己找资料,看视频,学习了java的输入输出流,多线程,网络编程等等, 而三大框架(Struts.Hibernate.Spring)基本只是开了个头就出来实习了,尤其对于Spring更是没有进行系统的学习, 虽然在实习的时候通过看项目,基本明白了spring mvc编程的框架是怎么回事,遇到需求知道如何写代码,在哪写代码,但是还是缺乏一个系统的认识. 因为最近公司项目使用 struts2 作为控制层框架,为了

  • Spring MVC 启动过程源码分析详解

    今天小编尝试从源码层面上对Spring mvc的初始化过程进行分析,一起揭开Spring mvc的真实面纱,也许我们都已经学会使用spring mvc,或者说对spring mvc的原理在理论上已经能倒背如流.在开始之前,这可能需要你掌握Java EE的一些基本知识,比如说我们要先学会Java EE 的Servlet技术规范,因为Spring mvc框架实现,底层是遵循Servlet规范的. 在开始源码分析之前,我们可能需要一个简单的案例工程,不慌,小编已经安排好了: 样例工程下载地址 : ht

  • Spring启动过程源码分析及简介

    目录 1.BeanDefinition 2.beanFactory 3.BeanDefinitionReader 4.ClassPathBeanDefinitionScanner 5.ConditionEvaluator 6.Aware 本文是通过AnnotationConfigApplicationContext读取配置类来一步一步去了解Spring的启动过程. 在看源码之前,我们要知道某些类的作用,这样更方便后续的了解. 1.BeanDefinition BeanDefinition就是Be

  • 基于Spring + Spring MVC + Mybatis 高性能web构建实例详解

    一直想写这篇文章,前段时间痴迷于JavaScript.NodeJs.AngularJS,做了大量的研究,对前后端交互有了更深层次的认识. 今天抽个时间写这篇文章,我有预感,这将是一篇很详细的文章,详细的配置,详细的注释,看起来应该很容易懂. 用最合适的技术去实现,并不断追求最佳实践.这就是架构之道. 希望这篇文章能给你们带来一些帮助,同时希望你们可以为这个项目贡献你的想法. 源码地址:https://github.com/Eliteams/quick4j 点击打开 源码地址:https://gi

  • Java Spring @Lazy延迟注入源码案例详解

    前言 有时候我们会在属性注入的时候添加@Lazy注解实现延迟注入,今天咱们通过阅读源码来分析下原因 一.一个简单的小例子 代码如下: @Service public class NormalService1 { @Autowired @Lazy private MyService myService; public void doSomething() { myService.getName(); } } 作用是为了进行延迟加载,在NormalService1进行属性注入的时候,如果MyServ

  • Spring mvc 分步式session的实例详解

    Spring mvc 分步式session的实例详解 Session代表服务器与浏览器的一次会话过程,它的信息是保存在服务器端的.在Servlet中,session指的是HttpSession类的对象.服务器在创建session后,会把sessionid以cookie的形式回写给客户端.只要客户端的浏览器不关,每一次访问服务器都会带上这个sessionid.这样就可以在每次请求的时候获取到session的信息. 下面以spring MVC以例来说明如果创建分步式session. 1.login

  • Spring MVC自定义日期类型转换器实例详解

    Spring MVC自定义日期类型转换器实例详解 WEB层采用Spring MVC框架,将查询到的数据传递给APP端或客户端,这没啥,但是坑的是实体类中有日期类型的属性,但是你必须提前格式化好之后返回给它们.说真的,以前真没这样做过,之前都是一口气查询到数据,然后在jsp页面上格式化,最后展示给用户.但是这次不同,这次我纯属操作数据,没有页面.直接从数据库拿数据给它们返数据.它们给我传数据我持久化数据,说到这里一个小问题就默默的来了. 首先把问题还原一下吧(这是一个数据导出功能),下图中用红框圈

  • Python日志打印里logging.getLogger源码分析详解

    实践环境 WIN 10 Python 3.6.5 函数说明 logging.getLogger(name=None) getLogger函数位于logging/__init__.py脚本 源码分析 _loggerClass = Logger # ...略 root = RootLogger(WARNING) Logger.root = root Logger.manager = Manager(Logger.root) # ...略 def getLogger(name=None): "&quo

  • React commit源码分析详解

    目录 总览 commitBeforeMutationEffects commitMutationEffects 插入 dom 节点 获取父节点及插入位置 判断当前节点是否为单节点 在对应位置插入节点 更新 dom 节点 更新 HostComponent 更新 HostText 删除 dom 节点 unmountHostComponents commitNestedUnmounts commitUnmount commitLayoutEffects 执行生命周期 处理回调 总结 总览 commit

  • SpringCloud微服务续约实现源码分析详解

    目录 一.前言 二.客户端续约 1.入口 构造初始化 initScheduledTasks()调度执行心跳任务 2.TimedSupervisorTask组件 构造初始化 TimedSupervisorTask#run()任务逻辑 3.心跳任务 HeartbeatThread私有内部类 发送心跳 4.发送心跳到注册中心 构建请求数据发送心跳 三.服务端处理客户端续约 1.InstanceRegistry#renew()逻辑 2.PeerAwareInstanceRegistryImpl#rene

  • spring mvc DispatcherServlet之前端控制器架构详解

    前端控制器是整个MVC框架中最为核心的一块,它主要用来拦截符合要求的外部请求,并把请求分发到不同的控制器去处理,根据控制器处理后的结果,生成相应的响应发送到客户端.前端控制器既可以使用Filter实现(Struts2采用这种方式),也可以使用Servlet来实现(spring MVC框架). DispatcherServlet 作为前置控制器是web服务器的入口,是spring mvc最重要的一个类,通过它的生命周期可以加深对web服务器的理解. servlet的生命周期 首先我们回忆一下ser

随机推荐