跳至主要内容

Spring Boot 2 启动流程解析

Spring Boot 2 启动流程解析

本文基于Spring Boot 2.1.4.RELEASE版本,希望通过"自顶向下"的方法来理解Spring Boot的启动流程。先从整体上了解流程走向,再查看对应源码。

启动类

Spring Boot的启动类通常如下:

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

首先看一下@SpringBootApplication注解,这个注解由3个注解组成
@SpringBootApplication=@EnableAutoConfiguration+@ComponentScan+@Configuration

注意:
@SpringBootApplication实际上注册的是@SpringBootConfiguration,但@SpringBootConfiguration里面包含了@Configuration。

各个注解作用如下:

  • @EnableAutoConfiguration 开启自动配置
  • @ComponentScan SpringBean扫描
  • @Configuration 开启配置类

然后再看一下SpringApplication类,这个类主要通过如下步骤启动应用:

  • 根据应用的classpath创建对应的ApplicationContext(独立应用、WEB应用或REACTIVE应用)
  • 注册CommandLinePropertySource,把命令行参数转换为Spring Properties
  • 刷新ApplicationContext,加载所有的单例SpringBean
  • 调用实现了CommandLineRunner的SpringBean中的run方法

下面从源码上分别来看下这些步骤,这些步骤主要在SpringApplication类中的run方法中(public ConfigurableApplicationContext run(String... args))

主要源码如下
代码清单1,创建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));
 // 根据classpath中的类来确定具体是哪种类型的应用
 this.webApplicationType = WebApplicationType.deduceFromClasspath();
 setInitializers((Collection) getSpringFactoriesInstances(
   ApplicationContextInitializer.class));
 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
 this.mainApplicationClass = deduceMainApplicationClass();
}

代码清单2SpringApplication类中run方法的主要代码

public ConfigurableApplicationContext run(String... args) {
    ......
 try {
  ApplicationArguments applicationArguments = new DefaultApplicationArguments(
    args);
  //注册CommandLinePropertySource,把命令行参数转换为Spring Properties
  ConfigurableEnvironment environment = prepareEnvironment(listeners,
    applicationArguments);
  configureIgnoreBeanInfo(environment);
  Banner printedBanner = printBanner(environment);
  //创建ApplicationContext
  context = createApplicationContext();
  exceptionReporters = getSpringFactoriesInstances(
    SpringBootExceptionReporter.class,
    new Class[] { ConfigurableApplicationContext.class }, context);
  prepareContext(context, environment, listeners, applicationArguments,
    printedBanner);
  //刷新ApplicationContext
  refreshContext(context);
  afterRefresh(context, applicationArguments);
  stopWatch.stop();
  if (this.logStartupInfo) {
   new StartupInfoLogger(this.mainApplicationClass)
     .logStarted(getApplicationLog(), stopWatch);
  }
  listeners.started(context);
  //调用实现了CommandLineRunner的SpringBean中的run方法
  callRunners(context, applicationArguments);
 }
 catch (Throwable ex) {
  handleRunFailure(context, ex, exceptionReporters, listeners);
  throw new IllegalStateException(ex);
 }
    ......
}

根据应用的classpath创建对应的ApplicationContext(独立应用、WEB应用或REACTIVE应用)

SpringApplication判断启动的应用类型

确定应用类型的具体方法在SpringApplication的构造方法中,具体看代码清单1中的this.webApplicationType = WebApplicationType.deduceFromClasspath();这行,deduceFromClasspath方法具体如下:

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;
}

实现比较清晰,具体是通过判断classpath中的类来确定是哪种类型的应用。

SpringApplication创建ApplicationContext

创建ApplicationContext的代码在代码清单2中的context = createApplicationContext();这行,createApplicationContext的实现如下:

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);
}

这个实现也比较清晰,根据之前确定的应用类型,反射实例化对应的ApplicationContext。

注册CommandLinePropertySource,把命令行参数转换为Spring Properties

注册CommandLinePropertySource的代码在代码清单2中的ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);这行,这里从方法名能看出来,主要是准备Spring环境的参数,具体实现如下:

private ConfigurableEnvironment prepareEnvironment(
  SpringApplicationRunListeners listeners,
  ApplicationArguments applicationArguments) {
 // 根据不同的应用类型返回环境参数
 ConfigurableEnvironment environment = getOrCreateEnvironment();
 //分别配置PropertySource和Profile
 configureEnvironment(environment, applicationArguments.getSourceArgs());
 //触发监听事件
 listeners.environmentPrepared(environment);
 //把环境参数绑定到SpringApplication
 bindToSpringApplication(environment);
 if (!this.isCustomEnvironment) {
  environment = new EnvironmentConverter(getClassLoader())
    .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
 }
 ConfigurationPropertySources.attach(environment);
 return environment;
}

准备Spring环境这个实现相对来说还是挺复杂,需要对Spring结构有较多认识才能看懂,所以这里主要还是专注启动步骤,注册CommandLinePropertySource的实现在configureEnvironment(environment, applicationArguments.getSourceArgs());这行,具体实现如下:

protected void configureEnvironment(ConfigurableEnvironment environment,
  String[] args) {
 if (this.addConversionService) {
 //转换服务,转换配置环境中的日期、数字等
  ConversionService conversionService = ApplicationConversionService
    .getSharedInstance();
  environment.setConversionService(
    (ConfigurableConversionService) conversionService);
 }
 //配置PropertySources
 configurePropertySources(environment, args);
 //配置Profiles
 configureProfiles(environment, args);
}

再具体到configurePropertySources(environment, args);这行里的方法:

protected void configurePropertySources(ConfigurableEnvironment environment,
  String[] args) {
 MutablePropertySources sources = environment.getPropertySources();
 if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
  sources.addLast(
    new MapPropertySource("defaultProperties", this.defaultProperties));
 }
 if (this.addCommandLineProperties && args.length > 0) {
  String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
  if (sources.contains(name)) {
   PropertySource<?> source = sources.get(name);
   CompositePropertySource composite = new CompositePropertySource(name);
   composite.addPropertySource(new SimpleCommandLinePropertySource(
     "springApplicationCommandLineArgs", args));
   composite.addPropertySource(source);
   sources.replace(name, composite);
  }
  else {
   sources.addFirst(new SimpleCommandLinePropertySource(args));
  }
 }
}

这段代码的作用是注册CommandLinePropertySource,把命令行参数转换为Spring Properties。实现也比较好理解,主要是先把defaultProperties加入到Spring Properties中,然后再合并COMMAND_LINE_PROPERTY_SOURCE_NAME中的Spring Properties。

刷新ApplicationContext,加载所有的单例SpringBean

刷新ApplicationContext的代码在代码清单2中的refreshContext(context);这行,refreshContext的实现如下:

private void refreshContext(ConfigurableApplicationContext context) {
    //主要调用ApplicationContext本身的refresh方法
 refresh(context);
 if (this.registerShutdownHook) {
  try {
      //注册虚拟机关闭时的钩子,具体实现在AbstractApplicationContext中
   context.registerShutdownHook();
  }
  catch (AccessControlException ex) {
   // Not allowed in some environments.
  }
 }
}

这里的refresh(context);主要还是调用了ApplicationContext本身的refresh方法,实现如下:

protected void refresh(ApplicationContext applicationContext) {
 Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
 ((AbstractApplicationContext) applicationContext).refresh();
}

此时,Spring的初始化已经基本完成。

调用实现了CommandLineRunner接口的SpringBean中的run方法

调用实现了CommandLineRunner的SpringBean中的run方法的代码在代码清单2中的callRunners(context, applicationArguments);这行,具体实现如下:

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);
  }
 }
}

从代码中可以看到,Spring不仅会调用实现了CommandLineRunner的SpringBean的run方法,还会调用实现了ApplicationRunner的run方法,具体区别在ApplicationRunner的run方法会传入Spring包装的ApplicationArguments,而CommandLineRunner的run方法会传入原始的命令行传入参数,具体代码如下:
ApplicationRunner

