spring boot原理分析(六):spring boot应用启动流程综述

前言

    在原理分析(一)已经整体概括了spring boot实现,spring boot主要是在已有Servlet容器+Servlet模板的基础上进行整合。具体来说包括三种,tomcat + spring mvc的模式是其中的一种,另外两种分别是Undertow+Servlet和Jetty+Servlet的模式。另外,大部分的外部模块的加入都是使用spring bean的方式进行动态配置,在原理分析(二)(三)(四)(五)中,分别对项目内外bean自动配置进行了分析,并详细介绍了常见的几个例子。至此,基于注解的bean的注入已经分析完了。
    本文将会从spring boot
的入口开始分析,展示spring boot在启动过程中,涉及了哪些组件或者模块的准备。在介绍spring mvc的DispatcherServlet的文章(tomcat + spring mvc原理(七))中,有过统览整个流程,再详细介绍每个局部组件原理的先例。这里将采用类似的思路。

入口

    前面spring boot的综述已经说过,spring boot应用内部包含了tomcat和spring mvc,既可以打包成war包,部署在外部的tomcat服务下,也可以打包成jar包,直接启动内置的tomcat,然后装载编写的spring mvc的应用。打包成war是spirng mvc开发过程中常用的,不是很新奇。至于如何使用内置的tomcat启动,则需要研究一番。

@SpringBootApplication
public class DemoApplication {

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

}

    既然要启动进程,也就逃脱不了使用java的入口函数main。关于上面@SpringBootApplication注解,上面几篇文章都是关于它的,有兴趣可以去查阅。main函数传入的参数,就是java启动时的入参。这里主要的问题是集中在SpringApplication的构造和它的run方法里。

构造函数的准备

    根据上面SpringApplication的run方法的调用方式,可以很容易推测出run是一个静态函数。

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

在run方法里构造了一个SpringApplication,只传入了在main函数中传入进来的DemoApplication.class。

public SpringApplication(Class<?>... primarySources) {
  this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
  //设置resourceLoader,这里为null
  this.resourceLoader = resourceLoader;
  Assert.notNull(primarySources, "PrimarySources must not be null");
  //设置基础类
  this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
  //设置Web应用类型
  this.webApplicationType = WebApplicationType.deduceFromClasspath();
  //设置上下文Context的初始化加载器
  setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
  //设置应用事件处理器
  setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
  //设置入口类
  this.mainApplicationClass = deduceMainApplicationClass();
}

    构造函数的调用顺序就是上面SpringApplication的由上自下,需要注意的是ResourceLoader传入时就等于null。在下面的SpringApplication的构造函数中,完成了几项初始化,依次是:

  1. 设置resourceLoader
  2. 设置基础类
  3. 设置Web应用类型
  4. 设置上下文Context的初始化加载器
  5. 设置应用事件处理器
  6. 设置入口类

    DemoApplication.class(这个是我自己命名的,名字可以改,都是这个类)作为基础类乍一听很陌生,但是其实在前面的文章里已经出现了“基础类包”这个词(《项目依赖包中容器的自动配置1》的环境上下文:基础包配置那一段)。@SpringBootApplication是一个@Configuration、@ComponentScan等组合的注解,被这个注解注释的类所在的包是项目中首要的基础包,其下所有的子包都会被扫描,如果存在bean会被自动注入到容器中。
    Web应用类型确实是没出现过,构造函数使用deduceFromClasspath方法设置这个值。deduceFromClasspath方法真如其名,Web应用类型是从class中推测出来的,如果class没有加载,还可以自行从Class默认路径中找出来加载。源码贴出来很多,但是内容很简单,就是根据静态String中定义的类有没有出现来判断是哪种服务类型。

private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
    "org.springframework.web.context.ConfigurableWebApplicationContext" };

private static final String WEBMVC_INDICATOR_CLASS = "org.springframework." + "web.servlet.DispatcherServlet";

