spring Boot启动流程&源码解析

目录

      • spring boot是如何通过Java -jar 启动的
          • java -jar 做了什么
          • Jar包的打包插件及核心方法
          • Jar包目录结构
          • META-INF内容
          • Archive的概念
          • JarLauncher
          • URLStreamHandler
        • spring boot的jar应用启动流程总结
          • 在IDE/开放目录启动Spring boot应用
      • Spring Boot是如何启动spring容器源码
        • 创建Spring Application
        • 启动
      • Spring Boot启动内嵌servlet容器

spring boot是如何通过Java -jar 启动的

得益于Spring Boot的封装,我们可以通过 java -jar 一行命令就可以启动一个web项目。再也不用搭建tomcat等相关容器。那么你是否关心过和探究过Spring Boot是如何封装达到这一效果的呢?本片文章带大家聊一聊,一探究竟。


我们都知道spring boot项目,当我们打包的时候会在pom文件中引入如下插件才能使用java -jar命令启动,这个插件帮我们生成了把我们所有依赖的jar包和我们的启动类打成jar包并指定Start-Class:启动类路径,难道指定这两个东西就能启动了吗?其实就算这样我们还是不能直接启动,因为Java没有提供任何标准的方式来加载嵌套的jar文件。在MANIFEAST文件中还执行了一个非常重要的配置就是Main-Class: org.springframework.boot.loader.JarLauncher(Spring Boot自定义出来的类加载器)来加载我们jar文件中的jar

 <plugin>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
java -jar 做了什么

先要弄清楚java -jar命令做了什么,在Oracle官网找到了这句描述。

If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.
//使用-jar参数时,后面的参数是的jar文件名(本例中是springbootstarterdemo-0.0.1-SNAPSHOT.jar);该jar文件中包含的是class和资源文件;在manifest文件中有Main-Class的定义;Main-Class的源码中指定了整个应用的启动类;(in its source code) 

小结一下:
java -jar 会找到jar中的manifest,再找到真正的启动类

这有个疑惑:在MANIFEST.MF文件中有这么一行内容

Start-Class: com.example.SpringbootMybatisApplication

前面Java官方文档中,只提到过main-class,并没有提到Start-class;
Start-Class的值 com.example.SpringbootMybatisApplication,正是我们Java代码中的唯一类,所在的地址和类名称。
所以问题就来了::理论上执行java -jar 命令时Main-Class: org.springframework.boot.loader.JarLauncher类会被执行,但是实际上Start-Class: com.example.SpringbootMybatisApplication被执行了,这两个之间有什么联系?

  • 因为Java没有提供任何标准的方式来加载嵌套jar文件(即,它们本身包含在jar中的jar文件)。
Jar包的打包插件及核心方法
<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

执行maven clean package之后,会生成两个文件
spring Boot启动流程&源码解析_第1张图片
spring-boot-maven-plugin项目存在于spring-boot-tools目录中。spring-boot-maven-plugin默认有5个:goals、repackage、run、start、build-info。在打包的时候默认使用的是repackage。
spring-boot-maven-plugin的repackage能够将mvn package生成的软件包,再次打包可执行的软件包,并将mvn package生成的软件包重命名为*.original。
spring-boot-maven-plugin在代码层调用了RepackageMojo的execute方法,而在该方法中用调用了repackage方法。repackage方法代码及操作解析如下:

private void repackage() throws MojoExecutionException {
   // maven生成的jar,最终的命名将加上.original后缀
   Artifact source = getSourceArtifact();
   // 最终为可执行jar,即fat jar
   File target = getTargetFile();
   // 获取重新打包器,将maven生成的jar重新打包成可执行jar
   Repackager repackager = getRepackager(source.getFile());
   // 查找并过滤项目运行时依赖的jar
   Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
         getFilters(getAdditionalFilters()));
   // 将artifacts转换成libraries
   Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
         getLog());
   try {
      // 获得Spring Boot启动脚本
      LaunchScript launchScript = getLaunchScript();
      // 执行重新打包,生成fat jar
      repackager.repackage(target, libraries, launchScript);
   }catch (IOException ex) {
      throw new MojoExecutionException(ex.getMessage(), ex);
   }
   // 将maven生成的jar更新成.original文件
   updateArtifact(source, target, repackager.getBackupFile());
}

