技术面:SpringBoot(启动流程、如何优雅停机)

SpringBoot的启动流程

下面的代码是SpingBoot启动类里最基础的代码,SpringBoot的启动的入口就在这里,本文是在SpringBoot3的基础上进行的梳理。

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

在网上能搜到,各种博客和文章来讲解SpingBoot启动流程的,无一不是长篇大论的,把各个步骤,各个阶段执行的代码都贴出来。

确实,内容详尽固然有助于理解,但面试时只需记住主干流程即可,

谁在工作过程中,也不会无故的研究这个启动流程,心里有个大概的了解就可以了,如果实在隔得时间太长了,写代码时又真的会需要了解一下这个启动流程,那么直接在网上搜索,或者直接问AI都是可以的。

所以本次我总结这个启动流程的时候就是尽量的精简步骤,然后进行总结,让大家用自己的话,能给面试官解释清整个流程就行了。

另外还要说明一下,启动流程和实现自动装配不是一回事,启动流程里面是包括自动装配的。如果想了解SpringBoot是如何实现自动装配的,可以看之前写的文章【你来讲一下springboot的启动时的一个自动装配过程吧】

创建SpringBootApplication应用与刷新

首先最核心的一段代码SpringApplication.run(Application.class, args);这段代码会经历一系列的步骤,完成应用的初始化与启动。

这段代码的最后是会调用到SpringApplication.classConfigurableApplicationContext方法此方法里面执行的代码为(new SpringApplication(sources)).run(args),因此这个执行过程就分成的两部分,第一部分是new SpringApplication(sources)另一部分是.run(args)

java 复制代码
public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
    return (new SpringApplication(sources)).run(args);
}

new SpringApplication(sources)

这个构造方法的代码如下图所示

弄明白了initialize()方法其实就可以了。

  • 添加应用源 this.sources.addAll(Arrays.asList(sources));这段代码的是添加应用源的逻辑,将提供的源(通常是配置类)添加到应用的源列表中。
  • 设置 Web 环境 this.deduceWebEnvironment();判断应用是否应该运行在 Web 环境中,这会影响后续的 Web 相关配置。
  • 加载初始化器 this.getSpringFactoriesInstances(ApplicationContextInitializer.class)从 spring.factories 文件中加载所有列出的 ApplicationContextInitializer实现,并将它们设置到 SpringApplication 实例中,以便在应用上下文的初始化阶段执行它们。
  • 设置监听器 this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class))加载和设置 ApplicationListener实例,以便应用能够响应不同的事件。
  • 确定主应用类 this.deduceMainApplicationClass(),这个主应用程序类通常是包含 public static void main(String[]args)方法的类,是启动整个 Spring Boot 应用的入口点。

SpringApplication::run(args)

此方法就是用于启动SpringBoot应用,也是核心流程所在。

1、启动计时器

stopWatch.start();启动计时器,用于记录启动耗时。

2、获取和启动监听器
java 复制代码
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();

这一步从spring.factories中解析初始所有的SpringApplicationRunListener 实例,并通知

他们应用的启动过程已经开始。

3、装配环境参数
java 复制代码
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);

这一步主要是用来做参数绑定的,prepareEnvironment 方法会加载应用的外部配置。这包括application.propertiesapplication.yml 文件中的属性,环境变量,系统属性等。所以,我们自定义的那些参数就是在这一步被绑定的。

java 复制代码
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
    ConfigurableEnvironment environment = this.getOrCreateEnvironment();
    this.configureEnvironment(environment, applicationArguments.getSourceArgs());
    listeners.environmentPrepared(environment);
    if (!this.webEnvironment) {
        environment = (new EnvironmentConverter(this.getClassLoader())).convertToStandardEnvironmentIfNecessary(environment);
    }
    return environment;
}
4、创建应用上下文
java 复制代码
context = this.createApplicationContext();

