找回密码
 立即注册
首页 业界区 业界 源码浅析:SpringBoot main方法结束为什么程序不停止 ...

源码浅析:SpringBoot main方法结束为什么程序不停止

顾星 2025-9-26 11:33:33
前言

对于Java开发来说,天天都在用SpringBoot,每次启动都执行了main方法,该方法应该是最容易让人忽视的地方之一,不过几行代码,为什么执行完后JVM不结束呢?
本文以内嵌tomcat为例进行说明,并分享一些debug和画图的技巧。
原因

先说结论,是因为main方法启动了一个线程,这个线程是非daemon的,并且run方法执行的任务是TomcatWebServer.this.tomcat.getServer().await();(死循环),即非daemon线程+任务不停止=程序不退出。
debug源码

技巧

在debug时,有的源码是抽象方法,我们可以用快捷键F7跳转到具体正在执行的实现类方法,另外Alt+F9可以强制到达光标的位置。
流程

下面将debug对应的源码,有兴趣的朋友可以跟着动手试试。
SpringBoot启动入口,调用静态run方法。
  1. /** 一般demo
  2. * @date 2021/9/12 9:09
  3. * @author www.cnblogs.com/theRhyme
  4. */
  5. @SpringBootApplication
  6. public class CommonDemoApplication {
  7.     public static void main(String[] args) {
  8.         SpringApplication.run(CommonDemoApplication.class, args);
  9.     }
  10. }
复制代码
调用重载的run方法
  1. public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
  2.                 return run(new Class<?>[] { primarySource }, args);
  3.         }
复制代码
创建SpringApplication对象调用run方法
  1. public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
  2.                 return new SpringApplication(primarySources).run(args);
  3.         }
