SpringBoot如何启动与初始化

本文以SpringBoot Web项目为例子分析(只引入web包)java

如题所示,本文主要划分两个部分进行介绍,SpringBoot的启动和SpringBoot的初始化。 相信你们第一次启动SpringBoot的时候都感到很是神奇,一个简单的java –jar xxx.jar命令就能把一个web应用启动了,甚至不用放到Tomcat容器里,这实在是使人叹服的优雅和简洁!react

究其本质,SpringBoot将应用打包成了一个fat jar包,而不是咱们常见的jar包。fat jar在启动时会作一系列隐藏复杂的准备工做,最终呈现为如此简单的启动命令。fat jar技术并非SpringBoot独创,但确实是SpringBoot将其发扬光大。下面咱们一块儿来了解一下这个启动过程。web

SpringBoot的启动

首先咱们来看一下SpringBoot fat jar的结构spring

blockchain-0.0.1-SNAPSHOT.jar
├── META-INF
│   └── MANIFEST.MF
├── BOOT-INF
│   ├── classes
│   │   └── BlockchinaApplication.class
│   │   └── 应用程序
│   └── lib
│       └── spring-core.jar
│       └── 第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── JarLauncher
└── WarLauncher
└── springboot启动程序
复制代码

每一个jar包都存在一个META-INF/ MANIFEST.MF文件,可粗略理解为jar包的配置文件。编程

一个典型的SpringBoot fat jar包含如下几个关键部分springboot

  • Spring-Boot-Version: 2.1.4.RELEASE
  • Main-Class: org.springframework.boot.loader.JarLauncher
  • Start-Class: org.ypq.its.blockchain.BlockchainApplication
  • Spring-Boot-Classes: BOOT-INF/classes/
  • Spring-Boot-Lib: BOOT-INF/lib/
  • Created-By: Apache Maven 3.3.9
  • Build-Jdk: 1.8.0_45

Main-Class说明了该fat jar的入口启动类JarLauncher,执行命令java –jar blockchain-0.0.1-SNAPSHOT.jar的时候JVM会找到JarLauncher并运行它的main方法,源码以下bash

public static void main(String[] args) throws Exception {
   new JarLauncher().launch(args);
}
复制代码

new JarLauncher会调用父类ExecutableArchiveLauncher的无参构造方法服务器

public ExecutableArchiveLauncher() {
   try {
      this.archive = createArchive();
   }
   catch (Exception ex) {
      throw new IllegalStateException(ex);
   }
}
复制代码

archive是SpringBoot对归档文件的一个抽象,对于jar包是JarFileArchive,对于文件目录是ExplodedArchive。
createArchive方法会找到当前类所在的路径,构造一个Archive。app

launch方法less

protected void launch(String[] args) throws Exception {
    // 注册Handler
   JarFile.registerUrlProtocolHandler();
    // 找出fat jar里包含的全部archive,将其全部URL找出来构建LaunchedURLClassLoader
   ClassLoader classLoader = createClassLoader(getClassPathArchives());
    // 将LaunchedURLClassLoader设置到线程上下文,调起咱们应用的main方法
   launch(args, getMainClass(), classLoader);
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    // 能够看到SpringBoot应用使用的不是APPClassLoader,而是自定义的LaunchedURLClassLoader
   return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
    // 设置线程的ContextLoader
   Thread.currentThread().setContextClassLoader(classLoader);
    // 调起应用main方法
   createMainMethodRunner(mainClass, args, classLoader).run();
}

protected String getMainClass() throws Exception {
   Manifest manifest = this.archive.getManifest();
   String mainClass = null;
   if (manifest != null) {
    // 找出MANIFEST.MF的Start-Class属性,做为入口启动类
        mainClass = manifest.getMainAttributes().getValue("Start-Class");
   }
   if (mainClass == null) {
    throw new IllegalStateException(
        "No 'Start-Class' manifest entry specified in " + this);
   }
   return mainClass;
}