执行了以上命令之后,便生成了打包结果对应的两个文件。下面针对文件内容和结构进行一探究竟。

Jar包目录结构

首先来看看jar的目录结构,都包含哪些目录和文件,解压jar包可以看到如下结构:

spring-boot-learn-0.0.1-SNAPSHOT
├── META-INF
│   └── MANIFEST.MF
├── BOOT-INF
│   ├── classes
│   │   └── 应用程序类
│   └── lib
│       └── 第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动程序
META-INF内容

在上述目录结构中,META-INF记录了相关jar包的基础信息,包括入口程序等。

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springboot-mybatis
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.example.SpringbootMybatisApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.12.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

可以看到有Main-Class是org.springframework.boot.loader.JarLauncher ,这个是jar启动的Main函数。
还有一个Start-Class是com.example.SpringbootMybatisApplication,这个是我们应用自己的Main函数。

Archive的概念

再继续了解底层和原理之前,我们先来了解一下Archive的概念:

  • archive即归档文件,这个概念在Linux下比较常见
  • 通常就是一个tar/zip格式的压缩包
  • jar是zip格式
    Spring Boot抽象了Archive的概念,一个Archive可以是jar(Jar FileArchive),也可以是一个文件目录(ExplodeArchive),可以抽象为统一访问资源的逻辑是:关于Spring Boot中Archive的源码如下:
public interface Archive extends Iterable<Archive.Entry> {
    // 获取该归档的url
    URL getUrl() throws MalformedURLException;
    // 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
    Manifest getManifest() throws IOException;
    // 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar
    List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}

SpringBoot定义了一个接口用于描述资源,也就是org.springframework.boot.loader.archive.Archive。该接口有两个实现,分别是org.springframework.boot.loader.archive.ExplodedArchive 和 org.springframework.boot.loader.archive.JarFileArchive。前者用于在文件夹目录下寻找资源,后者用于在jar包下寻找资源。而在Spring Boot打包的fatjar中,则是使用后者。

JarFile:对于jar包的封装,每个JarFileArchive都会对应一个Jar File。Jar File被构造的时候会解析内部结构,去获取jar包的各个文件夹或者文件,这些文件或文件夹被封装到Entry中,也存储在Jar File Archive中。如果Entry是个jar,会解析成Jar File Archive。
比如一个Jar FileArchive对应的URL为:

jar:file:/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/

它对应的Jar File为:

/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar

这个JarFile有很多Entry,比如:

META-INF/
META-INF/MANIFEST.MF
spring/
spring/study/
....
spring/study/executablejar/ExecutableJarApplication.class
lib/spring-boot-starter-1.3.5.RELEASE.jar
lib/spring-boot-1.3.5.RELEASE.jar
...

JarFileArchive内部的一些依赖jar对应的URL(SpringBoot使用org.springframework,loader.jar.Handler处理器来处理这些URL);

jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-starter-web-1.3.5.RELEASE.jar!/
jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class

我们看到如果有jar包中包含jar,或者jar包中包含jar包里面的class文件,会使用!/分隔开,这种方式只有org.springframework.boot.loader.jar.Handler能处理,它是Spring Boot内部扩展出来的一种URL协议。

JarLauncher

在spring Boot项目依赖中加入如下依赖就可以找到JarLuancher类

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-loader</artifactId>
</dependency>

从MANIFEST.MF可以看到Main函数是JarLauncher,下面来分析它的工作流程。JarLauncher类的继承结构是:

class JarLauncher extends ExecutableArchiveLauncher
class ExecutableArchiveLauncher extends Launcher

Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.
按照定义,JarLauncher可以加载内部/BOOT-INF/lib下的jar及/BOOT-INF/classes下的应用class,其实JarLauncher实现很简单:

public class JarLauncher extends ExecutableArchiveLauncher {
    public JarLauncher() {}
    public static void main(String[] args) throws Exception {
        new JarLauncher().launch(args);
    }
}

其主入口新建了JarLuancher并调用父类Luncher中的Luanch方法启动程序,父类ExecutableArchiveLauncher找到自己所在的jar,并创建archive。
JarLuancher继承于org.springframework.boot.loader.ExecutableArchiveLauncher。该类的无参构造方法主要是构建了当前main方法所在的Fat Jar的Jar FileArchive对象。下面来看下launch方法,该主方法主要是做两件事情:
(1)以FatJar为file作为入参,构造JarFileArchive对象。获取其中所有的资源目标,取得其Url,将这些URL作为参数,构建了一个URLClassLoader。
(2)以第一步构建的ClassLoader加载MANIFEST.MF文件中Start-Class指向的业务类,并且执行静态方法main。进而启动整个程序。

public abstract class ExecutableArchiveLauncher extends Launcher {
    private final Archive archive;
    public ExecutableArchiveLauncher() {
        try {
            // 找到自己所在的jar,并创建Archive
            this.archive = createArchive();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
}
 
 
public abstract class Launcher {
    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
        String path = (location == null ? null : location.getSchemeSpecificPart());
        if (path == null) {
            throw new IllegalStateException("Unable to determine code source archive");
        }
        File root = new File(path);
        if (!root.exists()) {
            throw new IllegalStateException(
                    "Unable to determine code source archive from " + root);
        }
        return (root.isDirectory() ? new ExplodedArchive(root)
                : new JarFileArchive(root));
    }
}

在Launcher的launch方法中,通过以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archive,通过这些archives的url生成LaunchedURLClassLoader,并将其设置为线程上下文类加载器,启动应用。
至此,才执行我们应用程序主入口类的main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载。

URLStreamHandler

java中描述资源常使用URL。而URL有一个方法用于打开链接java.net.URL#openConnection()。由于URL用于表达各种各样的资源,打开资源的具体动作由java.net.URLStreamHandler这个类的子类来完成。根据不同的协议,会有不同的handler实现。而JDK内置了相当多的handler实现用于应对不同的协议。比如jar、file、http等等。URL内部有一个静态HashTable属性,用于保存已经被发现的协议和handler实例的映射。
获得URLStreamHandler有三种方法:
(1)实现URLStreamHandlerFactory接口,通过方法URL.setURLStreamHandlerFactory设置。该属性是一个静态属性,且只能被设置一次。
(2)直接提供URLStreamHandler的子类,作为URL的构造方法的入参之一。但是在JVM中有固定的规范要求:
子类的类名必须是Handler,同时最后一级的包名必须是协议的名称。比如自定义了Http的协议实现,则类名必然为xx.http.Handler;
JVM启动的时候,需要设置java.protocol.handler.pkgs系统属性,如果有多个实现类,那么中间用|隔开。因为JVM在尝试寻找Handler时,会从这个属性中获取包名前缀,最终使用包名前缀.协议名.Handler,使用Class.forName方法尝试初始化类,如果初始化成功,则会使用该类的实现作为协议实现。
为了实现这个目标,SpringBoot首先从支持jar in jar中内容读取做了定制,也就是支持多个!/分隔符的url路径。SpringBoot定制了以下两个方面:
(1)实现了一个java.net.URLStreamHandler的子类org.springframework.boot.loader.jar.Handler。该Handler支持识别多个!/分隔符,并且正确的打开URLConnection。打开的Connection是SpringBoot定制的org.springframework.boot.loader.jar.JarURLConnection实现。
(2)实现了一个java.net.JarURLConnection的子类org.springframework.boot.loader.jar.JarURLConnection。该链接支持多个!/分隔符,并且自己实现了在这种情况下获取InputStream的方法。而为了能够在org.springframework.boot.loader.jar.JarURLConnection正确获取输入流,SpringBoot自定义了一套读取ZipFile的工具类和方法。这部分和ZIP压缩算法规范紧密相连,就不拓展了。

spring boot的jar应用启动流程总结

总结SpringBoot应用启动流程

