SpringBoot 2.4.4 启动原理

博文目录


本节问题

  • 为什么SpringBoot的jar可以直接运行
  • SpringBoot是如何启动Spring容器的
  • SpringBoot是如何启动内置Tomcat的
  • 外部Tomcat是如何启动SpringBoot的
  • 什么是SPI机制

SpringBoot 是如何通过 jar 包方式启动的

运行 java -jar 命令

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 选项, 它的参数值是包含了应用的class和resource文件的jar包的名字, 必须在manifest文件头中通过Main-Class来指定启动类

SpringBoot 打包的插件 spring-boot-maven-plugin

SpringBoot 项目的 pom.xml 文件中默认使用 spring-boot-maven-plugin 插件进行打包, 执行 mvn package 后, 会生成两个文件

demo-1.0.0.jar
demo-1.0.0.jar.original

spring-boot-maven-plugin 插件项目存在于 spring-boot-tools 目录中。插件默认有5个goals:repackage、run、start、stop、build-info。在打包的时候默认使用的是repackage。repackage能够将mvn package生成的jar包,再次打包为可执行的软件包,并将mvn package生成的软件包重命名为*.original。repackage在代码层面调用了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());
}

执行以上命令之后,便生成了打包结果对应的两个文件。

SpringBoot 打包的 jar 的结构

demo-1.0.0.jar
├── META-INF
│   └── MANIFEST.MF
├── BOOT-INF
│   ├── classes
│   │   └── 应用程序类
│   └── lib
│       └── 第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动程序, JarLauncher.class, WarLauncher.class

jar包中包含了其他的jar, 叫做 fat jar

MANIFEST.MF

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: demo
Implementation-Version: 1.0.0
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.mrathena.demo.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.4.4
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.mrathena.demo.Application, 这个才是应用自己的main函数

org.springframework.boot.loader.JarLauncher

Archive

即归档文件,这个概念在linux下比较常见。通常就是一个tar/zip格式的压缩包。jar是zip格式。

SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层。关于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包环境下寻找资源。而在SpringBoot打包的fatJar中,使用该实现

JarFile

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

比如一个JarFileArchive对应的URL为:

jar:file:/develop/test/target/test-1.0.0.jar!/

它对应的JarFile为:

/develop/test/target/test-1.0.0.jar

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

jar:file:/develop/test/target/test-1.0.0.jar!/lib/spring-boot-starter-web-1.3.5.RELEASE.jar!/

jar:file:/develop/test/target/test-1.0.0.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 能处理,它是SpringBoot内部扩展出来的一种URL协议。

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

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

在创建JarLauncher时,父类ExecutableArchiveLauncher找到自己所在的jar,并创建archive。
JarLauncher继承于org.springframework.boot.loader.ExecutableArchiveLauncher。该类的无参构造方法最主要的功能就是构建了当前main方法所在的FatJar的JarFileArchive对象。下面来看launch方法。该方法主要是做了2个事情:

  • 以FatJar为file作为入参,构造JarFileArchive对象。获取其中所有的资源目标,取得其Url,将这些URL作为参数,构建了一个URLClassLoader。
  • 以第一步构建的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有三种方法:

  • 实现URLStreamHandlerFactory接口,通过方法URL.setURLStreamHandlerFactory设置。该属性是一个静态属性,且只能被设置一次。
  • 直接提供URLStreamHandler的子类,作为URL的构造方法的入参之一。但是在JVM中有固定的规范要求:子类的类名必须是Handler,同时最后一级的包名必须是协议的名称。比如自定义了Http的协议实现,则类名必然为xx.http.Handler;

JVM启动的时候,需要设置java.protocol.handler.pkgs系统属性,如果有多个实现类,那么中间用|隔开。因为JVM在尝试寻找Handler时,会从这个属性中获取包名前缀,最终使用包名前缀.协议名.Handler,使用Class.forName方法尝试初始化类,如果初始化成功,则会使用该类的实现作为协议实现。