public void run() throws Exception {
    // 使用LaunchedURLClassLoader加载应用启动类
   Class<?> mainClass = Thread.currentThread().getContextClassLoader()
         .loadClass(this.mainClassName);
    // 反射找出main方法并调用
   Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
   mainMethod.invoke(null, new Object[] { this.args });
}
复制代码

每一个jar都会对应一个url,如

  • jar:file:/blockchain-0.0.1-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/

jar中的资源,也会对应一个url,并以'!/'分割,如

  • jar:file:/ blockchain-0.0.1-SNAPSHOT/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

对于原始的JarFile URL,只支持一个'!/',SpringBoot扩展了此协议,使其支持多个'!/',以实现jar in jar的资源,如

  • jar:file:/ blockchain-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

在JarFile.registerUrlProtocolHandler()方法里,SpringBoot将org.springframework.boot.loader.jar. Handler注册,该Handler继承了URLStreamHandler,支持多个jar的嵌套(即jar in jar),是SpringBoot fat jar加载内部jar资源的基础。

public class Handler extends URLStreamHandler {
}
复制代码

接下来扫描全部嵌套的jar,构建自定义的LaunchedURLClassLoader,设置到线程上下文,而后找出应用的启动类,调用main方法。所以到咱们应用的main方法以前,SpringBoot已经帮咱们配置好LaunchedURLClassLoader,而且具备加载BOOT-INF/class(应用自己的类)和BOOT-INF/lib(第三方依赖类)下面的全部类的能力,以上过程用一个图简要归纳一下。

若是咱们用IDE(Intellij IDEA或者eclipse)来启动SpringBoot应用,因为依赖的jar都已经放到classpath中,故不存在以上过程。本地调试与服务器运行的场景仍是有少量差别。

接下来就到SpringBoot应用的初始化

SpringBoot应用的初始化十分简洁,只有一行,对应调用SpringApplication.run静态方法。跟踪查看该静态方法,主要完成两个操做,一是建立SpringApplication对象,二是调用该对象的run方法。这两个操做看似简单,实际上包含了大量复杂的初始化操做,下面咱们就一块儿来一探究竟。

public static void main(String[] args) {
    SpringApplication.run(BlockchainApplication.class, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
   return new SpringApplication(primarySources).run(args);
}
复制代码

咱们先看一下SpringApplication的构造方法:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
   this.resourceLoader = resourceLoader;
   Assert.notNull(primarySources, "PrimarySources must not be null");
   this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 解析applicationType
   this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 设置Initializers 
   setInitializers((Collection) getSpringFactoriesInstances(
         ApplicationContextInitializer.class));
    // 设置Listeners
   setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
   this.mainApplicationClass = deduceMainApplicationClass();
}
复制代码

主要包括三个比较重要的地方 deduceFromClasspath会根据classpath特定类是否存在来决定applicationType,总共有三种类型,分别是REACTIVE,SERVLET和NONE。 REACTIVE是响应式web,若是包含

org.springframework.web.reactive.DispatcherHandler
复制代码

就会认为是响应式类型 NONE是普通应用程序,若是不包含

javax.servlet.Servlet
org.springframework.web.context.ConfigurableWebApplicationContext
复制代码

就认为是普通应用程序
其他状况就是SERVLET,也是咱们最经常使用的类型

接下来是设置initializer和listener,参数中都调用了getSpringFactoriesInstances,这是SpringBoot一种新的拓展机制,它会扫描classpath下全部包中的META-INF/spring.factories,将特定的类实例化(使用无参构造方法)。 一个典型spring-boot-starter.jar的spring.factories包含如下内容,initializer有4个,listener有9个。

实际上,算上其余依赖包,initializer应该是有6个,listener有10个。因此SpringApplication有6个实例化后的initializer,10个实例化后的listener。 到此为止SpringApplication的构造方法结束。

接下来就是run方法了,如下源码在关键地方进行了一些简单的注释