  1. Spring Boot应用打包之后,生成Fat Jar,包含了应用依赖的jar包和Spring Boot Loader相关的类
  2. FatJar 启动Main函数是JarLanucher,它负责创建一个LanuchedClassLoader来加载lib下面的jar,并以一个新线程启动应用的Main函数,那么Class Loader是如何读取到Resource,它又需要哪些能力?查找资源和读取资源的能力。对应的API:
public URL findResource(String name)
public InputStream getResourceAsStream(String name)

SpringBoot构造LaunchedURLClassLoader时,传递了一个URL[]数组。数组里是lib目录下面的jar的URL。
对于一个URL,JDK或者ClassLoader如何知道怎么读取到里面的内容的?流程如下:
LaunchedURLClassLoader.loadClass
URL.getContent()
URL.openConnection()
Handler.openConnection(URL)
最终调用的是JarURLConnection的getInputStream()函数。

//org.springframework.boot.loader.jar.JarURLConnection
 @Override
public InputStream getInputStream() throws IOException {
   connect();
   if (this.jarEntryName.isEmpty()) {
     throw new IOException("no entry name specified");
   }
   return this.jarEntryData.getInputStream();
 }

从一个URL,到最终读取到URL里的内容,整个过程是比较复杂的,总结下:

  • Spring boot注册了一个Handler来处理”jar:”这种协议的URL。
  • Spring boot扩展了JarFile和JarURLConnection,内部处理jar in jar的情况。
  • 在处理多重jar in jar的URL时,Spring Boot会循环处理,并缓存已经加载到的JarFile。
  • 对于多重jar in jar,实际上是解压到了临时目录来处理,可以参考JarFileArchive里的代码。
  • 在获取URL的InputStream时,最终获取到的是JarFile里的JarEntryData。
    细节很多,上面只列出比较重要的步骤。最后,URLClassLoader是如何getResource的呢?URLClassLoader在构造时,有URL[]数组参数,它内部会用这个数组来构造一个URLClassPath:
    URLClassPath ucp = new URLClassPath(urls);
    在URLClassPath内部会为这些URLS都构造一个Loader,然后在getResource时,会从这些Loader里一个个去尝试获取。如果获取成功的话,就像下面那样包装为一个Resource。
Resource getResource(final String name, boolean check) {
    final URL url;
    try {
        url = new URL(base, ParseUtil.encodePath(name, false));
    } catch (MalformedURLException e) {
        throw new IllegalArgumentException("name");
    }
    final URLConnection uc;
    try {
        if (check) {
            URLClassPath.check(url);
        }
        uc = url.openConnection();
        InputStream in = uc.getInputStream();
        if (uc instanceof JarURLConnection) {
            /* Need to remember the jar file so it can be closed
             * in a hurry.
             */
            JarURLConnection juc = (JarURLConnection)uc;
            jarfile = JarLoader.checkJar(juc.getJarFile());
        }
    } catch (Exception e) {
        return null;
    }
    return new Resource() {
        public String getName() { return name; }
        public URL getURL() { return url; }
        public URL getCodeSourceURL() { return base; }
        public InputStream getInputStream() throws IOException {
            return uc.getInputStream();
        }
        public int getContentLength() throws IOException {
            return uc.getContentLength();
        }
    };
}
            JarURLConnection juc = (JarURLConnection)uc;

从代码里可以看到,实际上是调用了url.openConnection()。这样完整的链条就可以连接起来了。

在IDE/开放目录启动Spring boot应用

在上面只提到在一个fat jar里启动SpringBoot应用的过程,那么IDE里Spring boot是如何启动的呢?
在IDE里,直接运行的Main函数是应用的Main函数:

@SpringBootApplication
public class SpringBootDemoApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringBootDemoApplication.class, args);
    }
}

