SpringBoot?main方法結(jié)束程序不停止的原因分析及解決方法
前言
對于Java開發(fā)來說,天天都在用SpringBoot,每次啟動都執(zhí)行了main方法,該方法應(yīng)該是最容易讓人忽視的地方之一,不過幾行代碼,為什么執(zhí)行完后JVM不結(jié)束呢?
本文以內(nèi)嵌tomcat為例進(jìn)行說明,并分享一些debug和畫圖的技巧。
原因
先說結(jié)論,是因為main方法啟動了一個線程,這個線程是非daemon的,并且run方法執(zhí)行的任務(wù)是TomcatWebServer.this.tomcat.getServer().await();(死循環(huán)),即非daemon線程+任務(wù)不停止=程序不退出。
debug源碼
技巧
在debug時,有的源碼是抽象方法,我們可以用快捷鍵F7跳轉(zhuǎn)到具體正在執(zhí)行的實現(xiàn)類方法,另外Alt+F9可以強(qiáng)制到達(dá)光標(biāo)的位置。
流程
下面將debug對應(yīng)的源碼,有興趣的朋友可以跟著動手試試。
SpringBoot啟動入口,調(diào)用靜態(tài)run方法。
/** 一般demo
* @date 2021/9/12 9:09
* @author www.cnblogs.com/theRhyme
*/
@SpringBootApplication
public class CommonDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CommonDemoApplication.class, args);
}
}調(diào)用重載的run方法
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}創(chuàng)建SpringApplication對象調(diào)用run方法
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}由于該run方法很長,這里只貼到與本文main方法結(jié)束為何程序不退出的代碼,對整個啟動流程有興趣的可以去看這篇:Springboot啟動原理和自動配置原理解析 這里我們注意refreshContext。
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
……refreshContext調(diào)用了一個抽象方法,我們在debug模式使用F7進(jìn)入具體的實現(xiàn)類。
protected void refresh(ConfigurableApplicationContext applicationContext) {
applicationContext.refresh();
}這里就初始化一些資源(placeholder,beanFactory,BeanPostProcessor,MessageSource,ApplicationEventMulticaster),注意onRefresh方法。
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
……進(jìn)入onRefresh,這里會創(chuàng)建WebServer:
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}這里是具體創(chuàng)建webServer的步驟,注意getTomcatWebServer。
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}創(chuàng)建TomcatWebServer對象。
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
}設(shè)置一些屬性,并執(zhí)行initialize方法。
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
initialize();
}初始化并啟動tomcat容器,然后就開起非daemon await線程。
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
addInstanceIdToEngineName();
Context context = findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
// Remove service connectors so that protocol binding doesn't
// happen when the service is started.
removeServiceConnectors();
}
});
// Start the server to trigger initialization listeners
this.tomcat.start();
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
}
catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
startDaemonAwaitThread();
}
catch (Exception ex) {
stopSilently();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}創(chuàng)建非daemon線程設(shè)置線程名等參數(shù)并啟動。
private void startDaemonAwaitThread() {
Thread awaitThread = new Thread("container-" + (containerCounter.get())) {
@Override
public void run() {
TomcatWebServer.this.tomcat.getServer().await();
}
};
awaitThread.setContextClassLoader(getClass().getClassLoader());
awaitThread.setDaemon(false);
awaitThread.start();
}至此由于awaitThread.setDaemon(false);和TomcatWebServer.this.tomcat.getServer().await();,啟動該線程awaitThread后,main方法后續(xù)雖然執(zhí)行完畢,但是程序不會退出。
await方法
這里單獨(dú)看一下TomcatWebServer.this.tomcat.getServer().await();。
該方法的Java doc:
/** * Wait until a proper shutdown command is received, then return. * This keeps the main thread alive - the thread pool listening for http * connections is daemon threads. */
指的是通過等候關(guān)閉命令這個動作來保持main線程存活,而HTTP線程作為daemon線程會在main線程結(jié)束時終止。
任務(wù)一直運(yùn)行的原因:源碼如下,debug會進(jìn)入getPortWithOffset()的值是-1的分支(注意這里不是server.port端口號),然后會不斷循環(huán)Thread.sleep( 10000 )直到發(fā)出關(guān)機(jī)指令修改stopAwait的值為true。
@Override
public void await() {
// Negative values - don't wait on port - tomcat is embedded or we just don't like ports
if (getPortWithOffset() == -2) {
// undocumented yet - for embedding apps that are around, alive.
return;
}
if (getPortWithOffset() == -1) {
try {
awaitThread = Thread.currentThread();
while(!stopAwait) {
try {
Thread.sleep( 10000 );
} catch( InterruptedException ex ) {
// continue and check the flag
}
}
} finally {
awaitThread = null;
}
return;
}
……stopAwait的值只會在org.apache.catalina.core.StandardServer#stopAwait中被修改,源碼如下:
public void stopAwait() {
stopAwait=true;
Thread t = awaitThread;
if (t != null) {
ServerSocket s = awaitSocket;
if (s != null) {
awaitSocket = null;
try {
s.close();
} catch (IOException e) {
// Ignored
}
}
t.interrupt();
try {
t.join(1000);
} catch (InterruptedException e) {
// Ignored
}
}
}而該方法會在容器生命周期結(jié)束方法org.apache.catalina.core.StandardServer#stopInternal中被調(diào)用。
非daemon線程的意義
setDaemon介紹
上面將線程設(shè)置為非daemon線程:awaitThread.setDaemon(false)。
java.lang.Thread#setDaemon源碼如下:
/**
* Marks this thread as either a {@linkplain #isDaemon daemon} thread
* or a user thread. The Java Virtual Machine exits when the only
* threads running are all daemon threads.
*
* <p> This method must be invoked before the thread is started.
*
* @param on
* if {@code true}, marks this thread as a daemon thread
*
* @throws IllegalThreadStateException
* if this thread is {@linkplain #isAlive alive}
*
* @throws SecurityException
* if {@link #checkAccess} determines that the current
* thread cannot modify this thread
*/
public final void setDaemon(boolean on) {
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}根據(jù)上面的Java doc注釋可知:標(biāo)記該線程是否是daemon線程,而JVM退出僅當(dāng)只剩下daemon線程。
所以非daemon線程存活,JVM是不會退出的。
例子
如下代碼,我們在main方法中啟動了一個非daemon線程,并且調(diào)用了阻塞方法java.io.InputStream#read()。
// https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": start");
Thread awaitThread =
new Thread("non-daemon") {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ": start");
System.in.read();
System.out.println(Thread.currentThread().getName() + ": end");
} catch (IOException e) {
e.printStackTrace();
}
}
};
awaitThread.setDaemon(false);
awaitThread.start();
System.out.println(Thread.currentThread().getName() + ": end");
}啟動程序后,再不進(jìn)行鍵盤輸入的情況下,程序不會停止,運(yùn)行結(jié)果如下:
main: start
main: end
non-daemon: start
main線程結(jié)束,但是程序不退出。
-1的原因
上面留了個問題,為什么getPortWithOffset()的返回值是-1。
如下getPort()的值為-1,此時相當(dāng)于直接調(diào)用了getPort()方法。
https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
@Override
public int getPortWithOffset() {
// Non-positive port values have special meanings and the offset should
// not apply.
int port = getPort();
if (port > 0) {
return port + getPortOffset();
} else {
return port;
}
}而getPort直接取的是port屬性。
@Override
public int getPort() {
return this.port;
}注意這里的port不是我們指定的server.port這個屬性,而是關(guān)閉命令監(jiān)聽的端口。
/**
* The port number on which we wait for shutdown commands.
*/
private int port = 8005;為什么是8005而不是-1呢?那是在哪被修改了呢?
port屬性提供的修改方式是setPort(),而使用Alt+F7找到在getServer中被修改為-1。