public ConfigurableApplicationContext run(String... args) {
    // 开启定时器,统计启动时间
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
   configureHeadlessProperty();
    // 获取并初始化全部RunListener
   SpringApplicationRunListeners listeners = getRunListeners(args);
    // 发布启动事件
   listeners.starting();
   try {
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
    // 准备好环境environment,即配置文件等
      ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
      configureIgnoreBeanInfo(environment);
    // 打印SpringBoot Logo
      Banner printedBanner = printBanner(environment);
    // 建立咱们最经常使用的ApplicationContext
      context = createApplicationContext();
    // 获取异常报告器,在启动发生异常的时候用友好的方式提示用户
      exceptionReporters = getSpringFactoriesInstances(
            SpringBootExceptionReporter.class,
            new Class[] { ConfigurableApplicationContext.class }, context);
    // 准备Context,加载启动类做为source
      prepareContext(context, environment, listeners, applicationArguments,
            printedBanner);
    // Spring初始化的核心逻辑,构建整个容器
      refreshContext(context);
      afterRefresh(context, applicationArguments);
    // 中止计时,统计启动耗时
      stopWatch.stop();
      if (this.logStartupInfo) {
         new StartupInfoLogger(this.mainApplicationClass)
               .logStarted(getApplicationLog(), stopWatch);
      }
      listeners.started(context);
    // 调用runner接口供应用自定义初始化
      callRunners(context, applicationArguments);
   }
   catch (Throwable ex) {
    // 处理启动中抛出的异常,使用异常报告器输出
      handleRunFailure(context, ex, exceptionReporters, listeners);
      throw new IllegalStateException(ex);
   }

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

咱们经过注释大概了解了一下run方法,先不急着往下分析,咱们来看下SpringApplicationRunListener,从RunListener这个名字看出它是run方法的listener,监听事件覆盖了启动过程的生命周期,从它下手再好不过了。总共有7个状态以下所示:

将其整理成表格

SpringApplicationRunListener

顺序 方法名 说明
1 starting Run 方法调用时立刻执行,最先执行,所以能够作一些很早期的工做,这个方法没有参数,能作的事情也很是有限
2 environmentPrepared 当environment准备好后执行,此时ApplicationContext还没有建立
3 contextPrepared 当ApplicationContext准备好后执行,此时还没有加载source
4 contextLoaded 加载source后调用,此时还没有refresh
5 started RefreshContext后执行,说明应用已经基本启动完毕,还没有调用ApplicationRunner等初始化
6 running 调用ApplicationRunner后执行,已经进入应用的就绪状态
7 failed 启动过程当中出现异常时执行

而EventPublishingRunListener是惟一一个Runlistener,将上面不一样时间点包装成一个个事件传播出去,对应关系以下

SpringApplicationEvent

顺序 方法名 对应事件
1 starting ApplicationStartingEvent
2 environmentPrepared ApplicationEnvironmentPreparedEvent
3 contextPrepared ApplicationContextInitializedEvent
4 contextLoaded ApplicationPreparedEvent
5 started ApplicationStartedEvent
6 running ApplicationReadyEvent
7 failed ApplicationFailedEvent

上面提到的各个事件都是指SpringBoot里新定义的事件,与原来Spring的事件不一样(起码名字不一样)

EventPublishingRunListener在初始化的时候会读取SpringApplication里面的10个listener(上文已经提到过),每当有对应的事件就会通知这10个listener,其中ConfigFileApplicationListener和LoggingApplicationListener与咱们的开发密切相关,简单介绍以下,有机会再仔细研究。

ConfigFileApplicationListener

响应事件 实现功能
ApplicationEnvironmentPreparedEvent 查找配置文件,并对其进行解析
ApplicationPreparedEvent 对defaultProperties的配置文件进行排序,基本没用到

LoggingApplicationListener

响应事件 实现功能
ApplicationStartingEvent 按照logback、log4j、javaLogging的优先顺序肯定日志系统,并预初始化
ApplicationEnvironmentPreparedEvent 对日志系统进行初始化,此后就可使用日志系统了
ApplicationPreparedEvent 将日志系统注册到spring容器中
ContextClosedEvent 清理日志系统
ApplicationFailedEvent 清理日志系统

Starting阶段

初始化上文提到的SpringApplicationRunListener,而后发布ApplicationStartingEvent事件。

environmentPrepared阶段

Environment在Spring的两个关键部分是profiles和properties,引伸出来的两个关键属性是propertySources(属性源,即环境变量、启动参数和配置文件等)和propertyResolver(属性解析器)。

propertySources SpringBoot根据applicationType(REACTIVE,SERVLET和NONE)建立Environment,在本例中是SERVLET,会建立StandardServletEnvironment,此时有4个PropertySources,分别是

  • servletConfigInit
  • servletContextInit
  • systemProperties(user.dir)
  • systemEnviroment(环境变量)

propertyResolver 接下来就是配置propertyResolver,它有一个很重要的属性是ConversionService,默认包含了各类各样的转换器,共132个,根据代码直观感觉一下,光是scalar数量相关的就几十个了。。。

public static void addDefaultConverters(ConverterRegistry converterRegistry) {
   addScalarConverters(converterRegistry);
   addCollectionConverters(converterRegistry);

   converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
   converterRegistry.addConverter(new StringToTimeZoneConverter());
   converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
   converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());

   converterRegistry.addConverter(new ObjectToObjectConverter());
   converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
   converterRegistry.addConverter(new FallbackObjectToStringConverter());
   converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}