private static final String WEBFLUX_INDICATOR_CLASS = "org." + "springframework.web.reactive.DispatcherHandler";

private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";

private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";

static WebApplicationType deduceFromClasspath() {
  if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
      && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
    return WebApplicationType.REACTIVE;
  }
  for (String className : SERVLET_INDICATOR_CLASSES) {
    if (!ClassUtils.isPresent(className, null)) {
      return WebApplicationType.NONE;
    }
  }
  return WebApplicationType.SERVLET;
}

这里判断了三种类型,SERVLET、REACTIVE和NONE。我们讲spring mvc是Servlet容器,所以服务即SERVLET WEB类型。REACTIVE也是一种WEB服务的类型,代表着非阻塞响应式编程,正是spirng mvc不擅长的事,具体可以参考spring-webflux。NONE类型说这个服务不是WEB类型,是其他服务类型。
    上下文Context的初始化加载器是用来初始化上下文Context,Context是spring boot加载过程中最重要的模块,包含了spring boot的配置信息、bean和resource等等,tomcat实例创建工厂和spring mvc的DispatcherServlet都在里面,可以说上下文Context即万物。后面会细讲。
    应用事件监听器ApplicationListener是spring boot自己定义的,而不是tomcat文章中(tomcat容器动态加载)提到的容器生命周期管理的容器状态事件监听器,ApplicationListener是针对spring boot整个应用的事件监听的处理器,有很多种情况下可能需要使用到,扩展性还是挺好的。后面还会构造用来管理这些事件监听处理器的SpringApplicationRunListener。
    设置入口类,大家可能会一头雾水,不是已经传入了DemoApplication.class吗?这里是因为DemoApplication中不一定要定义main,同理,也不一定在main函数中直接调用SpringApplication.run。deduceMainApplicationClass采用的方法是使用异常栈逆递归倒推,找到main函数的类,原理还是挺有意思的,可以借鉴一下。

private Class<?> deduceMainApplicationClass() {
  try {
    StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
    for (StackTraceElement stackTraceElement : stackTrace) {
      if ("main".equals(stackTraceElement.getMethodName())) {
        return Class.forName(stackTraceElement.getClassName());
      }
    }
  }
  catch (ClassNotFoundException ex) {
    // Swallow and continue
  }
  return null;
}

    构造函数里面还有一个点,是关于spring的,前面也出现过。SpringFactoriesLoader.loadFactoryNames(参考项目依赖包中容器的自动配置1)在getSpringFactoriesInstance中又被用来获取类的完整类名,然后使用反射获取构造函数构造了实例,这个部分的细节也值得一看。因为是技术实现问题,和本主题无关,这里不多讲。

SpringApplication run

public ConfigurableApplicationContext run(String... args) {
  //spring提供的运行时间打印工具,记录启动时间
  StopWatch stopWatch = new StopWatch();
  stopWatch.start();
  //主角
  ConfigurableApplicationContext context = null;
  //异常报告
  Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
  //设置java headless模式
  configureHeadlessProperty();
  //获取应用事件监听器的管理器
  SpringApplicationRunListeners listeners = getRunListeners(args);
  listeners.starting();
  try {
    //应用参数构造
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    //环境配置准备
    ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
    configureIgnoreBeanInfo(environment);
    //条幅打印
    Banner printedBanner = printBanner(environment);
    context = createApplicationContext();
    //获取异常上报器
    exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
        new Class[] { ConfigurableApplicationContext.class }, context);
    //准备上下文
    prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    //刷新上下文
    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, exceptionReporters, listeners);
    throw new IllegalStateException(ex);
  }

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

    上面是SpringApplication run方法的整个流程,由于是异步的,spring boot启动时,run方法所在的线程跑完结束后,spring boot应用就算完全启动了。run方法中,有些组件或者模块涉及内容比较多,这里我只会大致过下其作用,后面文章会细分析,有些就比较简单,可以直接解释清楚。