为了实现这个目标,SpringBoot首先从支持jar in jar中内容读取做了定制,也就是支持多个!/分隔符的url路径。SpringBoot定制了以下两个方面:

  • 实现了一个java.net.URLStreamHandler的子类org.springframework.boot.loader.jar.Handler。该Handler支持识别多个!/分隔符,并且正确的打开URLConnection。打开的Connection是SpringBoot定制的org.springframework.boot.loader.jar.JarURLConnection实现。
  • 实现了一个java.net.JarURLConnection的子类org.springframework.boot.loader.jar.JarURLConnection。该链接支持多个!/分隔符,并且自己实现了在这种情况下获取InputStream的方法。而为了能够在org.springframework.boot.loader.jar.JarURLConnection正确获取输入流,SpringBoot自定义了一套读取ZipFile的工具类和方法。这部分和ZIP压缩算法规范紧密相连。

SpringBoot 的Jar应用启动流程总结

  1. SpringBoot应用通过 spring-boot-maven-plugin 打包成一个Fat jar,包含了应用依赖的jar包和Spring Boot loader相关的类
  2. 在 MANIFEST.MF 文件中指定 Main-Class 是 SpringBoot 自定义的一个类加载器 JarLauncher, java -jar 执行时启动其main方法
  3. JarLauncher 负责创建一个LaunchedURLClassLoader来加载/lib下面的jar,并以一个新线程启动 Start-Class 指定的应用的Main函数

SpringBoot 是如何通过 IDE 方式启动的

因为依赖的Jar都让IDE放到classpath里了,所以SpringBoot直接启动Application的main方法就完事了。

Spring 启动过程

Spring在启动的时候, 先实例化一个 ApplicationContext, 如 ClassPathXmlApplicationContext(xml), AnnotationConfigApplicationContext(配置类), 内部调用 AbstractApplicationContext 的 refresh 方法, 其中 invokeBeanFactoryPostProcessors 方法负责将所有配置的Bean转化为 BeanDefinition(解析 @ComponentScan, @Import, @ImportResource, @Bean 等), finishBeanFactoryInitialization 方法负责将Bean实例化出来

SpringBoot 启动过程

// 方式1
SpringApplication.run(Application.class, args);
// 方式2
SpringApplication app = new SpringApplication(Application.class);
app.setBannerMode(Banner.Mode.OFF);
app.run(args);
// 方式3
// 有时我们需要创建多层次的ApplicationContext (例如,父子关系的Spring的ApplicationContext和SpringMVC),通过parent() 和 child()来创建多层次的ApplicationContext. 官方还提供了通过配置application.properties文件在SpringBoot启动过程中添加一些定制逻辑的方案
new SpringApplicationBuilder().sources(Parent.class).child(Application.class).run(args);
// org.springframework.boot.SpringApplication#run(java.lang.Class<?>[], java.lang.String[])
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
	return new SpringApplication(primarySources).run(args);
}

new SpringApplication

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	this.resourceLoader = resourceLoader;
	Assert.notNull(primarySources, "PrimarySources must not be null");
	// 将启动类放入primarySources, 将作为一个入口配置类被解析, 相当于AnnotationConfigApplicationContext传入的配置类
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
	// 根据classpath是否存在某些类,推算当前web应用类型(webFlux, servlet), 将据此启动不同的ApplicationContext
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
	// 从 spring.factories 中获取所有key:org.springframework.boot.Bootstrapper, 目前好像没有自带的, 可能其他jar里面有?
	// 可以用于在使用BootstrapRegistry之前对其进行初始化的回调接口????
	this.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));
	// 从 spring.factories 中获取所有key:org.springframework.context.ApplicationContextInitializer
	// spring上下文初始化的回调函数, 在上下文(ConfigurableApplicationContext)刷新(refresh)之前调用
	setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
	// 从 spring.factories 中获取所有key: org.springframework.context.ApplicationListener
	// SpringBoot提供了各种各样的事件监听器,用来订阅SpringBoot在运行阶段中的各种事件, 如启动成功/环境准备/上下文准备等
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
	// 根据main方法推算出mainApplicationClass, 这个类有什么作用呢?
	// 这么实现是因为不强制要求main方法里面启动SpringApplication, 而且run方法里面传入的也不一定就是main方法所在的类
	this.mainApplicationClass = deduceMainApplicationClass();
}