在server.setPort( -1 );打一個斷點(diǎn),重新debug,可以知道具體修改的時機(jī)。
之前我們debug過方法createWebServer,是具體創(chuàng)建webServer的步驟,但是我們這里要進(jìn)入getWebServer。
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
this.webServer = factory.getWebServer(getSelfInitializer());
……配置tomca實例參數(shù),但是要注意這里的tomcat.getService()方法。
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}內(nèi)部調(diào)用getServer()。
public Service getService() {
return getServer().findServices()[0];
}至此,就是這里就將server.setPort( -1 );。
public Server getServer() {
if (server != null) {
return server;
}
System.setProperty("catalina.useNaming", "false");
server = new StandardServer();
initBaseDir();
// Set configuration source
ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));
// https://www.cnblogs.com/theRhyme/p/-/springboot-not-stop-after-main
server.setPort( -1 );
Service service = new StandardService();
service.setName("Tomcat");
server.addService(service);
return server;
}調(diào)用鏈
技巧
如果我們想畫一個方法本次被調(diào)用(線程內(nèi)部)的流程圖,那么我們可以debug進(jìn)入該方法,Alt+F8執(zhí)行如下代碼,打印出方法調(diào)用棧對應(yīng)的mermaid js 內(nèi)容,然后使用文本繪圖工具進(jìn)行渲染。
// https://www.cnblogs.com/theRhyme
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
List<String> methodChain = Arrays.stream(stackTrace)
.filter(e -> !e.getClassName().startsWith("java.") && !e.getClassName().startsWith("jdk.") && !e.getMethodName().contains("<"))
.map(e -> e.getClassName() + "." + e.getMethodName())
.collect(Collectors.toList());
StringBuilder mermaidCode = new StringBuilder("graph TD\n");
for (int i = methodChain.size() - 1; i > 0; i--) {
mermaidCode.append(String.format(" %s --> %s\n",
methodChain.get(i),
methodChain.get(i-1)));
}
System.out.println(mermaidCode);這種方式比較適合線程內(nèi)部展示具體方法的被調(diào)用關(guān)系,可以自定義根據(jù)包名等條件過濾掉不想要展示的類,但是對于跨線程的調(diào)用卻不起作用,因為原理是線程自身的調(diào)用棧。
具體內(nèi)容
如圖,debug到org.springframework.boot.web.embedded.tomcat.TomcatWebServer#startDaemonAwaitThread內(nèi)部,執(zhí)行上面的代碼。

