spring-boot-starter-web阻止Spring Boot退出的实现代码是什么?
使用spring-boot-starter-web时,Spring Boot应用启动完成后不会自动退出,JetBrains IDEA会展示对应标识提示启动完成:

如果使用以下两种方式手动保活:
public static void main ( String[] args ){ SpringApplication.run(Application.class, args); Thread.sleep(Long.MAX_VALUE); }
或者
public class MyRunner implements ApplicationRunner{ public void run() { Thread.sleep(Long.MAX_VALUE); } }
虽然也能让进程持续运行,但IDEA启动图标会一直处于加载状态,还会导致SpringBootTest测试用例永久阻塞,和spring-boot-starter-web的保活逻辑有本质差异。
Spring Boot本身没有编写主动阻塞主线程的保活代码,JVM进程退出的判定规则是:当所有存活线程均为守护线程时,进程会自动终止。spring-boot-starter-web正是利用这个规则,通过启动长期存活的非守护Web服务线程让进程保持运行,整个启动流程不会被阻塞,这是和手动Thread.sleep方案的核心区别。
1. Web服务启动入口
Web场景下的ApplicationContext实现为ServletWebServerApplicationContext,在容器刷新阶段会触发Web服务器创建:
@Override protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } }
createWebServer()方法会初始化对应内嵌Web容器(Tomcat/Jetty/Undertow等)的WebServer实例,同时注册WebServerStartStopLifecycle生命周期处理器:
private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(getSelfInitializer()); // 注册Web服务启停生命周期处理器,会在所有Bean初始化完成后触发 getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.webServer)); getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this.webServer)); } // 省略其余Servlet上下文初始化逻辑 }
WebServerStartStopLifecycle会在Spring容器启动完成的最后阶段调用WebServer.start(),执行完这步后SpringApplication.run()方法就会正常返回,不会阻塞主线程。
2. 非守护保活线程实现
以默认内嵌的Tomcat容器为例,TomcatWebServer启动时会单独启动一个非守护线程用于端口监听等待,核心代码如下:
@Override public void start() throws WebServerException { synchronized (this.monitor) { if (this.started) { return; } try { // 省略Tomcat连接器、引擎等组件初始化逻辑 this.tomcat.start(); // 启动独立的等待线程 startNonDaemonAwaitThread(); this.started = true; } catch (Exception ex) { // 省略异常处理逻辑 } } } private void startNonDaemonAwaitThread() { Thread awaitThread = new Thread("container-" + (containerCounter.get())) { @Override public void run() { // Tomcat内部循环监听端口,阻塞等待请求 TomcatWebServer.this.tomcat.getServer().await(); } }; awaitThread.setContextClassLoader(getClass().getClassLoader()); // 关键配置:设置线程为非守护线程,JVM会等待该线程存活不会退出 awaitThread.setDaemon(false); awaitThread.start(); }
Jetty、Undertow等其他内嵌容器的实现逻辑一致,都是启动独立的非守护线程执行端口监听,不阻塞Spring Boot主启动流程。
上述两种手动保活写法的本质问题是阻塞了Spring Boot主启动流程:
- 直接在
SpringApplication.run()后写Thread.sleep:run方法执行完启动流程后会被sleep阻塞,主线程无法结束 - 在
ApplicationRunner#run()中sleep:ApplicationRunner的执行属于Spring Boot启动流程的最后环节,方法不返回则run方法无法结束
这两种写法都会导致IDEA检测不到启动流程完成,一直显示加载状态;执行SpringBootTest时,启动流程被阻塞永远无法进入测试用例执行阶段。
内容的提问来源于stack exchange,提问作者Alceatraz

