博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Spring Boot下Spring MVC框架的启动原理——SpringBoot应用的启动过程——万字长文
阅读量:3960 次
发布时间:2019-05-24

本文共 16227 字,大约阅读时间需要 54 分钟。

文章目录

Spring Boot是一个容纳百川的启动框架,在这套框架体系中,可以非常方便地使用各种基于Spring 实现的功能,这其中也包括 Spring MVC。Spring MVC在Spring Boot框架的支持下,大大地简化了Web开发的过程。

简化开发者们使用SpringMVC并不是一件简单的事,Spring Boot做了很多额外的工作才实现了这个目的。把SpringMVC嵌入到整个SpringBoot框架中,经历了一系列的过程。本章从Spring Boot 应用的启动方法开始,以启动方法为入口进行调试来研究源码的方式,一步步地研究基于Tomcat的内嵌Servlet容器的创建与启动过程,最终分析Spring MVC的核心组件DispatcherServlet的注册过程。通过本章可以学到以下知识点。

  • SpringBoot应用的启动过程。

  • 基于SpringBoot的自动配置生效原理。

  • 内嵌ServletWebServer如何通过自动配置创建和启动。

  • DispatcherServlet 如何通过自动配置注册到ServletWebServer中。

SpringBoot应用的启动过程。就是我们今天要讲的内容,先下一个结论:SpringBoot应用的启动过程可以分成两个步骤执行:创建 SpringApplication和运行 SpringApplication,接下来的章节就是划分成这两个步骤来讲解。

1:创建 SpringApplication

作者在这里,想先给出源码的调试过程,继而再进行源码解读,如果直接上源码解读,可能会给大家带来迷惑,这些方法都是哪来的????

1.1 创建 SpringApplication过程的源码调试

如下是作者做的一个毕业设计管理项目的主程序类,

@SpringBootApplicationpublic class SpringBootGraduationDesignApplication{
public static void main(String[] args) {
4 SpringApplication.run(SpringBootGraduationDesignApplication.class, args); }}

在第4行打上断点,并且以debug模式启动,创建 SpringApplication过程的源码调试步骤流程如下

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

1.2 创建 SpringApplication过程的源码解读

上面我们已经介绍了SpringApplication创建的源码调试,接下来,我们就讲解出现在调试过程中的一些方法。

SpringApplication类

/**静态方法启动springAplication@param source创建上下文使用的资源@param args 启动时的命令行参数@return 返回创建的应用上下文ConfigurableApplicationContext,也就是最常用的Spring的Appl icatinContext*/	public static ConfigurableApplicationContext run(Class
primarySource, String... args) {
//调用下面的重载方法,只传入一个source return run(new Class
[] {
primarySource }, args); }

SpringApplication类