輸出內(nèi)容:
graph TD
org.springframework.boot.devtools.restart.RestartLauncher.run --> cnblogscomtheRhyme.infrastructure.demos.common.CommonDemoApplication.main
cnblogscomtheRhyme.infrastructure.demos.common.CommonDemoApplication.main --> org.springframework.boot.SpringApplication.run
org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.run
org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.run
org.springframework.boot.SpringApplication.run --> org.springframework.boot.SpringApplication.refreshContext
org.springframework.boot.SpringApplication.refreshContext --> org.springframework.boot.SpringApplication.refresh
org.springframework.boot.SpringApplication.refresh --> org.springframework.boot.SpringApplication.refresh
org.springframework.boot.SpringApplication.refresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh --> org.springframework.context.support.AbstractApplicationContext.refresh
org.springframework.context.support.AbstractApplicationContext.refresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh --> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer --> org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize
org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize --> org.springframework.boot.web.embedded.tomcat.TomcatWebServer.startDaemonAwaitThread
org.springframework.boot.web.embedded.tomcat.TomcatWebServer.startDaemonAwaitThread --> idea.debugger.rt.GeneratedEvaluationClass.invoke
把內(nèi)容放入文本繪圖中,即可得到如下流程圖:


到此這篇關(guān)于SpringBoot main方法結(jié)束程序不停止的原因分析及解決方法的文章就介紹到這了,更多相關(guān)SpringBoot main方法結(jié)束程序不停止內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決java.util.zip.ZipException: Not in GZIP&nbs
這篇文章主要介紹了解決java.util.zip.ZipException: Not in GZIP format報錯問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12
Springboot整合Druid實現(xiàn)對訪問的監(jiān)控方式
這篇文章主要介紹了Springboot整合Druid實現(xiàn)對訪問的監(jiān)控方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
Spring Boot如何通過自定義注解實現(xiàn)日志打印詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot如何通過自定義注解實現(xiàn)日志打印的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
SpringCloud3.x集成BigQuery的代碼實現(xiàn)
Google BigQuery 是一種高性能、可應(yīng)用于大數(shù)據(jù)分析的公主云數(shù)據(jù)庫服務(wù),Spring Cloud 提供了完善的工具和核心功能,可以進(jìn)行泛動分布應(yīng)用構(gòu)建,本文給大家介紹了SpringCloud3.x集成BigQuery的代碼實現(xiàn),需要的朋友可以參考下2025-01-01
Spring Boot Excel文件導(dǎo)出下載實現(xiàn)代碼
這篇文章帶領(lǐng)我們直接實現(xiàn)Excel文件的直接導(dǎo)出下載,后續(xù)開發(fā)不需要開發(fā)很多代碼,直接繼承已經(jīng)寫好的代碼,增加一個Xml配置就可以直接導(dǎo)出。具體實現(xiàn)代碼大家跟隨小編一起通過本文學(xué)習(xí)吧2018-11-11
Spring+Mybatis 實現(xiàn)aop數(shù)據(jù)庫讀寫分離與多數(shù)據(jù)庫源配置操作
這篇文章主要介紹了Spring+Mybatis 實現(xiàn)aop數(shù)據(jù)庫讀寫分離與多數(shù)據(jù)庫源配置操作,需要的朋友可以參考下2017-09-09
Spring Boot熱加載jar實現(xiàn)動態(tài)插件的思路
本文主要介紹在 Spring Boot 工程中熱加載 jar 包并注冊成為 Bean 對象的一種實現(xiàn)思路,在動態(tài)擴(kuò)展功能的同時支持在插件中注入主程序的 Bean 實現(xiàn)功能更強(qiáng)大的插件2021-10-10
Springboot項目保存本地系統(tǒng)日志文件的實現(xiàn)方法
這篇文章主要介紹了Springboot項目保存本地系統(tǒng)日志文件的實現(xiàn)方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04

