为什么 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 方法结束时进程不会退出。