Introduce ConfigurableApplicationContext.pause() and SmartLifecycle.isPauseable()

Closes gh-35269
This commit is contained in:
Juergen Hoeller
2025-08-01 15:08:15 +02:00
parent 96bc1f50c7
commit 149d468ce4
12 changed files with 191 additions and 53 deletions
@@ -221,16 +221,27 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life
void refresh() throws BeansException, IllegalStateException;
/**
* Stop all beans in this application context if necessary, and subsequently
* Pause all beans in this application context if necessary, and subsequently
* restart all auto-startup beans, effectively restoring the lifecycle state
* after {@link #refresh()} (typically after a preceding {@link #stop()} call
* when a full {@link #start()} of even lazy-starting beans is to be avoided).
* @since 7.0
* @see #stop()
* @see #pause()
* @see #start()
* @see SmartLifecycle#isAutoStartup()
*/
void restart();
/**
* Stop all beans in this application context unless they explicitly opt out of
* pausing through {@link SmartLifecycle#isPauseable()} returning {@code false}.
* @since 7.0
* @see #restart()
* @see #stop()
* @see SmartLifecycle#isPauseable()
*/
void pause();
/**
* Register a shutdown hook with the JVM runtime, closing this context
* on JVM shutdown unless it has already been closed at that time.
@@ -44,6 +44,15 @@ public interface LifecycleProcessor extends Lifecycle {
start();
}
/**
* Notification of context pause for auto-stopping components.
* @since 7.0
* @see ConfigurableApplicationContext#pause()
*/
default void onPause() {
stop();
}
/**
* Notification of context close phase for auto-stopping components
* before destruction.
@@ -85,7 +85,7 @@ public interface SmartLifecycle extends Lifecycle, Phased {
/**
* Returns {@code true} if this {@code Lifecycle} component should get
* started automatically by the container at the time that the containing
* {@link ApplicationContext} gets refreshed.
* {@link ApplicationContext} gets refreshed or restarted.
* <p>A value of {@code false} indicates that the component is intended to
* be started through an explicit {@link #start()} call instead, analogous
* to a plain {@link Lifecycle} implementation.
@@ -93,12 +93,35 @@ public interface SmartLifecycle extends Lifecycle, Phased {
* @see #start()
* @see #getPhase()
* @see LifecycleProcessor#onRefresh()
* @see LifecycleProcessor#onRestart()
* @see ConfigurableApplicationContext#refresh()
* @see ConfigurableApplicationContext#restart()
*/
default boolean isAutoStartup() {
return true;
}
/**
* Returns {@code true} if this {@code Lifecycle} component is able to
* participate in a restart sequence, receiving corresponding {@link #stop()}
* and {@link #start()} calls with a potential pause in-between.
* <p>A value of {@code false} indicates that the component prefers to
* be skipped in a pause scenario, neither receiving a {@link #stop()}
* call nor a subsequent {@link #start()} call, analogous to a plain
* {@link Lifecycle} implementation. It will only receive a {@link #stop()}
* call on close and on explicit context-wide stopping but not on pause.
* <p>The default implementation returns {@code true}.
* @since 7.0
* @see #stop()
* @see LifecycleProcessor#onPause()
* @see LifecycleProcessor#onClose()
* @see ConfigurableApplicationContext#pause()
* @see ConfigurableApplicationContext#close()
*/
default boolean isPauseable() {
return true;
}
/**
* Indicates that a Lifecycle component must stop if it is currently running.
* <p>The provided callback is used by the {@link LifecycleProcessor} to support
@@ -0,0 +1,46 @@
/*
* Copyright 2002-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.event;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Event raised when an {@code ApplicationContext} gets paused.
*
* <p>Note that {@code ContextPausedEvent} is a specialization of
* {@link ContextStoppedEvent}.
*
* @author Juergen Hoeller
* @since 7.0
* @see ConfigurableApplicationContext#pause()
* @see ContextRestartedEvent
* @see ContextStoppedEvent
*/
@SuppressWarnings("serial")
public class ContextPausedEvent extends ContextStoppedEvent {
/**
* Create a new {@code ContextRestartedEvent}.
* @param source the {@code ContextPausedEvent} that has been restarted
* (must not be {@code null})
*/
public ContextPausedEvent(ApplicationContext source) {
super(source);
}
}
@@ -17,6 +17,7 @@
package org.springframework.context.event;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Event raised when an {@code ApplicationContext} gets restarted.
@@ -26,8 +27,9 @@ import org.springframework.context.ApplicationContext;
*
* @author Sam Brannen
* @since 7.0
* @see ConfigurableApplicationContext#restart()
* @see ContextPausedEvent
* @see ContextStartedEvent
* @see ContextStoppedEvent
*/
@SuppressWarnings("serial")
public class ContextRestartedEvent extends ContextStartedEvent {
@@ -66,6 +66,7 @@ import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextPausedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextRestartedEvent;
import org.springframework.context.event.ContextStartedEvent;
@@ -1555,6 +1556,12 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
publishEvent(new ContextRestartedEvent(this));
}
@Override
public void pause() {
getLifecycleProcessor().onPause();
publishEvent(new ContextPausedEvent(this));
}
@Override
public boolean isRunning() {
return (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning());
@@ -287,7 +287,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
*/
@Override
public void stop() {
stopBeans();
stopBeans(false);
this.running = false;
}
@@ -308,7 +308,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
catch (ApplicationContextException ex) {
// Some bean failed to auto-start within context refresh:
// stop already started beans on context refresh failure.
stopBeans();
stopBeans(false);
throw ex;
}
this.running = true;
@@ -318,15 +318,23 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
public void onRestart() {
this.stoppedBeans = null;
if (this.running) {
stopBeans();
stopBeans(true);
}
startBeans(true);
this.running = true;
}
@Override
public void onPause() {
if (this.running) {
stopBeans(true);
this.running = false;
}
}
@Override
public void onClose() {
stopBeans();
stopBeans(false);
this.running = false;
}
@@ -341,7 +349,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
void stopForRestart() {
if (this.running) {
this.stoppedBeans = ConcurrentHashMap.newKeySet();
stopBeans();
stopBeans(false);
this.running = false;
}
}
@@ -361,7 +369,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
lifecycleBeans.forEach((beanName, bean) -> {
if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
int startupPhase = getPhase(bean);
phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly))
phases.computeIfAbsent(
startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly, false))
.add(beanName, bean);
}
});
@@ -424,13 +433,14 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
(!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup()));
}
private void stopBeans() {
private void stopBeans(boolean pauseableOnly) {
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder());
lifecycleBeans.forEach((beanName, bean) -> {
int shutdownPhase = getPhase(bean);
phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false))
phases.computeIfAbsent(
shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false, pauseableOnly))
.add(beanName, bean);
});
@@ -446,13 +456,13 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
* @param beanName the name of the bean to stop
*/
private void doStop(Map<String, ? extends Lifecycle> lifecycleBeans, final String beanName,
final CountDownLatch latch, final Set<String> countDownBeanNames) {
boolean pauseableOnly, final CountDownLatch latch, final Set<String> countDownBeanNames) {
Lifecycle bean = lifecycleBeans.remove(beanName);
if (bean != null) {
String[] dependentBeans = getBeanFactory().getDependentBeans(beanName);
for (String dependentBean : dependentBeans) {
doStop(lifecycleBeans, dependentBean, latch, countDownBeanNames);
doStop(lifecycleBeans, dependentBean, pauseableOnly, latch, countDownBeanNames);
}
try {
if (bean.isRunning()) {
@@ -461,20 +471,22 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
stoppedBeans.add(beanName);
}
if (bean instanceof SmartLifecycle smartLifecycle) {
if (logger.isTraceEnabled()) {
logger.trace("Asking bean '" + beanName + "' of type [" +
bean.getClass().getName() + "] to stop");
}
countDownBeanNames.add(beanName);
smartLifecycle.stop(() -> {
latch.countDown();
countDownBeanNames.remove(beanName);
if (logger.isDebugEnabled()) {
logger.debug("Bean '" + beanName + "' completed its stop procedure");
if (!pauseableOnly || smartLifecycle.isPauseable()) {
if (logger.isTraceEnabled()) {
logger.trace("Asking bean '" + beanName + "' of type [" +
bean.getClass().getName() + "] to stop");
}
});
countDownBeanNames.add(beanName);
smartLifecycle.stop(() -> {
latch.countDown();
countDownBeanNames.remove(beanName);
if (logger.isDebugEnabled()) {
logger.debug("Bean '" + beanName + "' completed its stop procedure");
}
});
}
}
else {
else if (!pauseableOnly) {
if (logger.isTraceEnabled()) {
logger.trace("Stopping bean '" + beanName + "' of type [" +
bean.getClass().getName() + "]");
@@ -562,14 +574,19 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
private final boolean autoStartupOnly;
private final boolean pauseableOnly;
private final List<LifecycleGroupMember> members = new ArrayList<>();
private int smartMemberCount;
public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans, boolean autoStartupOnly) {
public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans,
boolean autoStartupOnly, boolean pauseableOnly) {
this.phase = phase;
this.lifecycleBeans = lifecycleBeans;
this.autoStartupOnly = autoStartupOnly;
this.pauseableOnly = pauseableOnly;
}
public void add(String name, Lifecycle bean) {
@@ -621,7 +638,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
Set<String> lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet());
for (LifecycleGroupMember member : this.members) {
if (lifecycleBeanNames.contains(member.name)) {
doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames);
doStop(this.lifecycleBeans, member.name, this.pauseableOnly, latch, countDownBeanNames);
}
else if (member.bean instanceof SmartLifecycle) {
// Already removed: must have been a dependent bean from another phase