SpringBoot的启动流程源码解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 1 前言2 追本溯源3 容器启动流程3.1 应用启动计时3.2 打印 Banner3.3 创建上下文实例createApplicationContext3.4 构建容器上下文prepareContext3.5 小结

1 前言


在拥有 Spring Boot 以前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat ),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。



在 IDE 中也需要对 Web 容器进行一些配置,才能够运行或者 Debug。而使用 Spring Boot 我们只需要像运行普通 JavaSE 程序一样,run 一下 main () 方法就可以启动一个 Web 应用了。


2 追本溯源


只需要下面几行代码我们就可以跑起一个 Web 服务器:


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


3 容器启动流程


接下来,我们沿着 run () 方法来顺藤摸瓜。进入 SpringApplication 类,来看看 run () 方法的具体实现:


public class SpringApplication {
  ......
  public ConfigurableApplicationContext run(String... args) {
    // 1 应用启动计时开始
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 2 声明上下文
    ConfigurableApplicationContext context = null;
    // 3 初始化异常报告集合
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 4 设置 java.awt.headless 属性
    configureHeadlessProperty();
    // 5 启动监听器
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
      // 6 初始化默认应用参数
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
      // 7 准备应用环境
      ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
      configureIgnoreBeanInfo(environment);
      // 8 打印 Banner(Spring Boot 的 LOGO)
      Banner printedBanner = printBanner(environment);
      // 9 通过反射创建上下文实例
      context = createApplicationContext();
      // 10 构建异常报告
      exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
          new Class[] { ConfigurableApplicationContext.class }, context);
      // 11 构建上下文
      prepareContext(context, environment, listeners, applicationArguments, printedBanner);
      // 12 刷新上下文
      refreshContext(context);
      // 13 刷新上下文后处理
      afterRefresh(context, applicationArguments);
      // 14 应用启动计时结束
      stopWatch.stop();
      if (this.logStartupInfo) {
        // 15 打印启动时间日志
        new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
      }
      // 16 发布上下文启动完成事件
      listeners.started(context);
      // 17 调用 runners
      callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
      // 18 应用启动发生异常后的处理
      handleRunFailure(context, ex, exceptionReporters, listeners);
      throw new IllegalStateException(ex);
    }
    try {
      // 19 发布上下文就绪事件
      listeners.running(context);
    }
    catch (Throwable ex) {
      handleRunFailure(context, ex, exceptionReporters, null);
      throw new IllegalStateException(ex);
    }
    return context;
  }
  ......
}


Spring Boot 启动时做的所有操作都这这个方法里面,当然在调用上面这个 run () 方法之前,还创建了一个 SpringApplication 的实例对象。因为上面这个 run () 方法并不是一个静态方法,所以需要一个对象实例才能被调用。

可以看到,方法的返回值类型为 ConfigurableApplicationContext,这是一个接口,我们真正得到的是 AnnotationConfigServletWebServerApplicationContext 的实例。通过类名我们可以知道,这是一个基于注解的 Servlet Web 应用上下文(上下文(context)是 Spring 中的核心概念)。


3.1 应用启动计时


在 Spring Boot 应用启动完成时,我们经常会看到类似下面内容的一条日志:


Started SpringbootApplication in 4.9 seconds (JVM running for 5.553)


应用启动后,会将本次启动所花费的时间打印出来,让我们对于启动的速度有一个大致的了解,也方便我们对其进行优化。记录启动时间的工作是 run () 方法做的第一件事,由 stopWatch.start () 开启时间统计,具体代码如下:


public void start(String taskName) throws IllegalStateException {
    if (this.currentTaskName != null) {
        throw new IllegalStateException("Can't start StopWatch: it's already running");
    }
    // 记录启动时间
    this.currentTaskName = taskName;
    this.startTimeNanos = System.nanoTime();
}

然后到了 run () 方法的基本任务完成的时候,由 stopWatch.stop ()(编号 14 的位置)对启动时间做了一个计算,源码也很简单:


public void stop() throws IllegalStateException {
    if (this.currentTaskName == null) {
       throw new IllegalStateException("Can't stop StopWatch: it's not running");
    }
    // 计算启动时间
    long lastTime = System.nanoTime() - this.startTimeNanos;
  this.totalTimeNanos += lastTime;
    ......
}


最后,在 run () 中的编号 15 的位置将启动时间打印出来:


if (this.logStartupInfo) {
    // 打印启动时间
   new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

3.2 打印 Banner


Spring Boot 每次启动是还会打印一个自己的 LOGO,就像下面这样:


这种做法很常见,像 Redis、Docker 等都会在启动的时候将自己的 LOGO 打印出来。Spring Boot 默认情况下会打印那个标志性的 “树叶” 和 “Spring” 的字样,下面带着当前的版本。

在 run () 中编号 8 的位置调用打印 Banner 的逻辑,最终由 SpringBootBanner 类的 printBanner () 完成。这个图案定义在一个常量数组中,代码如下:


class SpringBootBanner implements Banner {
    private static final String[] BANNER = {
            "", 
            "  .   ____          _            __ _ _",
            " /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\", 
            "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
            " \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )", 
            "  '  |____| .__|_| |_|_| |_\\__, | / / / /",
            " =========|_|==============|___/=/_/_/_/" 
    };
    ......
  public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {
    for (String line : BANNER) {
      printStream.println(line);
    }
    ......
  }
}


真正打印的逻辑就是 printBanner () 方法里面的那个 for 循环。



3.3 创建上下文实例createApplicationContext


下面我们来到 run () 方法中编号 9 的位置,这里调用了一个 createApplicationContext () 方法,点进去我们会看到它的代码如下:


public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
      + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            switch (this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
                    break;
                case REACTIVE:
                    contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                    break;
                default:
                    contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
            }
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}


