mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-08-29 03:06:45 +08:00
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:
parent
6880fb0fc8
commit
fcf77ed65d
@ -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.
|
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.
|
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.
|
||||||
|
@ -65,6 +65,7 @@ import org.springframework.context.annotation.AnnotationConfigUtils;
|
|||||||
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
|
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
|
||||||
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
|
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
|
||||||
import org.springframework.context.aot.AotApplicationContextInitializer;
|
import org.springframework.context.aot.AotApplicationContextInitializer;
|
||||||
|
import org.springframework.context.event.ContextClosedEvent;
|
||||||
import org.springframework.context.support.AbstractApplicationContext;
|
import org.springframework.context.support.AbstractApplicationContext;
|
||||||
import org.springframework.context.support.GenericApplicationContext;
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
import org.springframework.core.GenericTypeResolver;
|
import org.springframework.core.GenericTypeResolver;
|
||||||
@ -163,6 +164,7 @@ import org.springframework.util.function.ThrowingSupplier;
|
|||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Ethan Rubinson
|
* @author Ethan Rubinson
|
||||||
* @author Chris Bono
|
* @author Chris Bono
|
||||||
|
* @author Moritz Halbritter
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @see #run(Class, String[])
|
* @see #run(Class, String[])
|
||||||
* @see #run(Class[], String[])
|
* @see #run(Class[], String[])
|
||||||
@ -240,6 +242,8 @@ public class SpringApplication {
|
|||||||
|
|
||||||
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
|
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
|
||||||
|
|
||||||
|
private boolean keepAlive;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link SpringApplication} instance. The application context will load
|
* Create a new {@link SpringApplication} instance. The application context will load
|
||||||
* beans from the specified primary sources (see {@link SpringApplication class-level}
|
* beans from the specified primary sources (see {@link SpringApplication class-level}
|
||||||
@ -409,6 +413,11 @@ public class SpringApplication {
|
|||||||
if (this.lazyInitialization) {
|
if (this.lazyInitialization) {
|
||||||
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
|
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
|
||||||
}
|
}
|
||||||
|
if (this.keepAlive) {
|
||||||
|
KeepAlive keepAlive = new KeepAlive();
|
||||||
|
keepAlive.start();
|
||||||
|
context.addApplicationListener(keepAlive);
|
||||||
|
}
|
||||||
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
|
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
|
||||||
if (!AotDetector.useGeneratedArtifacts()) {
|
if (!AotDetector.useGeneratedArtifacts()) {
|
||||||
// Load the sources
|
// Load the sources
|
||||||
@ -1277,6 +1286,26 @@ public class SpringApplication {
|
|||||||
return this.applicationStartup;
|
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
|
* Return a {@link SpringApplicationShutdownHandlers} instance that can be used to add
|
||||||
* or remove handlers that perform actions before the JVM is shutdown.
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -387,6 +387,13 @@
|
|||||||
"type": "org.springframework.boot.cloud.CloudPlatform",
|
"type": "org.springframework.boot.cloud.CloudPlatform",
|
||||||
"description": "Override the Cloud Platform auto-detection."
|
"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",
|
"name": "spring.main.lazy-initialization",
|
||||||
"type": "java.lang.Boolean",
|
"type": "java.lang.Boolean",
|
||||||
|
@ -158,6 +158,7 @@ import static org.mockito.Mockito.spy;
|
|||||||
* @author Nguyen Bao Sach
|
* @author Nguyen Bao Sach
|
||||||
* @author Chris Bono
|
* @author Chris Bono
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
|
* @author Moritz Halbritter
|
||||||
*/
|
*/
|
||||||
@ExtendWith(OutputCaptureExtension.class)
|
@ExtendWith(OutputCaptureExtension.class)
|
||||||
class SpringApplicationTests {
|
class SpringApplicationTests {
|
||||||
@ -1390,6 +1391,30 @@ class SpringApplicationTests {
|
|||||||
assertThatNoException().isThrownBy(() -> this.context.getBean(SingleUseAdditionalConfig.class));
|
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(
|
private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
|
||||||
S state) {
|
S state) {
|
||||||
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)
|
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 {
|
static class TestEventListener<E extends ApplicationEvent> implements SmartApplicationListener {
|
||||||
|
|
||||||
private final Class<E> eventType;
|
private final Class<E> eventType;
|
||||||
|
Loading…
Reference in New Issue
Block a user