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
This commit is contained in:
Moritz Halbritter 2023-10-12 16:26:17 +02:00
parent 6880fb0fc8
commit fcf77ed65d
4 changed files with 109 additions and 0 deletions

View File

@ -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.

View File

@ -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<ContextClosedEvent> {
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;
}
}
}
}
}

View File

@ -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",

View File

@ -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<Thread> 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<Thread> threadsBeforeClose = getCurrentThreads();
assertThat(threadsBeforeClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isNotEmpty();
this.context.close();
Set<Thread> threadsAfterClose = getCurrentThreads();
assertThat(threadsAfterClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isEmpty();
}
private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
S state) {
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)
@ -1432,6 +1457,10 @@ class SpringApplicationTests {
};
}
private Set<Thread> getCurrentThreads() {
return Thread.getAllStackTraces().keySet();
}
static class TestEventListener<E extends ApplicationEvent> implements SmartApplicationListener {
private final Class<E> eventType;