这个方法就是根据 SpringBootApplication 的 webApplicationType 属性的值,利用反射来创建不同类型的应用上下文(context)。而属性 webApplicationType 的值是在前面执行构造方法的时候由 WebApplicationType.deduceFromClasspath()获得的。通过方法名很容易看出来,就是根据 classpath 中的类来推断当前的应用类型。



我们这里是一个普通的 Web 应用,所以最终返回的类型为 SERVLET。所以会通过反射加载 DEFAULT_SERVLET_WEB_CONTEXT_CLASS,最后返回一个 AnnotationConfigServletWebServerApplicationContext实例(就像我们上文所说的那样)。


3.4 构建容器上下文prepareContext


接着我们来到 run () 方法编号 11 的 prepareContext () 方法。通过方法名,我们也能猜到它是为 context 做上台前的准备工作的。


private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
      SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
    ......
    // 加载资源
    load(context, sources.toArray(new Object[0]));
    listeners.contextLoaded(context);
}

在这个方法中,会做一些准备工作,包括初始化容器上下文、设置环境、加载资源等。

加载资源

上面的代码中,又调用了一个很关键的方法 —— load ()。这个 load () 方法真正的作用是去调用 BeanDefinitionLoader 类的 load () 方法。源码如下:


class BeanDefinitionLoader {
    ......
  int load() {
    int count = 0;
    for (Object source : this.sources) {
      count += load(source);
    }
    return count;
  }
  private int load(Object source) {
    Assert.notNull(source, "Source must not be null");
    if (source instanceof Class<?>) {
      return load((Class<?>) source);
    }
    if (source instanceof Resource) {
      return load((Resource) source);
    }
    if (source instanceof Package) {
      return load((Package) source);
    }
    if (source instanceof CharSequence) {
      return load((CharSequence) source);
    }
    throw new IllegalArgumentException("Invalid source type " + source.getClass());
  }
    ......
}


可以看到,load () 方法在加载 Spring 中各种资源。其中我们最熟悉的就是 load ((Class<?>) source) 和 load ((Package) source) 了。一个用来加载类,一个用来加载扫描的包。


load ((Class<?>) source) 中会通过调用 isComponent () 方法来判断资源是否为 Spring 容器管理的组件。 isComponent () 方法通过资源是否包含 @Component 注解(@Controller、@Service、@Repository 等都包含在内)来区分是否为 Spring 容器管理的组件。


而 load ((Package) source) 方法则是用来加载 @ComponentScan 注解定义的包路径。


3.5 小结


我们知道,Spring 是一个容器,我们喜欢它的一个重要原因就是它帮我们把 Bean 进行了统一的管理。Bean 的创建与销毁都由 Spring 来完成,而我们只需要关注使用,这也是 Spring IoC 的核心工作内容。

到此,Spring 真正开始开展 Bean 管理的工作了,prepareContext () 方法把所有需要管理的 Bean 统计出来,在后面的 refreshContext () 方法中会进行更进一步的操作。 refreshContext () 方法和自动配置关系紧密。


相关文章
|
6月前
|
缓存 Java 程序员
springboot的启动流程总结
springboot的启动流程总结
|
6月前
|
设计模式 Java 容器
SpringBoot2 | SpringBoot启动流程源码分析(二)
SpringBoot2 | SpringBoot启动流程源码分析(二)
79 0
|
Java 应用服务中间件 容器
springboot启动流程
springboot的启动流程;
|
Java 开发者 微服务
SpringBoot启动流程大揭秘
通俗易懂读源码--SpringBoot启动流程详解
SpringBoot启动流程大揭秘
|
Java Spring 容器
springboot的启动流程
springboot的启动流程
|
3月前
|
XML Java 应用服务中间件
SpringBoot启动流程解析
SpringBoot启动流程解析
47 0
|
Java 容器 Spring
springboot启动流程原理
springboot启动流程原理
143 0
|
6月前
|
Java 中间件 容器
SpringBoot2 | SpringBoot启动流程源码分析(一)
SpringBoot2 | SpringBoot启动流程源码分析(一)
82 0
|
Java 应用服务中间件 Maven
SpringBoot启动流程步骤总结
SpringBoot启动流程步骤总结
406 0
|
Java 应用服务中间件 数据库连接
SpringBoot启动流程是什么?
Spring Boot 启动流程可以概括为以下几个步骤
737 0