深入理解SpringApplication

Spring容器配置

SpringApplication能够从各种不同的配置源读取bean的定义。

Spring Boot建议采用Java注解配置的方式提供一个全局唯一的配置类。但是,你可以同时使用多种不同的配置源。如果是Java注解的配置方式,会使用AnnotatedBeanDefinitionReader加载配置(通过全类名)。如果是XML的配置方式,则会使用XmlBeanDefinitionReader加载配置(通过XML文件地址)

如果除了primarySources配置类以外,还需要其它的ApplicationContext配置源,则可以调用SpringApplication#setSources(Set<String> sources)方法进行设置,该方法的参数既可以接受一个配置类的全类名,也可以是一个XML配置文件的地址

ApplicationRunner | CommandLineRunner

如果我们想在SpringBoot启动时传入一些参数进行一些特殊的业务逻辑处理,此时可以实现ApplicationRunner 或者 CommandLineRunner 接口,这两个接口都只有一个run()方法,该run()方法会在SpringApplication.run(​) 完成之前被调用

另外,如果有多个类实现了ApplicationRunner 或者 CommandLineRunner 接口,我们可以在该类上标注@Order注解或者让该类再实现org.springframework.core.Ordered接口来保证执行的顺序

推断应用类型

public enum WebApplicationType {
	// 该应用程序不应作为Web应用程序运行,也不应启动嵌入式Web服务器。
	NONE,
	// 该应用程序应作为基于Servlet的Web应用程序运行,并应启动嵌入式Servlet Web服务器。
	SERVLET,
	// 该应用程序应作为反应式Web应用程序运行,并应启动嵌入式反应式Web服务器。
	REACTIVE;
}
应用类型和创建ApplicationContext的关系
  • NONE: AnnotationConfigApplicationContext
  • SERVLET: AnnotationConfigServletWebServerApplicationContext
  • REACTIVE: AnnotationConfigReactiveWebServerApplicationContext

ApplicationContextInitializer & ApplicationListener

# spring-boot-autoconfigure.jar
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# spring-boot.jar
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
# spring-boot-autoconfigure.jar
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# spring-boot.jar
org.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.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

run SpringBoot启动的核心逻辑

public ConfigurableApplicationContext run(String... args) {
	// 记录启动开始时间
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	// ????
	DefaultBootstrapContext bootstrapContext = createBootstrapContext();
	// spring上下文的接口, 用来接收任何ApplicationContext实现
	ConfigurableApplicationContext context = null;
	// 开启了Headless模式????
	configureHeadlessProperty();
	// 在spring.factroies中读取SpringApplicationRunListener, 用来发布事件或者运行监听器, 貌似只有EventPublishingRunListener
	// 其内部持有一个ApplicationEventMulticaster(事件多播器), 用来管理ApplicationListener并向其发布事件
	// SimpleApplicationEventMulticaster是默认多播器, 负责将所有事件多播到所有已注册的侦听器, 而侦听器可以忽略它们不感兴趣的事件(instanceof), 监听器的侦听方法会使用线程池执行
	// 一般都是实现了ApplicationEventPublisher的ApplicationContext, 使用事件多播器来作为实际上发布事件的委托
	// 初始化EventPublishingRunListener实例的时候会把之前SpringApplication中的listeners都注册到其中的事件多播器上
	SpringApplicationRunListeners listeners = getRunListeners(args);
	// 调用EventPublishingRunListener的starting方法, 使用事件多播器发送ApplicationStartingEvent事件
	// SpringApplicationRunListener还有 starting, environmentPrepared, contextPrepared, contextLoaded, started, running, failed 这么些方法
	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);
		// 加载springioc容器, 由于是使用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;
}

prepareEnvironment

SpringBoot 2.4.4 Environment

createApplicationContext