其实在IDE里启动SpringBoot应用是最简单的一种情况,因为依赖的Jar都让IDE放到classpath里了,所以Spring boot直接启动就完事了。
还有一种情况是在一个开放目录下启动SpringBoot启动。所谓的开放目录就是把fat jar解压,然后直接启动应用。
这时,Spring boot会判断当前是否在一个目录里,如果是的,则构造一个ExplodedArchive(前面在jar里时是JarFileArchive),后面的启动流程类似fat jar的。

总结
JarLauncher通过加载BOOT-INF/classes目录及BOOT-INF/lib目录下jar文件,实现了fat jar的启动。
SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载。
SpringBoot通过扩展URLClassLoader–LauncherURLClassLoader,实现了jar in jar中class文件的加载。
WarLauncher通过加载WEB-INF/classes目录及WEB-INF/lib和WEB-INF/lib-provided目录下的jar文件,实现了war文件的直接启动及web容器中的启动。

Spring Boot是如何启动spring容器源码

1、调用SpringApplication.run方法启动spring boot应用

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args); //调用run方法
    }
}

2、 使用自定义SpringApplication进行启动

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
		return new SpringApplication(primarySources).run(args);
	}
创建Spring Application
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		//将启动类放入primarySources,因为我们的启动类是一个配置类,配置了ComponentScan,在spring中启动也是传入一个配置类
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		//根据class Path下的类,推算当前web应用类型(webFlux,servlet)
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		//就是去spring.factroies中去获取所有key:org.springframework.context.ApplicationContextInitializer
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		//就是去spring.factories 中去获取所有key: org.springframework.context.ApplicationListener
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		// 根据main方法推算出mainApplicationClass
		this.mainApplicationClass = deduceMainApplicationClass();
	}

总结:

  1. 获取启动类:根据启动类加载ioc容器
  2. 获取web应用类型
  3. spring.factories读取了对外扩展的ApplicationContextInitializer,ApplicationListener主要为了:对外扩展,对内解耦(比如全局配置文件、热部署文件)
  4. 根据main推算出所在的类
启动

在创建Spring Application之后调用run方法

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

run方法:

public ConfigurableApplicationContext run(String... args) {
		//用来记录当前Spring Boot启动耗时
		StopWatch stopWatch = new StopWatch();
		//记录启动开始时间
		stopWatch.start();
		//它是任何spring上下文接口,所以可以接受任何ApplicationContext实现
		ConfigurableApplicationContext context = null;
		// 开启了Headless模式:
		configureHeadlessProperty();
		// 去spring.factroies中读取了SpringApplicationRunListener 的组件,  就是用来发布事件或者运行监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
		// 发布1.ApplicationStartingEvent事件,在运行开始时发送
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			// 根据命令行参数 实例化一个ApplicationArguments 
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 预初始化环境: 读取环境变量,读取配置文件信息(基于监听器)
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			// 忽略beaninfo的bean
			configureIgnoreBeanInfo(environment);
			// 打印Banner 横幅
			Banner printedBanner = printBanner(environment);
			// 根据webApplicationType创建Spring上下文  
			context = createApplicationContext();
			context.setApplicationStartup(this.applicationStartup);
			//预初始化spring上下文
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
			 // 加载spring ioc 容器   **相当重要   由于是使用AnnotationConfigServletWebServerApplicationContext 启动的spring			 容器所以springboot对它做了扩展:
      //  加载自动配置类:invokeBeanFactoryPostProcessors ,  创建servlet容器onRefresh
			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) {
			handleRunFailure(context, ex, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

总结:

  1. 初始化SpringApplication 从spring.factories 读取 listener ApplicationContextInitializer 。
    2.运行run方法
    3.读取 环境变量 配置信息…
  2. 创建springApplication上下文:ServletWebServerApplicationContext
  3. 预初始化上下文 : 读取启动类
    6.调用refresh 加载ioc容器
    加载所有的自动配置类
    创建servlet容器
    ps.在这个过程中springboot会调用很多监听器对外进行扩展

Spring Boot启动内嵌servlet容器

1、在refreshContext()方法中调用refresh方法下的onRefresh方法

 public void refresh() throws BeansException, IllegalStateException {
        synchronized(this.startupShutdownMonitor) {
            StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
            this.prepareRefresh();
            ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
            this.prepareBeanFactory(beanFactory);

            try {
                this.postProcessBeanFactory(beanFactory);
                StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
                this.invokeBeanFactoryPostProcessors(beanFactory);
                this.registerBeanPostProcessors(beanFactory);
                beanPostProcess.end();
                this.initMessageSource();
                this.initApplicationEventMulticaster();
                this.onRefresh();//启动servlet容器
                this.registerListeners();
                this.finishBeanFactoryInitialization(beanFactory);
                this.finishRefresh();
            } catch (BeansException var10) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
                }

                this.destroyBeans();
                this.cancelRefresh(var10);
                throw var10;
            } finally {
                this.resetCommonCaches();
                contextRefresh.end();
            }

        }
    }