启动StopWatch

    StopWatch是spring提供的一个工具,用来监控和打印运行时间,使用过程包括构造、start和stop,在run方法里都有提现。最终打印是在StartupInfoLogger方法里,下面是打印出来的效果。
spring boot原理分析(六):spring boot应用启动流程综述-time.png

异常报告

    SpringBootExceptionReporter是可以用来获取启动过程中的异常,并上报打印。后面使用了getSpringFactoriesInstances方法进行获取所有异常上报器,在handleRunFailure方法中使用了这些实例。这个组件需要更加详细的分析。

java headless模式

    java的服务器开发时会使用这个模式。服务器可能缺少显示设备、键盘或鼠标这些外设,但又需要使用他们提供的功能,生成相应的数据,提供给客户端。设置这个模式,是告诉程序,不能指望硬件设备提供这些功能,需要依靠系统的计算能力模拟这些特性。设置方法就是:

System.setProperty("java.awt.headless", "true");
应用事件监听器的管理器

    SpringApplicationRunListener负责启动过程中,事件监听的处理器的管理,实际就是对上文提到的ApplicationListener的管理。SpringApplicationRunListener既负责管理event和发送event又负责管理ApplicationListener处理event。SpringApplicationRunListener实例可能有多个,在run方法运行过程中,能够看到SpringApplicationRunListeners的构造获取和starting、started、running。starting、started、running方法内部都是遍历所有SpringApplicationRunListener,单独调用每个SpringApplicationRunListener相应名字的方法实现的,而每个SpringApplicationRunListener又是调用自己管理的ApplicationListener处理对应的event。这个组件也会单独细讲。

应用参数

    ApplicationArguments是spring boot对应用参数进行了封装,输入参数是args。

环境配置

    Environment是指应用程序的运行环境,包括profile和properties配置,properties配置不仅包括项目内的properties文件,还有JVM system properties、操作系统环境变量等。由此构造ConfigurableEnvironment需要传入args也顺理成章。

条幅

    Banner是指spring boot启动过程中,会在日志中打印一个条幅,显示本服务基于spring boot。这个应该不用多说吧,炫酷。
spring boot原理分析(六):spring boot应用启动流程综述-banner.png

spring boot上下文

    spring boot的上下文Context是spring boot中最重要的一个模块,内部包含了spring boot服务运行过程中各种重要的信息,比如beanFactory、注册的bean、environment等等。
    prepareContext方法中对Context进行了初始化,设置了一些Context中的一些内容,比如environment、banner,最后还根据基础包记录primarySources,加载了基础包中的bean。refreshContext方法中调用了context的refresh方法,刷新上下文,创建并启动了tomcat实例,加载了spring mvc的Servlet,可以说服务的所有启动工作都是在这个方法中完成的。afterRefresh是一个模板方法,用意是继承SpringApplication的子类如果想在Context的refresh完成之后做点事,可以实现这个方法。
    所有关于Context的内容,后续会用单独篇幅来介绍。

启动后置处理
private void callRunners(ApplicationContext context, ApplicationArguments args) {
  List<Object> runners = new ArrayList<>();
  runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
  runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
  AnnotationAwareOrderComparator.sort(runners);
  for (Object runner : new LinkedHashSet<>(runners)) {
    if (runner instanceof ApplicationRunner) {
      callRunner((ApplicationRunner) runner, args);
    }
    if (runner instanceof CommandLineRunner) {
      callRunner((CommandLineRunner) runner, args);
    }
  }
}

    callRunners(context, applicationArguments)中加载了ApplicationRunner和CommandLineRunner两种类型的子类实例bean,并调用了它们的run方法。ApplicationRunner和CommandLineRunner是用于在spring boot启动完成后处理一些用户自己定义的逻辑,比如加载文件、配置数据库等等。使用方式就是继承这两个类,重载里面的run方法。至于这两个类的区别是run方法的输入参数不一样,ApplicationRunner中run方法的参数为ApplicationArguments,而CommandLineRunner接口中run方法的参数为String数组。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值