/**静态方法启动springAplication@param source创建上下文使用的资源@param args 启动时的命令行参数@return 返回创建的应用上下文ConfigurableApplicationContext,也就是最常用的Spring的Appl icatinContext*/	public static ConfigurableApplicationContext run(Class
[] primarySources, String[] args) {
//创建SpringApplication实例,执行run方法之后返回ApplicationContext return new SpringApplication(primarySources).run(args); }

本节重点讲解SpringApplication的创建,看一下创建的逻辑,代码如下

SpringApplication类

/**@param sources创建上下文使用的资源数组*/	public SpringApplication(Class
... primarySources) {
//构造时直接进行初始化 this(null, primarySources); }

SpringApplication类

/**初始化方法,初始化一些属性@param sources 创建上下文使用资源数组*/	public SpringApplication(ResourceLoader resourceLoader, Class
... primarySources) {
//把启动类添加到sources列表中 this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); //推断是否是Web环境 this.webApplicationType = WebApplicationType.deduceFromClasspath();// 从spring.factories文件工厂中获取ApplicationContextInitializer并设置到当前实例中 setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class));//从spring.factories文件工厂中获取ApplicationListener并设置到当前实例中 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));//推断启动类 this.mainApplicationClass = deduceMainApplicationClass(); }

如上SpringApplication的构造方法就是创建SpringApplication的过程,总结成如下步骤依次说明

  1. 推断是否是Web环境
  2. 通过spring工厂设置应用组件
  3. 推断main方法所在的启动类

1.2.1 推断是否是Web环境

SpringApplication类中的deduceFromClasspath方法

//基于Netty的WebFlux框架private static final String WEBFLUX_INDICATOR_CLASS = "org."			+ "springframework.web.reactive.DispatcherHandler";//基于Servlet的web框架,多数用于Tomcat的Servlet容器private static final String WEBMVC_INDICATOR_CLASS = "org.springframework."			+ "web.servlet.DispatcherServlet";		/**推断是否是Web环境@return 推断结果:None,Servlet,Reactive*/ 	static WebApplicationType deduceFromClasspath() {
//如果只存在Reactive相关类,则判断为Reactive if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE; } //如果不存在任意一 个Web相关环境的依赖,则判断为非Web环境 for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE; } } //否则都是Servlet环境 return WebApplicationType.SERVLET; }

1.2.2 通过spring工厂设置应用组件

检测特定类型的Web环境相关的类是否存在,接着两行代码逻辑基本相同,从spring.fatories工厂中分别获取Initializer组件类列表和Listener 组件类列表,并对它们进行实例化之后设置到当前应用中,代码如下:

/**从工厂获取指定类型的所有类实例@param type要获取的类型@return 实例化好的所有类*/	private 
Collection
getSpringFactoriesInstances(Class
type) {
//从spring.factories中获取所有type类型的实例对象 return getSpringFactoriesInstances(type, new Class
[] {
}); }
/**从工厂获取指定类型的所有类实例@param type 要获取的类型@param parameterTypes 实例化时使用的构造器的参数类型数组@param args 调用构造器实例化时的构造器参数@return 实例化好的所有类*/private 
Collection
getSpringFactoriesInstances(Class
type, Class
[] parameterTypes, Object... args) {
// 获取当前上下文类加载器,用于Classpath下的加载资源文件 ClassLoader classLoader = getClassLoader();//调用SpringFactoriesLoader.loadFactoryNames获取指定类的所有类型名,使用Set保证没有重复类型名 Set
names = new LinkedHashSet<>( SpringFactoriesLoader.loadFactoryNames(type, classLoader));//遍历names 通过反射获取拥有参数类型为parameterTypes的构造器,反射调用进行实例化 List
instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); AnnotationAwareOrderComparator.sort(instances); return instances; }

获取工厂中指定类对应所有类名的功能,是通过Spring工厂加载器SpringFactoriesLoader的loadFactoryNames方法实现。该方法根据类型Class和类加载器ClassLoader获取工厂中所有该Class类对应的类名列表,其中ClassLoader参数用于获取classpath 下的资源。

SpringFactoriesLoader.loadFactoryNames(type, classLoader));

在loadFactoryNames的逻辑中,会通过ClassLoader 查找classpath 下所有jar包中META-INF路径下的spring.factories文件。而spring. factories文件是properties类型的属性文件,本质上是key=value 类型的配置文件。spring 通过Properties 加载资源文件并生成Properties类型的实例,Properties 最终是key:value类型的map,在spring. factories中,key是类的全名,即通过Class getName()获取到的值。

而value则是用逗号分隔的所有工厂类名的拼接,这个方法就是通过传入的Class参数的全名作为key,获取到spring. factories中定义的所有value即所有的工厂类。

通过断点调试我们可以看到这个Initializer和Listener 组件列表

在这里插入图片描述
在这里插入图片描述
上面获取的组件列表,都是从spring boot包下面的META-INF/spring.factories获取的
spring.factories