private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
 try {
  (runner).run(args);
 }
 catch (Exception ex) {
  throw new IllegalStateException("Failed to execute ApplicationRunner", ex);
 }
}

CommandLineRunner

private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
 try {
  (runner).run(args.getSourceArgs());
 }
 catch (Exception ex) {
  throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
 }
}

到这里,基本上对Spring Boot 2的启动流程解析就结束了,主要根据文档上的启动步骤来理解,不过还有很多小细节没有涵盖到,比如exceptionReporters、打印banner等。

FAQ

为什么Spring Boot启动类上通常会加上@SpringBootApplication注解?

因为大多数开发者都会使用 自动配置、SpringBean扫描和配置类(@EnableAutoConfiguration+@ComponentScan+@Configuration),为了简化配置和遵循“约定优先于配置”设计,所以设计了@SpringBootApplication注解用于简化启动类的配置。

怎么知道SpringApplication主要是通过哪些步骤启动应用?

通过看源码上的类注释,一般文档做得比较好的框架都会有详细解释,有些会在类上,有些会在文档里。从这些解释入手是最方便的。如果框架的文档不完善,就只能靠自己提取重要步骤。

评论

此博客中的热门博文

国密SM2签名封装成PKCS7格式

在国内做金融行业,难免会有被强制使用国密算法的情况,而且一般还会指定必须使用硬件加密机之类的设备,所以我也稍微的研究了一下国密算法,使用软算法签名并封装 PKCS7 格式(文档中的一个交互)。 以下是基于 Bouncy Castle 的示例,密钥对的生成可以参考 Bouncy Castle 中 test 包下 SM2 相关代码 public static String sign ( ) throws Exception { //加载公钥 byte [ ] plainText = "hello, world" . getBytes ( ) ; FileInputStream input = new FileInputStream ( "F:\\certificate\\public.cer" ) ; CertificateFactory certificateFactory = new CertificateFactory ( ) ; X509Certificate certificate = ( X509Certificate ) certificateFactory . engineGenerateCertificate ( input ) ; input . close ( ) ; //加载私钥,private为换成实际的私钥 PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec ( "private" . getBytes ( ) ) ; //SM2算法实际上为ECC算法,并指定了一些参数值,所以这里的参数是EC KeyFactory factory = KeyFactory . getInstance ( "EC" , "BC" ) ; PrivateKey privateKey = factory . generatePrivate ( spec ) ; //以下为签名并封装成PKCS7格式 byte [ ] signedMessag

Spring Boot Actuator 2 示例

Welcome file 简介 Spring Boot Actuator为应用程序提供了各种开箱即用的运维特性,可以与应用方便的交互和监控。 使用环境:Java 11 和 Spring Boot 2.4.3.RELEASE 集成Spring Boot Actuator 在Spring Boot中集成Spring Boot Actuator与集成其他的框架类似,在 pom.xml 里引入相关的starter就可以: < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-actuator </ artifactId > </ dependency > < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-web </ artifactId > </ dependency > 由于大部分的使用场景还是web,所以这里也用Spring MVC做示例。 配置好 pom.xml 后,默认actuator仅暴露一些基本功能,实际使用中,根据需求暴露对应功能。为了简便测试,这里在 application.yml 中配置暴露全部功能: management : endpoints : web : exposure : include : "*" endpoint : health : enabled : true show-details : always probes : enabled : true shutdown : enabled : true metr

NextCloud数据目录迁移

最近服务器的环境坏了,所以迁移了NextCloud的数据目录。不过在迁移过程中有点小问题。 环境: Ubuntu 18.04 Docker 19.03.7 1.NextCloud页面不正常,Docker日志显示XX目录permission denied 参考了 这里 的做法,不过是把  /var/www/html/   整个目录的权限都修改为  chown -R www-data:www-data ,之后就不再报权限问题了。 2.数据库配置修改 因为NextCloud是在初始化时填的数据库连接信息,所以直接迁移数据目录的情况下,会导致应用连不到新的数据库环境。此时可以找到数据目录下的  config/config.php 文件,直接修改数据库连接配置。