为什么 Spring Boot 应用在 main 方法执行完成后不会退出?
在 Java 语言中,当一个程序所有可能的语句都执行完成后程序就会自动退出。也可以调用 System#exit
和 Runtime#exit
方法提前退出当前程序。
1 | public class Application { |
但是对一个 Spring Boot 应用程序来说,当所有可能的语句执行完成后程序却不会自动退出,这是为什么呢?
1 | import org.springframework.boot.SpringApplication; |
原理分析
下图是与这个问题有关的简化后的调用流程图
sequenceDiagram
Application ->> SpringApplication: run()
SpringApplication ->> ServletWebServerApplicationContext: createWebServer()
ServletWebServerApplicationContext ->> TomcatServletWebServerFactory: getWebServer()
TomcatServletWebServerFactory ->> TomcatWebServer: new TomcatWebServer()
TomcatWebServer ->> TomcatWebServer: initialize()
在 TomcatWebServer
的 initialize
方法的最后会调用 startDaemonAwaitThread
方法
1 | // Unlike Jetty, all Tomcat threads are daemon threads. We create a |
从代码的注释可以看出 startDaemonAwaitThread
方法会创建一个阻塞的非守护线程来阻止程序立即退出。在这个线程中会调用 StandardServer
的 await
方法
1 | /** |
在这个方法中会每隔 10 秒钟检测一次 stopAwait
属性是否为 true
,如果为 true
则会立即退出循环,从而结束非守护线程。
await
方法有三个分支,具体执行那个分支依赖 getPortWithOffset()
方法的结果
1 | public int getPortWithOffset() { |
StandardServer
中 port
属性的默认值为 8005
,而 getPortWithOffset()
方法的结果为 -1
,这是又是为什么呢?
sequenceDiagram
TomcatWebServer ->> Tomcat: start()
Tomcat ->> StandardServer: new StandardServer()
Tomcat ->> StandardServer: setPort(-1)
在 TomcatWebServer
的 initialize
方法调用 startDaemonAwaitThread
方法之前会调用 Tomcat
的 start
方法,在这个方法里会创建 StandardServer
对象并将它的 port
属性设置为 -1
。这样在调用 await
方法是才能进入循环检测 stopAwait
属性是否为 true
的分支中。
因此 Spring Boot 应用程序是利用了“当所有的非 daemon 线程结束时 JVM 进程才会终止”这一特性来实现在 main
方法结束时进程不会退出。
使用 SHUTDOWN 命令
在 await
方法中如果 getPortWithOffset()
方法返回的值既不是 -2
也不是 -1
,则会在 8005
(默认值)端口启动一个服务器。在 Windows 下使用命令 netstat -aon | findstr LISTENING | findstr :80*
查看 Tomcat 开启了哪些端口
1 | TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 49588 |
当客户端连接 8005
端口并发送 SHUTDOWN
命令时也会使非守护线程结束运行,从而终止 Tomcat 进程。
1 | import java.io.IOException; |
从前面的分析可以知道 getPortWithOffset()
会返回 -1
,所以这种方式只对使用传统方式启动的 Tomcat 有效,对 Spring Boot 应用无效。
这种方式是利用了“ServerSocket
的 accept
方法会一直阻塞,直到有 Socket
连接进来”这一特性来实现在 main
方法结束时进程不会退出。