# Application Context Initializersorg.springframework.context.ApplicationContextInitializer=\org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\org.springframework.boot.context.ContextIdApplicationContextInitializer,\org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer# Application Listenersorg.springframework.context.ApplicationListener=\org.springframework.boot.ClearCachesApplicationListener,\org.springframework.boot.builder.ParentContextCloserApplicationListener,\org.springframework.boot.context.FileEncodingApplicationListener,\org.springframework.boot.context.config.AnsiOutputApplicationListener,\org.springframework.boot.context.config.ConfigFileApplicationListener,\org.springframework.boot.context.config.DelegatingApplicationListener,\org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\org.springframework.boot.context.logging.LoggingApplicationListener,\org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

在获取到所有工厂类类名并通过反射实例化之后,把上面这些类型的实例设置到当前SpringAplication中。

1.2.3 推断main方法所在的启动类

main这个方法很有意思,在Java已有的API中,并没有直接获取main方法,也就是应用启动入口所在的类的API,那么这个方法是如何做推断的呢???

/**推动启动入口所在的类*/private Class
deduceMainApplicationClass() {
try {
// 通过创建一 个异常,获取当前方法执行的调用栈信息 StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) {
// 从底到顶遍历栈,找到方法名是main的栈,尝试推断该栈中的Class就是启动类 if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName()); } } } catch (ClassNotFoundException ex) {
// Swallow and continue } return null; }

创建一个异常,再从异常栈中获取信息。

如下是调试过程中,变量窗口获取到的值
在这里插入图片描述

至此SpringApplication的创建过程分析完,接下来进入启动阶段。

2:运行 SpringApplication

SpringApplication类

public static ConfigurableApplicationContext run(Class
[] primarySources, String[] args) {
3 return new SpringApplication(primarySources).run(args); }

由第3行代码可知,在SpringApplication创建完成后,可以调用run方法启动应用,传入String[]类型的参数用的启动参数。在run方法中,有准备环境、准备上下文、刷新上下文、启动完成等这些步骤构成了整个应用的启动逻辑。下面详细分析run方法,代码如下:

public ConfigurableApplicationContext run(String... args) {
//记录应用的启动时间 StopWatch stopWatch = new StopWatch();//启动停表 stopWatch.start(); ConfigurableApplicationContext context = null;//可配置应用上下文 Collection
exceptionReporters = new ArrayList<>(); configureHeadlessProperty();//SpringApplicationRunListener的容器,用于监听SpringApplication的各个启动事件 SpringApplicationRunListeners listeners = getRunListeners(args);//触发启动中的事件 listeners.starting(); try {
//通过启动参数生成应用参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args);//准备可配置的环境,在ApplicationContext中使用此环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment);//创建ioc容器 创建ApplicationContext ( 应用上下文的创建) context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] {
ConfigurableApplicationContext.class }, context);// 准备ApplicationContext IOC容器的基本信息 ( 应用上下文的准备) prepareContext(context, environment, listeners, applicationArguments, printedBanner);//刷新IOC容器 (应用上下文的刷新 ) refreshContext(context); afterRefresh(context, applicationArguments);//启动停表 stopWatch.stop(); if (this.logStartupInfo) {
//打印启动日志 new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); }//调用启动完成的事件回调 listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) {
//异常 } try {
//正常启动,调用运行中事件回调 listeners.running(context); } catch (Throwable ex) {
//异常 } return context; }

如上方法有几个关键点,分别是prepareEnvironment(应用环境的创建与准备) 、createApplicationContext(应用上下文的创建)、prepareContext (应用上下文的准备)、refreshContext (应用上下文的刷新),接下来依次分析整个过程。

2.1 应用环境的创建与准备

代码如下

/**准备应用环境@param listeners Spring应用的启动监听器,用于处理些事件回调@param applicationArguments启动时传入的启动参数的封装,一般以 “--参数名一参数值”方式配置参数@return 返回可配置的应用环境*/private ConfigurableEnvironment prepareEnvironment(			SpringApplicationRunListeners listeners,			ApplicationArguments applicationArguments) {
//根据情况获取或者创建环境。SpringApplication构造时可以主动指定环境。//如果是自动获取的环境,则会根据当前Web类型返回不同的环境//Servlet 环境返回StandardServletEnvironment//其他情况返回StandardEnvironment ConfigurableEnvironment environment = getOrCreateEnvironment();//配置应用环境,添加些默认的PropertySource 属性源 configureEnvironment(environment, applicationArguments.getSourceArgs());//调用监听器的环境已准备事件 listeners.environmentPrepared(environment);// 环境初始化完毕,绑定环境中一些属性到当前SpringApplicationt bindToSpringApplication(environment);//如果当前不是Web环境,则把环境做转换 if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()) .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment; }

在这一步中, 会加载以下数据源。

  • 通过 System.getProperties()获取 Jvm相关变量,通过Java 启动参数-Dkey=value指定。

  • 通过System.getenv()获取的当前系统的环境变量,Linux 通过export key=value指定,Windows 通过set key=value指定。

  • 通过命令行参数获取,使用应用启动参数-key=value来配置。

  • 通过 resources 或者resources/config 目录下的配置 文件application.yml 或者application.properties指定。

其中我们所熟知的Spring Boot的application.yml或者application.properties配置文件的属性加载,就和listeners.environmentPrepared事件相关,ConfigFileApplicationListener 的onApplicationEnvironmentPreparedEvent方法的执行逻辑中,会对resources目录下的文件进行加载并绑定到环境中。

环境创建和准备完毕,就开始创建应用上下文。

2.2 应用上下文的创建(创建IOC容器)

创建上下文的逻辑在createApplicationContext逻辑里面,我们来看一下具体应用上下文的创建,代码如下

public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."			+ "annotation.AnnotationConfigApplicationContext";/**根据环境创建对应类型的ApplicationContext@return 返回创建的应用上下文ConfigurableApplicationContext , 也就是最常用的Spring的ApplicationContext*/protected ConfigurableApplicationContext createApplicationContext() {
Class
contextClass = this.applicationContextClass;//如果当前配置中的applicationContextClass为空则根据环境自动获取,否则直接以配置为准 if (contextClass == null) {
try {
switch (this.webApplicationType) {
//根据是web环境类型,返回不同的ApplicationContext类型 case SERVLET: contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); break; case REACTIVE: contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default://默认是AnnotationConfigApplicationContext contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); } } catch (ClassNotFoundException ex) {
// } }//实例化应用上下文类 return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); }

通过断点调试我们可以知道,在web环境下,使用的是org.springframework.context.annotation.AnnotationConfigApplicationContext上下文。下面我们对上下文的准备进行阐述

在这里插入图片描述

2.3 应用上下文的准备(准备IOC容器的基本信息)

具体代码如下

private void prepareContext(ConfigurableApplicationContext context,			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,			ApplicationArguments applicationArguments, Banner printedBanner) {
//保存环境信息 context.setEnvironment(environment);//IOC容器的后置处理流程。 postProcessApplicationContext(context);//应用初始化器 applyInitializers(context);//所有的监听器 调用 contextLoaded listeners.contextPrepared(context); if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); } // Add boot specific singleton beans ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner); } if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory) .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); }//加载并获取到当前应用的配置资源 Set sources = getAllSources(); Assert.notEmpty(sources, "Sources must not be empty");//加载配置资源到应用上下文中 load(context, sources.toArray(new Object[0]));//调用监听器的上下文已加载回调事件 listeners.contextLoaded(context); }

2.4 应用上下文的刷新(刷新IOC容器)

通过上面的过程,得知使用应用上下文是AnnotationConfigServletWebServerApplicationContext类型。在run方法的refreshContext逻辑中,会调用生成ConfigApplicationContext上下文的refresh方法对创建的上下文进行刷新,刷新后,整个Spring应用启动起来

对于AnnotationConfigServletWebServerApplicationContext类型的ConfigurableApplication-Context,它的refresh 方法是在父类ServletWebServerApplicationContext 中的重写,通过断点调试来验证此结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如上调用父类,也就是AbstractApplicationContext的refresh方法
在这里插入图片描述
ServletWebServerApplicationContext 类中的refresh方法。如下才是真正的刷新上下文的执行方法

/**刷新上下文*/	@Override	public final void refresh() throws BeansException, IllegalStateException {
try {
//调用父类,也就是AbstractApplicationContext的refresh方法 //父类的refresh是个模板方法,其中执行了很多抽象方法,子类根据情况重写抽象方法,达到模板化refresh整个应用上下文 super.refresh(); } catch (RuntimeException ex) {
stopAndReleaseWebServer(); throw ex; } }

AbstractApplicationContext

在这里插入图片描述

3:一些想说的话

上面说了这么多,大家是不是都没感觉到这跟Spring MVC框架的启动原理,连DispatcherServlet的创建和注册都没看见呀!!!!!上面讲的这些好比吃饭时,服务员肯定要先上小菜才可以上大餐啊!!! SpringBoot应用的启动过程中有一个刷新ioc容器方法(refresh),该方法在ServletWebServerApplicationContext 中实现。而该类就跟ServletWebServer有关,而DispatcherServlet的创建入口又和,WebServer启动过程中执行的逻辑有关。只有我们先了解了SpringBoot应用的启动过程,一步一步脚印走,才可以对DispatcherServlet的创建与注册拨云见雾,就好像我们找DispathcherServlet的请求入口时,我如果直接就告诉你直接找doDispath方法是不是会很懵逼啊!

从2.4节,我们可以知道ServletWebServerApplicationContext 类中的refresh方法中有一个 stopAndReleaseWebServer();方法

/**刷新上下文*/	@Override	public final void refresh() throws BeansException, IllegalStateException {
try {
//调用父类,也就是AbstractApplicationContext的refresh方法 //父类的refresh是个模板方法,其中执行了很多抽象方法,子类根据情况重写抽象方法,达到模板化refresh整个应用上下文 super.refresh(); } catch (RuntimeException ex) {
stopAndReleaseWebServer(); throw ex; } }

stopAndReleaseWebServer方法作用:如果有异常,则停止内嵌的ServletWebServer。该方法的代码逻辑如下

/**停止内嵌Servlet容器*/	private void stopAndReleaseWebServer() {
WebServer webServer = this.webServer; if (webServer != null) {
try {
webServer.stop(); this.webServer = null; } catch (Exception ex) {
throw new IllegalStateException(ex); } } }

这里有停止内嵌ServletWebServer的方法,内嵌ServletWebServer是从this.webServer获取。根据这些信息可以推断,内嵌Servlet容器是在ServletWebServerApplicationContext中维护,那么这个容器的来源是什么呢?这就是我们以后要探讨的问题:ServletWebServer的注册,创建与启动过程。了解了这个以后,我们就可以找到DispathcerServlet的创建入口,然后了解它的创建和注册。SpringBoot在帮我们完成上面的全部操作的时候,Web容器在接收到HTTP请求时,经过预处理后才会把该请求交给DispatcherServlet处理。

转载地址:http://orozi.baihongyu.com/

你可能感兴趣的文章
Linux用户及用户组添加和删除操作
查看>>
通用 Makefile 的编写方法以及多目录 makefile 写法
查看>>
C++的4种智能指针剖析使用
查看>>
RPC框架实现之容灾策略
查看>>
Docker私库
查看>>
hdu——1106排序(重定向)
查看>>
hdu——1556Color the ball(树状数组)
查看>>
hdu——1541Stars(树状数组)
查看>>
快速幂的精简代码
查看>>
求大数乘方的前n位数字(对数加快速幂)
查看>>
hdu——2602Bone Collector(第一类背包问题)
查看>>
hdu——1711Number Sequence(kmp专练)
查看>>
strstr函数和find函数的异同
查看>>
Java的反射
查看>>
HTTP请求之POST与GET区别
查看>>
SSM结合Redis
查看>>
优化数据库的八种方法
查看>>
Java Web服务收到请求时线程的情况以及session情况
查看>>
SSM配置文件信息加密实现
查看>>
@Produces注解
查看>>