// ApplicationContextFactory
// 函数式接口, 只有一个 create 方法, 可以直接写成如下的函数式形式的对象, 表示传入应用类型返回ApplicationContext
ApplicationContextFactory DEFAULT = (webApplicationType) -> {
	try {
		switch (webApplicationType) {
		case SERVLET:
			return new AnnotationConfigServletWebServerApplicationContext();
		case REACTIVE:
			return new AnnotationConfigReactiveWebServerApplicationContext();
		default:
			return new AnnotationConfigApplicationContext();
		}
	}
	catch (Exception ex) {
		throw new IllegalStateException("Unable create a default ApplicationContext instance, "
				+ "you may need a custom ApplicationContextFactory", ex);
	}
};

创建了一个 AnnotationConfigServletWebServerApplicationContext 实例

// 无参构造函数, 和AnnotationConfigApplicationContext的形式差不多
public AnnotationConfigServletWebServerApplicationContext() {
	this.reader = new AnnotatedBeanDefinitionReader(this);
	this.scanner = new ClassPathBeanDefinitionScanner(this);
}

Spring AnnotationConfigApplicationContext 初始化

prepareContext

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
		ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
		ApplicationArguments applicationArguments, Banner printedBanner) {
	context.setEnvironment(environment);
	postProcessApplicationContext(context);
	// 拿到之前读取到所有ApplicationContextInitializer的组件, 循环调用initialize方法
	applyInitializers(context);
	// 发布了ApplicationContextInitializedEvent
	listeners.contextPrepared(context);
	bootstrapContext.close(context);
	if (this.logStartupInfo) {
		logStartupInfo(context.getParent() == null);
		logStartupProfileInfo(context);
	}
	// Add boot specific singleton beans
	// 获取当前spring上下文beanFactory (负责创建bean)
	ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
	beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
	if (printedBanner != null) {
		beanFactory.registerSingleton("springBootBanner", printedBanner);
	}
	// 在Spring下 如果出现2个重名的bean, 则后读取到的会覆盖前面
	// 在SpringBoot 在这里设置了不允许覆盖, 当出现2个重名的bean 会抛出异常
	if (beanFactory instanceof DefaultListableBeanFactory) {
		((DefaultListableBeanFactory) beanFactory)
				.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
	}
	// 设置当前spring容器是不是要将所有的bean设置为懒加载
	if (this.lazyInitialization) {
		context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
	}
	// Load the sources
	// 获取到SpringApplication初始化时传入的主配置类, 即 `SpringApplication.run(Application.class, args);` 中的 Application.class
	Set<Object> sources = getAllSources();
	Assert.notEmpty(sources, "Sources must not be empty");
	// 读取主启动类, 将它注册为BeanDefinition, 就像我们reader.register(启动类); 一样读取配置类 (因为后续要根据配置类解析配置的所有bean)
	load(context, sources.toArray(new Object[0]));
	// 发送ApplicationPreparedEvent
	listeners.contextLoaded(context);
}
private void load(Class<?> source) {
	if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
		// Any GroovyLoaders added in beans{} DSL can contribute beans here
		GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
		((GroovyBeanDefinitionReader) this.groovyReader).beans(loader.getBeans());
	}
	if (isEligible(source)) {
		// AnnotatedBeanDefinitionReader 读取 配置类
		this.annotatedReader.register(source);
	}
}

refreshContext

最终会调用到 AbstractApplicationContext#refresh, 走SpringIoC容器流程, 覆盖其中的onRefresh方法, 创建Web容器

AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext
ServletWebServerApplicationContext extends GenericWebApplicationContext
GenericWebApplicationContext extends GenericApplicationContext
GenericApplicationContext extends AbstractApplicationContext
// ServletWebServerApplicationContext#onRefresh
protected void onRefresh() {
	super.onRefresh();
	try {
		// 在这里创建web服务
		createWebServer();
	}
	catch (Throwable ex) {
		throw new ApplicationContextException("Unable to start web server", ex);
	}
}
private void createWebServer() {
	WebServer webServer = this.webServer;
	// 判断当前是使用内置WebServer还是外部WebServer, 如果是null, 则使用内置WebServer
	ServletContext servletContext = getServletContext();
	if (webServer == null && servletContext == null) {
		// 使用内置Tomcat
		StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
		// 从IOC容器中获取ServletWebServerFactory类型的bean, 该bean通过ServletWebServerFactoryAutoConfiguration自动配置类注册
		// 通过导入ServletWebServerFactoryConfiguration.EmbeddedTomcat.class配置类引入了TomcatServletWebServerFactory这个bean, 最终拿到的factory是TomcatServletWebServerFactory
		ServletWebServerFactory factory = getWebServerFactory();
		createWebServer.tag("factory", factory.getClass().toString());
		// 调用ServletWebServerFactory的getWebServer生成TomcatWebServer
		this.webServer = factory.getWebServer(getSelfInitializer());
		createWebServer.end();
		getBeanFactory().registerSingleton("webServerGracefulShutdown",
				new WebServerGracefulShutdownLifecycle(this.webServer));
		getBeanFactory().registerSingleton("webServerStartStop",
				new WebServerStartStopLifecycle(this, this.webServer));
	}
	else if (servletContext != null) {
		// 使用外部Tomcat
		try {
			getSelfInitializer().onStartup(servletContext);
		}
		catch (ServletException ex) {
			throw new ApplicationContextException("Cannot initialize servlet context", ex);
		}
	}
	initPropertySources();
}
public WebServer getWebServer(ServletContextInitializer... initializers) {
	if (this.disableMBeanRegistry) {
		Registry.disableRegistry();
	}
	// 生成Tomcat
	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

createWebServer 内会判断 servletContext 是否为null, null说明没有外部WebServer, 所以会创建内置WebServer, 所以SpringBoot使用内置WebServer是在refresh方法执行onRefresh时创建的

调用ServletRegistrationBean.addRegistration向servletContent中添加servlet, 调用ServletRegistrationBean.configure添加mapping

使用外部Tomcat
条件
  • 项目必须是一个war项目
  • 需要排除掉spring-boot-starter-web中引入的内嵌tomcat(或将其作用范围改成provided)
  • 需要找一个类(一般是Application类)继承SpringBootServletInitializer, 覆盖configure方法, 设置source为Application.class
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
		return application.sources(Application.class);
	}
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}
规范

《Java™ Servlet Specification》Version 3.0 Rev a - Rajiv Mordani - December 2010, Java Servlet 规范 - 3.0版 - 8.2.4 节

8.2.4 Shared libraries / runtimes pluggability

In addition to supporting fragments and use of annotations one of the requirements is that not only we be able to plug-in things that are bundled in the WEB-INF/lib but also plugin shared copies of frameworks - including being able to plug-in to the web container things like JAX-WS, JAX-RS and JSF that build on top of the web container. The ServletContainerInitializer allows handling such a use case
as described below.

An instance of the ServletContainerInitializer is looked up via the jar services API by the container at container / application startup time. The framework providing an implementation of the ServletContainerInitializer MUST bundle in the META-INF/services directory of the jar file a file called javax.servlet.ServletContainerInitializer, as per the jar services API, that points to the implementation class of the ServletContainerInitializer.

In addition to the ServletContainerInitializer we also have an annotation - HandlesTypes. The HandlesTypes annotation on the implementation of the ServletContainerInitializer is used to express interest in classes that may have anotations (type, method or field level annotations) specified in the value of the HandlesTypes or if it extends / implements one those classes anywhere in the class’ super types. The container uses the HandlesTypes annotation to determine when to invoke the initializer’s onStartup method. When examining the classes of an application to see if they match any of the criteria specified by the HandlesTypes annotation of a ServletContainerInitializer, the container may run into class loading problems if one or more of the application’s optional JAR files are missing. Since the container is not in a position to decide whether these types of class loading failures will prevent the application from working correctly, it must ignore them, while at the same time providing a configuration option that would log them.

If an implementation of ServletContainerInitializer does not have the @HandlesTypes annotation, or if there are no matches to any of the HandlesType specified, then it will get invoked once for every application with null as the value of the Set. This will allow for the initializer to determine based on the resources available in the application whether it needs to initialize a servet / filter or not.

The onStartup method of the ServletContainerInitializer will be invoked when the application is coming up before any of the listener’s events are fired.

The ServletContainerInitializer’s onStartup method get’s a Set of Classes that either extend / implement the classes that the initializer expressed interest in or if it is annotated with any of the classes specified via the @HandlesTypes annotation.