到这一步就真的开始启动了,第一步就是先要创建一个Spring的上下文出来,只有有了这个上下文才能进行Bean的加载、配置等工作。

5、准备上下文

这一步很关键了,有很多核心操作在这里。

java 复制代码
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);

preareContent()方法代码如下,每一步都加上了注释说明。

java 复制代码
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
	  // 设置环境变量
    context.setEnvironment(environment);
    // 对应用上下文进行处理,一些自定义处理逻辑也在这里
    this.postProcessApplicationContext(context);
    // 应用所有的ApplicationContentInitializer
    this.applyInitializers(context);
    // 通知监听器上下文准备完毕工作已完成。
    listeners.contextPrepared(context);
    // 记录启动信息和使用的配置文件信息
    if (this.logStartupInfo) {
        this.logStartupInfo(context.getParent() == null);
        this.logStartupProfileInfo(context);
    }

// 向应用上下文中添加指定的单例Bean    
context.getBeanFactory().registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
    }
		// 加载配置应用源
    Set<Object> sources = this.getSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    this.load(context, sources.toArray(new Object[sources.size()]));
    // 通知监听器上下文加载已完成
    listeners.contextLoaded(context);
}
6、刷新上下文

this.refreshContext(context);这一步,是Spring启动的核心步骤了,这一步骤包括了实例化所有的 Bean、设置它们之间的依赖关系以及执行其他的初始化任务。

java 复制代码
public void refresh() throws BeansException, IllegalStateException {
        synchronized(this.startupShutdownMonitor) {
         // 为此刷新操作准备
         this.prepareRefresh();
         // 通知子类刷新内部bean工厂
         ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
         // 为刷新做好bean工厂的准备
         this.prepareBeanFactory(beanFactory);

         try {
         // 允许在上下文子类中对bean工厂进行处理
             this.postProcessBeanFactory(beanFactory);
             // 调用在上下文中注册为Bean的工厂处理器
             this.invokeBeanFactoryPostProcessors(beanFactory);
             // 注册拦截bean创建的bean处理器
             this.registerBeanPostProcessors(beanFactory);
             // 初始化此上下文的消息源
             this.initMessageSource();
             // 初始化上下文的的时间多播器
             this.initApplicationEventMulticaster();
             // 在特定上下文子类中初始化其他特殊Bean(tomcat等内嵌服务器也是在这一步启动)
             this.onRefresh();
             // 检测监听器Bean并,注册到容器
             this.registerListeners();
             // 实例化所有剩余的(非懒加载)单例
             this.finishBeanFactoryInitialization(beanFactory);
             // 最后一步,发布相应的事件
             this.finishRefresh();
         } catch (BeansException var9) {
             if (this.logger.isWarnEnabled()) {
                 this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
             }
						// 销毁已经创建的单利
             this.destroyBeans();
             // 重置"激活"标志
             this.cancelRefresh(var9);
             // 抛出异常
             throw var9;
         } finally {
         // 在spring的核心中重置常见的内存缓存,因为我们可能不再需要单例Bean的元数据
             this.resetCommonCaches();
         }

     }
 }
7、执行 CommandLineRunner 和 ApplicationRunner

this.afterRefresh(context, applicationArguments);执行启动参数命令或在应用完全启动后执行一些初始化逻辑,常用于数据初始化。

8、启动完成,返回 ApplicationContext

最终,run() 方法返回一个 ConfigurableApplicationContext,即 Spring 容器上下文,应用正式运行。

总结一下,可以用下面的话来回答面试:

SpringBoot启动流程从SpringApplication.run()开始,通过new SpringApplication()初始化应用源、判断Web环境、加载spring.factories中的初始化器和监听器,并确定主类;随后在run()中准备环境(加载配置)、创建ApplicationContext(如Web应用为ServletWebServerApplicationContext),通过refresh()完成Bean定义加载(含@EnableAutoConfiguration自动配置)、实例化单例Bean并启动内嵌服务器(如Tomcat),最后执行CommandLineRunner等初始化逻辑,最终返回ApplicationContext使应用运行。

