You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

spring-boot-starter-web阻止Spring Boot退出的实现代码是什么?

问题背景

使用spring-boot-starter-web时,Spring Boot应用启动完成后不会自动退出,JetBrains IDEA会展示对应标识提示启动完成:
IDEA Spring Boot启动完成提示1
IDEA Spring Boot启动完成提示2

如果使用以下两种方式手动保活:

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主启动流程。


手动sleep方案的问题

上述两种手动保活写法的本质问题是阻塞了Spring Boot主启动流程:

  • 直接在SpringApplication.run()后写Thread.sleep:run方法执行完启动流程后会被sleep阻塞,主线程无法结束
  • 在ApplicationRunner#run()中sleep:ApplicationRunner的执行属于Spring Boot启动流程的最后环节,方法不返回则run方法无法结束
    这两种写法都会导致IDEA检测不到启动流程完成,一直显示加载状态;执行SpringBootTest时,启动流程被阻塞永远无法进入测试用例执行阶段。

内容的提问来源于stack exchange,提问作者Alceatraz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 07:51:18