2、进入onRefresh方法
在这里插入图片描述

@Override
	protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer(); //创建servlet容器
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}

3、进入创建servlet容器方法

private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
			ServletWebServerFactory factory = getWebServerFactory();
			createWebServer.tag("factory", factory.getClass().toString());
			this.webServer = factory.getWebServer(getSelfInitializer()); //创建对应的servlet容器
			createWebServer.end();
			getBeanFactory().registerSingleton("webServerGracefulShutdown",
					new WebServerGracefulShutdownLifecycle(this.webServer));
			getBeanFactory().registerSingleton("webServerStartStop",
					new WebServerStartStopLifecycle(this, this.webServer));
		}
		else if (servletContext != null) {
			try {
				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context", ex);
			}
		}
		initPropertySources();
	}

在这里插入图片描述
4、这里我们以Tomcat容器为例

@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		if (this.disableMBeanRegistry) {
			Registry.disableRegistry();
		}
		Tomcat tomcat = new Tomcat();
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		Connector connector = new Connector(this.protocol);
		connector.setThrowOnFailure(true);
		tomcat.getService().addConnector(connector);
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat); //启动Tomcat
	}

5、进入getTomcatWebServer()——>启动Tomcat方法

public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
		Assert.notNull(tomcat, "Tomcat Server must not be null");
		this.tomcat = tomcat;
		this.autoStart = autoStart;
		this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
		initialize();//进入
	}
private void initialize() throws WebServerException {
		logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
		synchronized (this.monitor) {
			try {
				addInstanceIdToEngineName();

				Context context = findContext();
				context.addLifecycleListener((event) -> {
					if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
						// Remove service connectors so that protocol binding doesn't
						// happen when the service is started.
						removeServiceConnectors();
					}
				});

				// Start the server to trigger initialization listeners
				this.tomcat.start();//启动Tomcat方法

				// We can re-throw failure exception directly in the main thread
				rethrowDeferredStartupExceptions();

				try {
					ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
				}
				catch (NamingException ex) {
					// Naming is not enabled. Continue
				}

				// Unlike Jetty, all Tomcat threads are daemon threads. We create a
				// blocking non-daemon to stop immediate shutdown
				startDaemonAwaitThread();//将Tomcatawait
			}
			catch (Exception ex) {
				stopSilently();
				destroySilently();
				throw new WebServerException("Unable to start embedded Tomcat", ex);
			}
		}
	}
private void startDaemonAwaitThread() {
		Thread awaitThread = new Thread("container-" + (containerCounter.get())) {

			@Override
			public void run() {
				TomcatWebServer.this.tomcat.getServer().await();  //将Tomcatawait
			}

		};
		awaitThread.setContextClassLoader(getClass().getClassLoader());
		awaitThread.setDaemon(false);
		awaitThread.start();
	}

最后附上一张内置与外置servlet容器启动流程图

你可能感兴趣的:(spring,boot,java,后端)