SpringBoot如何优雅的停机

优雅停机(Graceful Shutdown)是指在应用停止时,系统能够有序地完成当前正在处理的任务,拒绝新请求,释放资源,从而避免请求中断和数据丢失,确保用户体验和数据一致性。

SpringBoot 2.3版本开始,框架原生支持优雅停机机制,这是最简单且官方推荐的实现方式。

使用方式,只需要在配置文件里面进行配置即可。

复制代码
# 启用优雅停机模式
server.shutdown=graceful

# 设置等待请求完成的超时时间(默认30秒)
spring.lifecycle.timeout-per-shutdown-phase=30s

工作原理

当应用接收到停止信号(如SIGTERM)时,内嵌Web服务器(Tomcat/Jetty/Undertow)会执行以下步骤:

  • 停止接收新请求:内嵌服务器停止接收新的HTTP请求。
  • 等待处理中请求:继续处理已接收的请求,直到超时。
  • 关闭应用上下文:按顺序销毁所有Bean(执行@PreDestroy方法),停止Web服务器。
  • 释放资源:释放数据库连接、线程池等资源。

不同 web服务器,优雅停机行为会有一定的差异。

服务器 优雅停机行为
Tomcat 停止接收新连接,等待处理中的请求完成,超时后强制关闭
Jetty 停止接收新请求,等待活跃请求完成
Undertow 关闭监听端口,等待活跃请求完成

使用SpringBoot Actuator实现优雅停机

使用SpringBoot Actuator实现优雅停机,也就是需要HTTP端点触发优雅停机,需要额外配置:

pom.xml增加依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置文件

puppet 复制代码
# 启用shutdown端点
management.endpoint.shutdown.enabled=true
# 暴露shutdown端点
management.endpoints.web.exposure.include=shutdown

触发优雅停机

bash 复制代码
curl -X POST http://localhost:8080/actuator/shutdown

Kubernetes环境下的最佳实践

在Kubernetes环境中,优雅停机需要与K8s生命周期配合。

首先,配置文件里面需要进行配置

perl 复制代码
# 启用优雅停机模式
server.shutdown=graceful
# 设置等待请求完成的超时时间(默认30秒)
spring.lifecycle.timeout-per-shutdown-phase=30s

Kubernetes Deployment配置

yaml 复制代码
terminationGracePeriodSeconds: 30
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
readinessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5

优雅停机具体的实现原理,主要有这几步

  • 信号接收阶段:通过Runtime.getRuntime().addShutdownHook()注册JVM关闭钩子,或通过Actuator端点接收停机请求
  • 应用上下文关闭阶段:发布ContextClosedEvent事件,按顺序销毁所有Bean,停止内嵌Web服务器
  • 连接器优雅停止阶段:不同服务器实现方式不同,但都确保等待处理中请求完成
相关推荐
Merrick3 小时前
亲手操作Java抽象语法树
java·后端
初级程序员Kyle4 小时前
开始改变第三天 Java并发(1)
java·后端
熊小猿4 小时前
如何在 Spring Boot 项目中使用 @Slf4j 注解结合 Logback 进行系统日志管理
java·开发语言·spring boot
豐儀麟阁贵4 小时前
5.4静态变量和静态方法
java·开发语言
ytgytg285 小时前
芋道源码:VUE3部署:避坑--验证码不现显示,管理后台无法访问后端接口等,完善中。。。
java·vue
乾坤瞬间5 小时前
【Java后端进行ai coding实践系列】如何使用ai coding实现计划任务增删改查
java·人工智能·python
CodeAmaz5 小时前
SpringBoot两级缓存实现
spring boot·spring·缓存
昔我往昔5 小时前
@ApiModel注解升级成@Schema注解
java
superlls5 小时前
(场景题)Java 导出 Excel 的两种方式
java·开发语言·后端·excel