复制代码

最后就是获取profile,而且发布ApplicationEnvironmentPreparedEvent事件。上文提到的ConfigFileApplicationListener在收到该事件后,就会对配置文件进行解析工做。

contextPrepared阶段

此时配置文件已经解析完成,能够尽情享用了。SpringBoot将spring.main的属性绑定到SpringApplication,打印banner(默认寻找classpath下的banner.png/jpg/txt等),而后开始着手构建context。

Context的构建与environment相似,根据ApplicationType(本例是SERVLET)构建AnnotationConfigServletWebServerApplicationContext,这个类的继承关系很是复杂,我以为比较关键的几点是:

  1. 拥有beanFactory属性,在父类GenericApplicationContext里初始化为DefaultListableBeanFactory,这也是咱们后面会常常用到的beanFactory实现类
  2. 拥有reader属性,实现类是AnnotatedBeanDefinitionReader,主要用于编程式注册的bean。
  3. 拥有scanner属性,实现类是ClassPathBeanDefinitionScanner,用于寻找Classpath上的候选bean,默认包括被@Component, @Repository,@Service和 @Controller 注解的bean。

而后准备异常报告器exceptionReporters,它也以getSpringFactoriesInstances的方式获取内置的FailureAnalyzers,FailureAnalyzers以一样的方式从获取FailureAnalyzer,默认状况下总共有17个。

其中有一个咱们常常遇到的ConnectorStartFailureAnalyzer,启动过程当中若是端口被占用,抛出ConnectorStartFailedException,就会调用该FailureAnalyzer,提示端口被占用信息。

class ConnectorStartFailureAnalyzer extends AbstractFailureAnalyzer<ConnectorStartFailedException> {
   @Override
   protected FailureAnalysis analyze(Throwable rootFailure, ConnectorStartFailedException cause) {
      return new FailureAnalysis(
            "The Tomcat connector configured to listen on port " + cause.getPort()
                  + " failed to start. The port may already be in use or the"
                  + " connector may be misconfigured.",
            "Verify the connector's configuration, identify and stop any process "
                  + "that's listening on port " + cause.getPort()
                  + ", or configure this application to listen on another port.",
            cause);
   }
}
复制代码

值得一提的是,全部FailureAnalyzer都继承了AbstractFailureAnalyzer