Servlet3.0研究之ServletContainerInitializer接口

ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册Servlet,Filter以及Listener,以取代通过web.xml配置注册。这样就利于开发内聚的web应用框架

在容器或应用程序启动时, 容器会根据JavaSPI机制在所有jar包中找 META-INF/services/javax.servlet.ServletContainerInitializer 文件, 其内容就是接口 javax.servlet.ServletContainerInitializer 的实现类, 容器启动阶段将实例化这些实现类, 并执行其onStartup方法

在实现ServletContainerInitializer时还可以通过 @HandlesTypes 注解来定义本实现类希望处理的类型,容器会将当前应用中所有这一类型(继承或者实现)的类放在ServletContainerInitializer接口的集合参数c中传递进来。如果不定义处理类型,或者应用中不存在相应的实现类,则集合参数c为空

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
Spring 的实现

在spring-web包中提供了ServletContainerInitializer的实现类SpringServletContainerInitializer

// 关注了WebApplicationInitializer, 容器将会从类路径下找到所有的WebApplicationInitializer类(继承/实现), 传入到onStartup的webAppInitializerClasses参数中
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
	@Override
	// 
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = Collections.emptyList();

		if (webAppInitializerClasses != null) {
			initializers = new ArrayList<>(webAppInitializerClasses.size());
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				// 该感兴趣的类不是接口,不是抽象类,且是WebApplicationInitializer的本类或子类
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						// 实例化该类并加入到initializers
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		// WebApplicationInitializer的实现类都可以使用@Order等排序
		AnnotationAwareOrderComparator.sort(initializers);
		for (WebApplicationInitializer initializer : initializers) {
			// 遍历调用每个WebApplicationInitializer的onStartup方法
			initializer.onStartup(servletContext);
		}
	}
}
SpringServletContainerInitializer与WebApplicationInitializer的关系

SpringServletContainerInitializer负责将ServletContext实例化并委派给任何用户自定义的WebApplicationInitializer实现类, 以便每个实例可以注册和配置Servlet, Filter, Listener(例如DispatcherServlet, ContextLoaderListener)

尽管确实会在所有Servlet 3.0+运行时下加载并调用SpringServletContainerInitializer的onStartup方法, 但用户仍可以选择是否在类路径上实现WebApplicationInitializer, 如果没有WebApplicationInitializer实现, SpringServletContainerInitializer的onStartup方法相当于在空跑

外置WebServer如何启动SpringBoot应用

Application类继承SpringBootServletInitializer, 覆盖configure方法, 设置source为Application.class, 即原始配置类

SpringBootServletInitializer实现了WebApplicationInitializer, 容器启动的时候会把该类传入到SpringServletContainerInitializer的onStartup方法参数中, 然后实例化并调用SpringBootServletInitializer的onStartup方法

SpringBootServletInitializer的onStartup方法内调用createRootApplicationContext方法, 会创建一个SpringApplication, 最终会调用到SpringApplication的run方法, 从而启动整个SpringBoot应用

SpringBoot 事件监听器发布顺序

事件时机
ApplicationStartingEvent在首次启动run方法时立即调用(注册侦听器和初始化程序之后)
ApplicationEnvironmentPreparedEvent在Environment准备好之后,创建ApplicationContext之前
ApplicationContextInitializedEvent创建ApplicationContext之后(并调用ApplicationContextInitializers之后),读取主配置类之前
ApplicationPreparedEventApplicationContext预加载之后, refresh方法调用之前
ApplicationStartedEventrefresh方法调用之后, 调用 CommandLineRunner 和 ApplicationRunner 之前
紧随其后发送带有LivenessState.CORRECT的AvailabilityChangeEvent,以指示该应用程序被视为处于活动状态
ApplicationReadyEvent调用调用 CommandLineRunner 和 ApplicationRunner 之后, run方法完成之前
紧随其后发送带有ReadabilityState.ACCEPTING_TRAFFIC的AvailabilityChangeEvent,以指示应用程序已准备就绪,可以处理请求
ApplicationFailedEvent运行应用程序时发生故障时调用
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值