diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle
index 5f63a11fb03..df384a1d25c 100644
--- a/framework-platform/framework-platform.gradle
+++ b/framework-platform/framework-platform.gradle
@@ -110,6 +110,7 @@ dependencies {
api("org.awaitility:awaitility:4.2.0")
api("org.bouncycastle:bcpkix-jdk18on:1.72")
api("org.codehaus.jettison:jettison:1.5.4")
+ api("org.crac:crac:0.1.3")
api("org.dom4j:dom4j:2.1.4")
api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.8")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.3")
diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle
index 04d4e1e8c72..754bfcb72fe 100644
--- a/spring-context/spring-context.gradle
+++ b/spring-context/spring-context.gradle
@@ -22,6 +22,7 @@ dependencies {
optional("org.aspectj:aspectjweaver")
optional("org.apache.groovy:groovy")
optional("org.apache-extras.beanshell:bsh")
+ optional("org.crac:crac")
optional("org.hibernate:hibernate-validator")
optional("org.jetbrains.kotlin:kotlin-reflect")
optional("org.jetbrains.kotlin:kotlin-stdlib")
diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java
index 59bd2ca3e00..7510f11f514 100644
--- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java
+++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java
@@ -26,6 +26,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -43,16 +44,25 @@ import org.springframework.context.Phased;
import org.springframework.context.SmartLifecycle;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
/**
* Default implementation of the {@link LifecycleProcessor} strategy.
*
+ *
Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in
+ * groups for specific phases, on startup/shutdown as well as for explicit start/stop
+ * interactions on a {@link org.springframework.context.ConfigurableApplicationContext}.
+ * As of 6.1, this also includes support for JVM snapshot checkpoints (Project CRaC).
+ *
* @author Mark Fisher
* @author Juergen Hoeller
* @since 3.0
*/
public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware {
+ private static final boolean cracPresent =
+ ClassUtils.isPresent("org.crac.Core", DefaultLifecycleProcessor.class.getClassLoader());
+
private final Log logger = LogFactory.getLog(getClass());
private volatile long timeoutPerShutdownPhase = 30000;
@@ -62,6 +72,16 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
@Nullable
private volatile ConfigurableListableBeanFactory beanFactory;
+ @Nullable
+ private volatile Set stoppedBeans;
+
+
+ public DefaultLifecycleProcessor() {
+ if (cracPresent) {
+ new CracDelegate().registerResource();
+ }
+ }
+
/**
* Specify the maximum time allotted in milliseconds for the shutdown of
@@ -100,6 +120,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
*/
@Override
public void start() {
+ this.stoppedBeans = null;
startBeans(false);
this.running = true;
}
@@ -120,6 +141,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
@Override
public void onRefresh() {
+ this.stoppedBeans = null;
startBeans(true);
this.running = true;
}
@@ -138,12 +160,28 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
// Internal helpers
+ void stopForRestart() {
+ if (this.running) {
+ this.stoppedBeans = Collections.newSetFromMap(new ConcurrentHashMap<>());
+ stopBeans();
+ this.running = false;
+ }
+ }
+
+ void restartAfterStop() {
+ if (this.stoppedBeans != null) {
+ startBeans(true);
+ this.stoppedBeans = null;
+ this.running = true;
+ }
+ }
+
private void startBeans(boolean autoStartupOnly) {
Map lifecycleBeans = getLifecycleBeans();
Map phases = new TreeMap<>();
lifecycleBeans.forEach((beanName, bean) -> {
- if (!autoStartupOnly || (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup())) {
+ if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
int phase = getPhase(bean);
phases.computeIfAbsent(
phase,
@@ -156,6 +194,12 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
}
}
+ private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) {
+ Set stoppedBeans = this.stoppedBeans;
+ return (stoppedBeans != null ? stoppedBeans.contains(beanName) :
+ (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup()));
+ }
+
/**
* Start the specified bean as part of the given set of Lifecycle beans,
* making sure that any beans that it depends on are started first.
@@ -169,8 +213,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
for (String dependency : dependenciesForBean) {
doStart(lifecycleBeans, dependency, autoStartupOnly);
}
- if (!bean.isRunning() &&
- (!autoStartupOnly || !(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup())) {
+ if (!bean.isRunning() && (!autoStartupOnly || toBeStarted(beanName, bean))) {
if (logger.isTraceEnabled()) {
logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]");
}
@@ -187,6 +230,12 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
}
}
+ private boolean toBeStarted(String beanName, Lifecycle bean) {
+ Set stoppedBeans = this.stoppedBeans;
+ return (stoppedBeans != null ? stoppedBeans.contains(beanName) :
+ (!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup()));
+ }
+
private void stopBeans() {
Map lifecycleBeans = getLifecycleBeans();
Map phases = new TreeMap<>(Comparator.reverseOrder());
@@ -219,6 +268,10 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
}
try {
if (bean.isRunning()) {
+ Set stoppedBeans = this.stoppedBeans;
+ if (stoppedBeans != null) {
+ stoppedBeans.add(beanName);
+ }
if (bean instanceof SmartLifecycle smartLifecycle) {
if (logger.isTraceEnabled()) {
logger.trace("Asking bean '" + beanName + "' of type [" +
@@ -391,4 +444,41 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
*/
private record LifecycleGroupMember(String name, Lifecycle bean) {}
+
+ /**
+ * Inner class to avoid a hard dependency on Project CRaC at runtime.
+ * @since 6.1
+ * @see org.crac.Core
+ */
+ private class CracDelegate {
+
+ public void registerResource() {
+ logger.debug("Registering JVM snapshot callback for Spring-managed lifecycle beans");
+ org.crac.Core.getGlobalContext().register(new CracResourceAdapter());
+ }
+ }
+
+
+ /**
+ * Resource adapter for Project CRaC, triggering a stop-and-restart cycle
+ * for Spring-managed lifecycle beans around a JVM snapshot checkpoint.
+ * @since 6.1
+ * @see #stopForRestart()
+ * @see #restartAfterStop()
+ */
+ private class CracResourceAdapter implements org.crac.Resource {
+
+ @Override
+ public void beforeCheckpoint(org.crac.Context extends org.crac.Resource> context) {
+ logger.debug("Stopping Spring-managed lifecycle beans before JVM snapshot checkpoint");
+ stopForRestart();
+ }
+
+ @Override
+ public void afterRestore(org.crac.Context extends org.crac.Resource> context) {
+ logger.debug("Restarting Spring-managed lifecycle beans after JVM snapshot restore");
+ restartAfterStop();
+ }
+ }
+
}
diff --git a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java
index 9bcfa4e7a69..4cf9b08b9c1 100644
--- a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java
+++ b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING;
/**
* @author Mark Fisher
+ * @author Juergen Hoeller
* @since 3.0
*/
class DefaultLifecycleProcessorTests {
@@ -58,12 +59,12 @@ class DefaultLifecycleProcessorTests {
Object contextLifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor");
assertThat(contextLifecycleProcessor).isNotNull();
assertThat(contextLifecycleProcessor).isSameAs(bean);
- assertThat(new DirectFieldAccessor(contextLifecycleProcessor).getPropertyValue(
- "timeoutPerShutdownPhase")).isEqualTo(1000L);
+ assertThat(new DirectFieldAccessor(contextLifecycleProcessor).getPropertyValue("timeoutPerShutdownPhase"))
+ .isEqualTo(1000L);
}
@Test
- void singleSmartLifecycleAutoStartup() throws Exception {
+ void singleSmartLifecycleAutoStartup() {
CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
bean.setAutoStartup(true);
@@ -79,7 +80,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void singleSmartLifecycleAutoStartupWithLazyInit() throws Exception {
+ void singleSmartLifecycleAutoStartupWithLazyInit() {
StaticApplicationContext context = new StaticApplicationContext();
RootBeanDefinition bd = new RootBeanDefinition(DummySmartLifecycleBean.class);
bd.setLazyInit(true);
@@ -93,7 +94,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() throws Exception {
+ void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() {
StaticApplicationContext context = new StaticApplicationContext();
RootBeanDefinition bd = new RootBeanDefinition(DummySmartLifecycleFactoryBean.class);
bd.setLazyInit(true);
@@ -107,7 +108,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void singleSmartLifecycleWithoutAutoStartup() throws Exception {
+ void singleSmartLifecycleWithoutAutoStartup() {
CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
bean.setAutoStartup(false);
@@ -125,7 +126,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() throws Exception {
+ void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() {
CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
bean.setAutoStartup(true);
@@ -148,7 +149,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void smartLifecycleGroupStartup() throws Exception {
+ void smartLifecycleGroupStartup() {
CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans);
TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
@@ -183,7 +184,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void contextRefreshThenStartWithMixedBeans() throws Exception {
+ void contextRefreshThenStartWithMixedBeans() {
CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans);
TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans);
@@ -218,7 +219,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void contextRefreshThenStopAndRestartWithMixedBeans() throws Exception {
+ void contextRefreshThenStopAndRestartWithMixedBeans() {
CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans);
TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans);
@@ -259,9 +260,62 @@ class DefaultLifecycleProcessorTests {
context.close();
}
+ @Test
+ void contextRefreshThenStopForRestartWithMixedBeans() {
+ CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
+ TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans);
+ TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans);
+ TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans);
+ TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans);
+ StaticApplicationContext context = new StaticApplicationContext();
+ context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1);
+ context.getBeanFactory().registerSingleton("smartBean1", smartBean1);
+ context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2);
+ context.getBeanFactory().registerSingleton("smartBean2", smartBean2);
+ assertThat(simpleBean1.isRunning()).isFalse();
+ assertThat(simpleBean2.isRunning()).isFalse();
+ assertThat(smartBean1.isRunning()).isFalse();
+ assertThat(smartBean2.isRunning()).isFalse();
+ context.refresh();
+ DefaultLifecycleProcessor lifecycleProcessor = (DefaultLifecycleProcessor)
+ new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor");
+ assertThat(smartBean1.isRunning()).isTrue();
+ assertThat(smartBean2.isRunning()).isTrue();
+ assertThat(simpleBean1.isRunning()).isFalse();
+ assertThat(simpleBean2.isRunning()).isFalse();
+ smartBean2.stop();
+ simpleBean1.start();
+ assertThat(startedBeans).hasSize(3);
+ assertThat(getPhase(startedBeans.get(0))).isEqualTo(-3);
+ assertThat(getPhase(startedBeans.get(1))).isEqualTo(5);
+ assertThat(getPhase(startedBeans.get(2))).isEqualTo(0);
+ lifecycleProcessor.stopForRestart();
+ assertThat(simpleBean1.isRunning()).isFalse();
+ assertThat(simpleBean2.isRunning()).isFalse();
+ assertThat(smartBean1.isRunning()).isFalse();
+ assertThat(smartBean2.isRunning()).isFalse();
+ lifecycleProcessor.restartAfterStop();
+ assertThat(smartBean1.isRunning()).isTrue();
+ assertThat(smartBean2.isRunning()).isFalse();
+ assertThat(simpleBean1.isRunning()).isTrue();
+ assertThat(simpleBean2.isRunning()).isFalse();
+ assertThat(startedBeans).hasSize(5);
+ assertThat(getPhase(startedBeans.get(3))).isEqualTo(0);
+ assertThat(getPhase(startedBeans.get(4))).isEqualTo(5);
+ context.start();
+ assertThat(smartBean1.isRunning()).isTrue();
+ assertThat(smartBean2.isRunning()).isTrue();
+ assertThat(simpleBean1.isRunning()).isTrue();
+ assertThat(simpleBean2.isRunning()).isTrue();
+ assertThat(startedBeans).hasSize(7);
+ assertThat(getPhase(startedBeans.get(5))).isEqualTo(-3);
+ assertThat(getPhase(startedBeans.get(6))).isEqualTo(0);
+ context.close();
+ }
+
@Test
@EnabledForTestGroups(LONG_RUNNING)
- void smartLifecycleGroupShutdown() throws Exception {
+ void smartLifecycleGroupShutdown() {
CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 300, stoppedBeans);
TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(3, 100, stoppedBeans);
@@ -292,7 +346,7 @@ class DefaultLifecycleProcessorTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
- void singleSmartLifecycleShutdown() throws Exception {
+ void singleSmartLifecycleShutdown() {
CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forShutdownTests(99, 300, stoppedBeans);
StaticApplicationContext context = new StaticApplicationContext();
@@ -307,7 +361,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void singleLifecycleShutdown() throws Exception {
+ void singleLifecycleShutdown() {
CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>();
Lifecycle bean = new TestLifecycleBean(null, stoppedBeans);
StaticApplicationContext context = new StaticApplicationContext();
@@ -324,7 +378,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void mixedShutdown() throws Exception {
+ void mixedShutdown() {
CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>();
Lifecycle bean1 = TestLifecycleBean.forShutdownTests(stoppedBeans);
Lifecycle bean2 = TestSmartLifecycleBean.forShutdownTests(500, 200, stoppedBeans);
@@ -373,7 +427,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void dependencyStartedFirstEvenIfItsPhaseIsHigher() throws Exception {
+ void dependencyStartedFirstEvenIfItsPhaseIsHigher() {
CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans);
TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans);
@@ -403,7 +457,7 @@ class DefaultLifecycleProcessorTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
- void dependentShutdownFirstEvenIfItsPhaseIsLower() throws Exception {
+ void dependentShutdownFirstEvenIfItsPhaseIsLower() {
CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 100, stoppedBeans);
TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans);
@@ -446,7 +500,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void dependencyStartedFirstAndIsSmartLifecycle() throws Exception {
+ void dependencyStartedFirstAndIsSmartLifecycle() {
CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forStartupTests(-99, startedBeans);
TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans);
@@ -478,7 +532,7 @@ class DefaultLifecycleProcessorTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
- void dependentShutdownFirstAndIsSmartLifecycle() throws Exception {
+ void dependentShutdownFirstAndIsSmartLifecycle() {
CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans);
TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forShutdownTests(-99, 100, stoppedBeans);
@@ -520,7 +574,7 @@ class DefaultLifecycleProcessorTests {
}
@Test
- void dependencyStartedFirstButNotSmartLifecycle() throws Exception {
+ void dependencyStartedFirstButNotSmartLifecycle() {
CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans);
TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans);
@@ -544,7 +598,7 @@ class DefaultLifecycleProcessorTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
- void dependentShutdownFirstButNotSmartLifecycle() throws Exception {
+ void dependentShutdownFirstButNotSmartLifecycle() {
CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans);
TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans);
@@ -583,8 +637,7 @@ class DefaultLifecycleProcessorTests {
private static int getPhase(Lifecycle lifecycle) {
- return (lifecycle instanceof SmartLifecycle) ?
- ((SmartLifecycle) lifecycle).getPhase() : 0;
+ return (lifecycle instanceof SmartLifecycle ? ((SmartLifecycle) lifecycle).getPhase() : 0);
}
@@ -596,7 +649,6 @@ class DefaultLifecycleProcessorTests {
private volatile boolean running;
-
static TestLifecycleBean forStartupTests(CopyOnWriteArrayList startedBeans) {
return new TestLifecycleBean(startedBeans, null);
}
@@ -734,7 +786,7 @@ class DefaultLifecycleProcessorTests {
DummySmartLifecycleBean bean = new DummySmartLifecycleBean();
@Override
- public Object getObject() throws Exception {
+ public Object getObject() {
return this.bean;
}