public abstract class AbstractFailureAnalyzer<T extends Throwable> implements FailureAnalyzer {
   @Override
   public FailureAnalysis analyze(Throwable failure) {
      T cause = findCause(failure, getCauseType());
      if (cause != null) {
         return analyze(failure, cause);
      }
      return null;
   }
}
复制代码

SpringBoot在根据泛型寻找合适的FailureAnalyzer时,使用了Spring提供的ResolvableType类。该类普遍应用于Spring的源码中,是Spring设计的基础。

@Override
public FailureAnalysis analyze(Throwable failure) {
   T cause = findCause(failure, getCauseType());
   if (cause != null) {
      return analyze(failure, cause);
   }
   return null;
}
// 找出当前类的泛型
protected Class<? extends T> getCauseType() {
   return (Class<? extends T>) ResolvableType
         .forClass(AbstractFailureAnalyzer.class, getClass()).resolveGeneric();
}
复制代码

// 判断抛出的异常是否当前类泛型的一个实例

protected final <E extends Throwable> E findCause(Throwable failure, Class<E> type) {
   while (failure != null) {
      if (type.isInstance(failure)) {
        return (E) failure;
      }
      failure = failure.getCause();
   }
   return null;
}
复制代码

我的认为上述设计针对一个FailureAnalyzer对应处理一种Exception的场景十分适合,而ApplicationListener对多种Event进行监听的场景更适合使用supportsEventType模式。

扯远了,再次回到咱们的contextPrepared阶段,最后一步是调用上文提到的6个initializer,它们都继承了ApplicationContextInitializer

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
   void initialize(C applicationContext);
}
复制代码

与上文的FailureAnalyzer相似,SpringBoot根据不一样的ApplicationContext寻找适合的ApplicationContextInitializer进行调用,因此说这种设计思路在Spring应用十分普遍。

其中一个initializer是ConditionEvaluationReportLoggingListener,它会在启动成功或失败后打印SpringBoot自动配置(AutoConfiguration)的Condition匹配信息,对于AutoConfiguration的调试十分有用。

最后发布ApplicationContextInitializedEvent事件,至此contextPrepared阶段结束。

contextLoaded阶段

这个阶段比较简单,主要往spring容器注册一些重要的类(此时Spring称其为source),其中最最最重要的就是SpringBoot的启动类了,称为PrimarySource。

SpringBoot支持在配置文件中指定附加的Source,但大多数状况下咱们只有一个启动类做为PrimarySource,在此阶段注册到spring容器,做为后续refreshContext的依据。 接下来发布ApplicationPreparedEvent事件,本阶段结束。

Started阶段

终于到了重头戏,本阶段调用了著名的AbstractApplicationContext.refresh()方法,大多数Spring的功能特性都在此处实现,但里面的逻辑又十分复杂,还夹杂着各类细枝末节,我也在抽空从新理清其主干脉络,限于篇幅,会在下一期的文章中着重介绍AbstractApplicationContext.refresh(),此处先行略过,目前咱们只要大概知道它完成了扫描bean,解析依赖关系,实例化单例对象等工做便可。

发布ApplicationStartedEvent事件,本阶段结束。

Running阶段

此时Spring自己已经启动完了,SpringBoot设计了ApplicationRunner接口供应用进行一些自定义初始化,都会在这阶段逐一调用。

发布ApplicationReadyEvent事件,本阶段结束。

Failed阶段

若是在上述的阶段中抛出异常,就会进入Failed阶段,发布ApplicationFailedEvent事件通知其余listener,利用上文介绍的FailureAnalyzers报告失败缘由。

小结

将上面过程用一张来简要归纳下

至此run方法结束,那么SpringBoot应用main方法也会跟着结束了,main线程退出。对于普通应用,因为没有其余守护线程,JVM会立刻关闭。对于web应用,Tomcat会启动一条守护线程,JVM依然保持工做,待Tomcat收到shutdown指令关闭守护线程后,JVM才会关闭。

关于Spring refreshContext和Tomcat的内容,我将在下期进行介绍,下期再见!

相关文章
相关标签/搜索