复制代码
由于该run方法很长,这里只贴到与本文main方法结束为何程序不退出的代码,对整个启动流程有兴趣的可以去看这篇:SpringBoot启动原理(基于2.3.9.RELEASE版本) 。这里我们注意refreshContext。
  1. public ConfigurableApplicationContext run(String... args) {
  2.                 StopWatch stopWatch = new StopWatch();
  3.                 stopWatch.start();
  4.                 DefaultBootstrapContext bootstrapContext = createBootstrapContext();
  5.                 ConfigurableApplicationContext context = null;
  6.                 configureHeadlessProperty();
  7.                 SpringApplicationRunListeners listeners = getRunListeners(args);
  8.                 listeners.starting(bootstrapContext, this.mainApplicationClass);
  9.                 try {
  10.                         ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
  11.                         ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
  12.                         configureIgnoreBeanInfo(environment);
  13.                         Banner printedBanner = printBanner(environment);
  14.                         context = createApplicationContext();
  15.                         context.setApplicationStartup(this.applicationStartup);
  16.                         prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
  17.                         refreshContext(context);
  18.             ……
复制代码
refreshContext调用了一个抽象方法,我们在debug模式使用F7进入具体的实现类。
  1. protected void refresh(ConfigurableApplicationContext applicationContext) {
  2.                 applicationContext.refresh();
  3.         }
复制代码
这里就初始化一些资源(placeholder,beanFactory,BeanPostProcessor,MessageSource,ApplicationEventMulticaster),注意onRefresh方法。
  1. @Override
  2.         public void refresh() throws BeansException, IllegalStateException {
  3.                 synchronized (this.startupShutdownMonitor) {
  4.                         StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
  5.                         // Prepare this context for refreshing.
  6.                         prepareRefresh();
  7.                         // Tell the subclass to refresh the internal bean factory.
  8.                         ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  9.                         // Prepare the bean factory for use in this context.
  10.                         prepareBeanFactory(beanFactory);
  11.                         try {
  12.                                 // Allows post-processing of the bean factory in context subclasses.
  13.                                 postProcessBeanFactory(beanFactory);
  14.                                 StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
  15.                                 // Invoke factory processors registered as beans in the context.
  16.                                 invokeBeanFactoryPostProcessors(beanFactory);
  17.                                 // Register bean processors that intercept bean creation.
  18.                                 registerBeanPostProcessors(beanFactory);
  19.                                 beanPostProcess.end();
  20.                                 // Initialize message source for this context.
  21.                                 initMessageSource();
  22.                                 // Initialize event multicaster for this context.
  23.                                 initApplicationEventMulticaster();
  24.                                 // Initialize other special beans in specific context subclasses.
  25.                                 onRefresh();
  26.                 ……
复制代码
进入onRefresh,这里会创建WebServer:
  1. @Override
  2.         protected void onRefresh() {
  3.                 super.onRefresh();
  4.                 try {
  5.                         createWebServer();
  6.                 }
  7.                 catch (Throwable ex) {
  8.                         throw new ApplicationContextException("Unable to start web server", ex);
  9.                 }
  10.         }
复制代码
这里是具体创建webServer的步骤,注意getTomcatWebServer。
  1. @Override
  2.         public WebServer getWebServer(ServletContextInitializer... initializers) {
  3.                 if (this.disableMBeanRegistry) {
  4.                         Registry.disableRegistry();
  5.                 }
  6.                 Tomcat tomcat = new Tomcat();
  7.                 File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
  8.                 tomcat.setBaseDir(baseDir.getAbsolutePath());
  9.                 Connector connector = new Connector(this.protocol);
  10.                 connector.setThrowOnFailure(true);
  11.                 tomcat.getService().addConnector(connector);
  12.                 customizeConnector(connector);
  13.                 tomcat.setConnector(connector);
  14.                 tomcat.getHost().setAutoDeploy(false);
  15.                 configureEngine(tomcat.getEngine());
  16.                 for (Connector additionalConnector : this.additionalTomcatConnectors) {
  17.                         tomcat.getService().addConnector(additionalConnector);
  18.                 }
  19.                 prepareContext(tomcat.getHost(), initializers);
  20.                 return getTomcatWebServer(tomcat);
  21.         }
复制代码
创建TomcatWebServer对象。
  1. protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
  2.                 return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
  3.         }
复制代码
设置一些属性,并执行initialize方法。
  1. public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
  2.                 Assert.notNull(tomcat, "Tomcat Server must not be null");
  3.                 this.tomcat = tomcat;
  4.                 this.autoStart = autoStart;
  5.                 this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
  6.                 initialize();
  7.         }
复制代码
初始化并启动tomcat容器,然后就开起非daemon await线程。
  1. private void initialize() throws WebServerException {
  2.                 logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
  3.                 synchronized (this.monitor) {
  4.                         try {
  5.                                 addInstanceIdToEngineName();
  6.                                 Context context = findContext();
  7.                                 context.addLifecycleListener((event) -> {
  8.                                         if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
  9.                                                 // Remove service connectors so that protocol binding doesn't
  10.                                                 // happen when the service is started.
  11.                                                 removeServiceConnectors();
  12.                                         }
  13.                                 });
  14.                                 // Start the server to trigger initialization listeners
  15.                                 this.tomcat.start();
  16.                                 // We can re-throw failure exception directly in the main thread
  17.                                 rethrowDeferredStartupExceptions();
  18.                                 try {
  19.                                         ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
  20.                                 }
  21.                                 catch (NamingException ex) {
  22.                                         // Naming is not enabled. Continue
  23.                                 }
  24.                                 // Unlike Jetty, all Tomcat threads are daemon threads. We create a
  25.                                 // blocking non-daemon to stop immediate shutdown
  26.                                 startDaemonAwaitThread();
  27.                         }
  28.                         catch (Exception ex) {
  29.                                 stopSilently();
  30.                                 destroySilently();
  31.                                 throw new WebServerException("Unable to start embedded Tomcat", ex);
  32.                         }
  33.                 }
  34.         }
复制代码
创建非daemon线程设置线程名等参数并启动。
  1. private void startDaemonAwaitThread() {
  2.                 Thread awaitThread = new Thread("container-" + (containerCounter.get())) {
  3.                         @Override
  4.                         public void run() {
  5.                                 TomcatWebServer.this.tomcat.getServer().await();
  6.                         }
  7.                 };
  8.                 awaitThread.setContextClassLoader(getClass().getClassLoader());
  9.                 awaitThread.setDaemon(false);
  10.                 awaitThread.start();
  11.         }
复制代码
至此由于awaitThread.setDaemon(false);和TomcatWebServer.this.tomcat.getServer().await();,启动该线程awaitThread后,main方法后续虽然执行完毕,但是程序不会退出。
https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
await方法

这里单独看一下TomcatWebServer.this.tomcat.getServer().await();。
该方法的Java doc:
  1. /**
  2. * Wait until a proper shutdown command is received, then return.
  3. * This keeps the main thread alive - the thread pool listening for http
  4. * connections is daemon threads.
  5. */
复制代码
指的是通过等候关闭命令这个动作来保持main线程存活,而HTTP线程作为daemon线程会在main线程结束时终止。
任务一直运行的原因:源码如下,debug会进入getPortWithOffset()的值是-1的分支(注意这里不是server.port端口号),然后会不断循环Thread.sleep( 10000 )直到发出关机指令修改stopAwait的值为true。
  1. @Override
  2.     public void await() {
  3.         // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
  4.         if (getPortWithOffset() == -2) {
  5.             // undocumented yet - for embedding apps that are around, alive.
  6.             return;
  7.         }
  8.         if (getPortWithOffset() == -1) {
  9.             try {
  10.                 awaitThread = Thread.currentThread();
  11.                 while(!stopAwait) {
  12.                     try {
  13.                         Thread.sleep( 10000 );
  14.                     } catch( InterruptedException ex ) {
  15.                         // continue and check the flag
  16.                     }
  17.                 }
  18.             } finally {
  19.                 awaitThread = null;
  20.             }
  21.             return;
  22.         }
  23.         ……
复制代码
stopAwait的值只会在org.apache.catalina.core.StandardServer#stopAwait中被修改,源码如下:
  1. public void stopAwait() {
  2.         stopAwait=true;
  3.         Thread t = awaitThread;
  4.         if (t != null) {
  5.             ServerSocket s = awaitSocket;
  6.             if (s != null) {
  7.                 awaitSocket = null;
  8.                 try {
  9.                     s.close();
  10.                 } catch (IOException e) {
  11.                     // Ignored
  12.                 }
  13.             }
  14.             t.interrupt();
  15.             try {
  16.                 t.join(1000);
  17.             } catch (InterruptedException e) {
  18.                 // Ignored
  19.             }
  20.         }
  21.     }
复制代码
而该方法会在容器生命周期结束方法org.apache.catalina.core.StandardServer#stopInternal中被调用。
非daemon线程的意义

setDaemon介绍

上面将线程设置为非daemon线程:awaitThread.setDaemon(false)。
java.lang.Thread#setDaemon源码如下:
  1. /**
  2.      * Marks this thread as either a {@linkplain #isDaemon daemon} thread
  3.      * or a user thread. The Java Virtual Machine exits when the only
  4.      * threads running are all daemon threads.
  5.      *
  6.      * <p> This method must be invoked before the thread is started.
  7.      *
  8.      * @param  on
  9.      *         if {@code true}, marks this thread as a daemon thread
  10.      *
  11.      * @throws  IllegalThreadStateException
  12.      *          if this thread is {@linkplain #isAlive alive}
  13.      *
  14.      * @throws  SecurityException
  15.      *          if {@link #checkAccess} determines that the current
  16.      *          thread cannot modify this thread
  17.      */
  18. public final void setDaemon(boolean on) {
  19.     checkAccess();
  20.     if (isAlive()) {
  21.         throw new IllegalThreadStateException();
  22.     }
  23.     daemon = on;
  24. }
复制代码
根据上面的Java doc注释可知:标记该线程是否是daemon线程,而JVM退出仅当只剩下daemon线程
所以非daemon线程存活,JVM是不会退出的
例子

如下代码,我们在main方法中启动了一个非daemon线程,并且调用了阻塞方法java.io.InputStream#read()。
  1. // https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
  2. public static void main(String[] args) {
  3.         System.out.println(Thread.currentThread().getName() + ": start");
  4.         Thread awaitThread =
  5.                 new Thread("non-daemon") {
  6.                     @Override
  7.                     public void run() {
  8.                         try {
  9.                             System.out.println(Thread.currentThread().getName() + ": start");
  10.                             System.in.read();
  11.                             System.out.println(Thread.currentThread().getName() + ": end");
  12.                         } catch (IOException e) {
  13.                             e.printStackTrace();
  14.                         }
  15.                     }
  16.                 };
  17.         awaitThread.setDaemon(false);
  18.         awaitThread.start();
  19.         System.out.println(Thread.currentThread().getName() + ": end");
  20.     }
复制代码
启动程序后,再不进行键盘输入的情况下,程序不会停止,运行结果如下:
  1. main: start
  2. main: end
  3. non-daemon: start
复制代码
main线程结束,但是程序不退出。
-1的原因

上面留了个问题,为什么getPortWithOffset()的返回值是-1。
如下getPort()的值为-1,此时相当于直接调用了getPort()方法。
  1. https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
  2. @Override
  3.     public int getPortWithOffset() {
  4.         // Non-positive port values have special meanings and the offset should
  5.         // not apply.
  6.         int port = getPort();
  7.         if (port > 0) {
  8.             return port + getPortOffset();
  9.         } else {
  10.             return port;
  11.         }
  12.     }
复制代码
而getPort直接取的是port属性。
  1. @Override
  2.     public int getPort() {
  3.         return this.port;
  4.     }
复制代码
注意这里的port不是我们指定的server.port这个属性,而是关闭命令监听的端口。
  1.     /**
  2.      * The port number on which we wait for shutdown commands.
  3.      */
  4.     private int port = 8005;
复制代码
为什么是8005而不是-1呢?那是在哪被修改了呢?
port属性提供的修改方式是setPort(),而使用Alt+F7找到在getServer中被修改为-1。
1.png

在server.setPort( -1 );打一个断点,重新debug,可以知道具体修改的时机。
之前我们debug过方法createWebServer,是具体创建webServer的步骤,但是我们这里要进入getWebServer。
  1. private void createWebServer() {
  2.                 WebServer webServer = this.webServer;
  3.                 ServletContext servletContext = getServletContext();
  4.                 if (webServer == null && servletContext == null) {
  5.                         StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
  6.                         ServletWebServerFactory factory = getWebServerFactory();
  7.                         createWebServer.tag("factory", factory.getClass().toString());
  8.                         this.webServer = factory.getWebServer(getSelfInitializer());
  9.             ……
复制代码
配置tomca实例参数,但是要注意这里的tomcat.getService()方法。
  1. public WebServer getWebServer(ServletContextInitializer... initializers) {
  2.                 if (this.disableMBeanRegistry) {
  3.                         Registry.disableRegistry();
  4.                 }
  5.                 Tomcat tomcat = new Tomcat();
  6.                 File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
  7.                 tomcat.setBaseDir(baseDir.getAbsolutePath());
  8.                 Connector connector = new Connector(this.protocol);
  9.                 connector.setThrowOnFailure(true);
  10.                 tomcat.getService().addConnector(connector);
  11.                 customizeConnector(connector);
  12.                 tomcat.setConnector(connector);
  13.                 tomcat.getHost().setAutoDeploy(false);
  14.                 configureEngine(tomcat.getEngine());
  15.                 for (Connector additionalConnector : this.additionalTomcatConnectors) {
  16.                         tomcat.getService().addConnector(additionalConnector);
  17.                 }
  18.                 prepareContext(tomcat.getHost(), initializers);
  19.                 return getTomcatWebServer(tomcat);
  20.         }
复制代码
内部调用getServer()。
  1. public Service getService() {
  2.         return getServer().findServices()[0];
  3.     }
复制代码
至此,就是这里就将server.setPort( -1 );。
  1. public Server getServer() {
  2.         if (server != null) {
  3.             return server;
  4.         }
  5.         System.setProperty("catalina.useNaming", "false");
  6.         server = new StandardServer();
  7.         initBaseDir();
  8.         // Set configuration source
  9.         ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));
  10.         // https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
  11.         server.setPort( -1 );
  12.         Service service = new StandardService();
  13.         service.setName("Tomcat");
  14.         server.addService(service);
  15.         return server;
  16.     }
复制代码
调用链

技巧

如果我们想画一个方法本次被调用(线程内部)的流程图,那么我们可以debug进入该方法,Alt+F8执行如下代码,打印出方法调用栈对应的mermaid js 内容,然后使用文本绘图工具进行渲染。
  1. // https://www.cnblogs.com/theRhyme
  2. StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
  3. List<String> methodChain = Arrays.stream(stackTrace)
  4.         .filter(e -> !e.getClassName().startsWith("java.") && !e.getClassName().startsWith("jdk.") && !e.getMethodName().contains("<"))
  5.         .map(e -> e.getClassName() + "." + e.getMethodName())
  6.         .collect(Collectors.toList());
  7. StringBuilder mermaidCode = new StringBuilder("graph TD\n");
  8. for (int i = methodChain.size() - 1; i > 0; i--) {
  9.     mermaidCode.append(String.format("    %s --> %s\n",
  10.             methodChain.get(i),
  11.             methodChain.get(i-1)));
  12. }
  13. System.out.println(mermaidCode);
复制代码
这种方式比较适合线程内部展示具体方法的被调用关系,可以自定义根据包名等条件过滤掉不想要展示的类,但是对于跨线程的调用却不起作用,因为原理是线程自身的调用栈。
具体内容

如图,debug到org.springframework.boot.web.embedded.tomcat.TomcatWebServer#startDaemonAwaitThread内部,执行上面的代码。
2.png

输出内容:
  1. graph TD
  2.     org.springframework.boot.devtools.restart.RestartLauncher.run --> cnblogscomtheRhyme.infrastructure.demos.common.CommonDemoApplication.main
  3.     cnblogscomtheRhyme.infrastructure.demos.common.CommonDemoApplication.main --> org.springframework.boot.SpringApplication.run
  4.     org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.run
  5.     org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.run
  6.     org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.refreshContext
  7.     org.springframework.boot.SpringApplication.refreshContext --> org.springframework.boot.SpringApplication.refresh
  8.     org.springframework.boot.SpringApplication.refresh --> org.springframework.boot.SpringApplication.refresh
  9.     org.springframework.boot.SpringApplication.refresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh
  10.     org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh --> org.springframework.context.support.AbstractApplicationContext.refresh
  11.     org.springframework.context.support.AbstractApplicationContext.refresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh
  12.     org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer
  13.     org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer
  14.     org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer
  15.     org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize
  16.     org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize --> org.springframework.boot.web.embedded.tomcat.TomcatWebServer.startDaemonAwaitThread
  17.     org.springframework.boot.web.embedded.tomcat.TomcatWebServer.startDaemonAwaitThread --> idea.debugger.rt.GeneratedEvaluationClass.invoke
复制代码
把内容放入文本绘图中,即可得到如下流程图:
3.jpg

出处:https://www.cnblogs.com/theRhyme/
4.png


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册