From fcf77ed65d6f79a57e177d06ebff5b3a48cf5407 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 12 Oct 2023 16:26:17 +0200 Subject: [PATCH] Add property to stop the JVM from exiting spring.main.keep-alive=true will spawn a non-daemon thread which stops if the context is closed Closes gh-37736 --- .../asciidoc/features/spring-application.adoc | 14 +++++ .../boot/SpringApplication.java | 59 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 7 +++ .../boot/SpringApplicationTests.java | 29 +++++++++ 4 files changed, 109 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc index 5255d703817..75c199c4921 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc @@ -376,3 +376,17 @@ Spring Boot ships with the `BufferingApplicationStartup` variant; this implement Applications can ask for the bean of type `BufferingApplicationStartup` in any component. Spring Boot can also be configured to expose a {spring-boot-actuator-restapi-docs}/#startup[`startup` endpoint] that provides this information as a JSON document. + + + +[[features.spring-application.virtual-threads]] +=== Virtual threads +If you're running on Java 21 or up, you can enable virtual threads by setting the property configprop:spring.threads.virtual.enabled[] to `true`. + +WARNING: One side effect of virtual threads is that these threads are daemon threads. +A JVM will exit if there are no non-daemon threads. +This behavior can be a problem when you rely on, e.g. `@Scheduled` beans to keep your application alive. +If you use virtual threads, the scheduler thread is a virtual thread and therefore a daemon thread and won't keep the JVM alive. +This does not only affect scheduling, but can be the case with other technologies, too! +To keep the JVM running in all cases, it is recommended to set the property configprop:spring.main.keep-alive[] to `true`. +This ensures that the JVM is kept alive, even if all threads are virtual threads. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 69f42ed0eac..8da2a18b0e3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -65,6 +65,7 @@ import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.aot.AotApplicationContextInitializer; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.GenericTypeResolver; @@ -163,6 +164,7 @@ import org.springframework.util.function.ThrowingSupplier; * @author Brian Clozel * @author Ethan Rubinson * @author Chris Bono + * @author Moritz Halbritter * @since 1.0.0 * @see #run(Class, String[]) * @see #run(Class[], String[]) @@ -240,6 +242,8 @@ public class SpringApplication { private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + private boolean keepAlive; + /** * Create a new {@link SpringApplication} instance. The application context will load * beans from the specified primary sources (see {@link SpringApplication class-level} @@ -409,6 +413,11 @@ public class SpringApplication { if (this.lazyInitialization) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); } + if (this.keepAlive) { + KeepAlive keepAlive = new KeepAlive(); + keepAlive.start(); + context.addApplicationListener(keepAlive); + } context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context)); if (!AotDetector.useGeneratedArtifacts()) { // Load the sources @@ -1277,6 +1286,26 @@ public class SpringApplication { return this.applicationStartup; } + /** + * Whether to keep the application alive even if there are no more non-daemon threads. + * @return whether to keep the application alive even if there are no more non-daemon + * threads + * @since 3.2.0 + */ + public boolean isKeepAlive() { + return this.keepAlive; + } + + /** + * Whether to keep the application alive even if there are no more non-daemon threads. + * @param keepAlive whether to keep the application alive even if there are no more + * non-daemon threads + * @since 3.2.0 + */ + public void setKeepAlive(boolean keepAlive) { + this.keepAlive = keepAlive; + } + /** * Return a {@link SpringApplicationShutdownHandlers} instance that can be used to add * or remove handlers that perform actions before the JVM is shutdown. @@ -1601,4 +1630,34 @@ public class SpringApplication { } + /** + * A non-daemon thread to keep the JVM alive. Reacts to {@link ContextClosedEvent} to + * stop itself when the application context is closed. + */ + private static final class KeepAlive extends Thread implements ApplicationListener { + + KeepAlive() { + setName("keep-alive"); + setDaemon(false); + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + interrupt(); + } + + @Override + public void run() { + while (true) { + try { + Thread.sleep(Long.MAX_VALUE); + } + catch (InterruptedException ex) { + break; + } + } + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 12f70410dd7..3484dd4ad10 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -387,6 +387,13 @@ "type": "org.springframework.boot.cloud.CloudPlatform", "description": "Override the Cloud Platform auto-detection." }, + { + "name": "spring.main.keep-alive", + "type": "java.lang.Boolean", + "sourceType": "org.springframework.boot.SpringApplication", + "description": "Whether to keep the application alive even if there are no more non-daemon threads.", + "defaultValue": false + }, { "name": "spring.main.lazy-initialization", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 9523dc903c2..8011aff1f2f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -158,6 +158,7 @@ import static org.mockito.Mockito.spy; * @author Nguyen Bao Sach * @author Chris Bono * @author Sebastien Deleuze + * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) class SpringApplicationTests { @@ -1390,6 +1391,30 @@ class SpringApplicationTests { assertThatNoException().isThrownBy(() -> this.context.getBean(SingleUseAdditionalConfig.class)); } + @Test + void shouldStartDaemonThreadIfKeepAliveIsEnabled() { + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setWebApplicationType(WebApplicationType.NONE); + this.context = application.run("--spring.main.keep-alive=true"); + Set threads = getCurrentThreads(); + assertThat(threads).filteredOn((thread) -> thread.getName().equals("keep-alive")) + .singleElement() + .satisfies((thread) -> assertThat(thread.isDaemon()).isFalse()); + } + + @Test + void shouldStopKeepAliveThreadIfContextIsClosed() { + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.setKeepAlive(true); + this.context = application.run(); + Set threadsBeforeClose = getCurrentThreads(); + assertThat(threadsBeforeClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isNotEmpty(); + this.context.close(); + Set threadsAfterClose = getCurrentThreads(); + assertThat(threadsAfterClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isEmpty(); + } + private ArgumentMatcher isAvailabilityChangeEventWithState( S state) { return (argument) -> (argument instanceof AvailabilityChangeEvent) @@ -1432,6 +1457,10 @@ class SpringApplicationTests { }; } + private Set getCurrentThreads() { + return Thread.getAllStackTraces().keySet(); + } + static class TestEventListener implements SmartApplicationListener { private final Class eventType;