org.springframework.context.support.DefaultLifecycleProcessor Maven / Gradle / Ivy
/*
* 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.
* 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.support;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
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.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.crac.CheckpointException;
import org.crac.Core;
import org.crac.RestoreException;
import org.crac.management.CRaCMXBean;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.Lifecycle;
import org.springframework.context.LifecycleProcessor;
import org.springframework.context.Phased;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.NativeDetector;
import org.springframework.core.SpringProperties;
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 checkpoint/restore (Project CRaC)
* when the {@code org.crac:crac} dependency on the classpath.
*
* @author Mark Fisher
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 3.0
*/
public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware {
/**
* Property name for a common context checkpoint: {@value}.
* @since 6.1
* @see #ON_REFRESH_VALUE
* @see org.crac.Core#checkpointRestore()
*/
public static final String CHECKPOINT_PROPERTY_NAME = "spring.context.checkpoint";
/**
* Property name for terminating the JVM when the context reaches a specific phase: {@value}.
* @since 6.1
* @see #ON_REFRESH_VALUE
*/
public static final String EXIT_PROPERTY_NAME = "spring.context.exit";
/**
* Recognized value for the context checkpoint and exit properties: {@value}.
* @since 6.1
* @see #CHECKPOINT_PROPERTY_NAME
* @see #EXIT_PROPERTY_NAME
*/
public static final String ON_REFRESH_VALUE = "onRefresh";
private static final boolean checkpointOnRefresh =
ON_REFRESH_VALUE.equalsIgnoreCase(SpringProperties.getProperty(CHECKPOINT_PROPERTY_NAME));
private static final boolean exitOnRefresh =
ON_REFRESH_VALUE.equalsIgnoreCase(SpringProperties.getProperty(EXIT_PROPERTY_NAME));
private final Log logger = LogFactory.getLog(getClass());
private volatile long timeoutPerShutdownPhase = 30000;
private volatile boolean running;
@Nullable
private volatile ConfigurableListableBeanFactory beanFactory;
@Nullable
private volatile Set stoppedBeans;
// Just for keeping a strong reference to the registered CRaC Resource, if any
@Nullable
private Object cracResource;
public DefaultLifecycleProcessor() {
if (!NativeDetector.inNativeImage() && ClassUtils.isPresent("org.crac.Core", getClass().getClassLoader())) {
this.cracResource = new CracDelegate().registerResource();
}
else if (checkpointOnRefresh) {
throw new IllegalStateException(
"Checkpoint on refresh requires a CRaC-enabled JVM and 'org.crac:crac' on the classpath");
}
}
/**
* Specify the maximum time allotted in milliseconds for the shutdown of any
* phase (group of {@link SmartLifecycle} beans with the same 'phase' value).
* The default value is 30000 milliseconds (30 seconds).
* @see SmartLifecycle#getPhase()
*/
public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) {
this.timeoutPerShutdownPhase = timeoutPerShutdownPhase;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (!(beanFactory instanceof ConfigurableListableBeanFactory clbf)) {
throw new IllegalArgumentException(
"DefaultLifecycleProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
}
this.beanFactory = clbf;
}
private ConfigurableListableBeanFactory getBeanFactory() {
ConfigurableListableBeanFactory beanFactory = this.beanFactory;
Assert.state(beanFactory != null, "No BeanFactory available");
return beanFactory;
}
// Lifecycle implementation
/**
* Start all registered beans that implement {@link Lifecycle} and are not
* already running. Any bean that implements {@link SmartLifecycle} will be
* started within its 'phase', and all phases will be ordered from lowest to
* highest value. All beans that do not implement {@link SmartLifecycle} will be
* started in the default phase 0. A bean declared as a dependency of another bean
* will be started before the dependent bean regardless of the declared phase.
*/
@Override
public void start() {
this.stoppedBeans = null;
startBeans(false);
// If any bean failed to explicitly start, the exception propagates here.
// The caller may choose to subsequently call stop() if appropriate.
this.running = true;
}
/**
* Stop all registered beans that implement {@link Lifecycle} and are
* currently running. Any bean that implements {@link SmartLifecycle} will be
* stopped within its 'phase', and all phases will be ordered from highest to
* lowest value. All beans that do not implement {@link SmartLifecycle} will be
* stopped in the default phase 0. A bean declared as dependent on another bean
* will be stopped before the dependency bean regardless of the declared phase.
*/
@Override
public void stop() {
stopBeans();
this.running = false;
}
@Override
public void onRefresh() {
if (checkpointOnRefresh) {
new CracDelegate().checkpointRestore();
}
if (exitOnRefresh) {
Runtime.getRuntime().halt(0);
}
this.stoppedBeans = null;
try {
startBeans(true);
}
catch (ApplicationContextException ex) {
// Some bean failed to auto-start within context refresh:
// stop already started beans on context refresh failure.
stopBeans();
throw ex;
}
this.running = true;
}
@Override
public void onClose() {
stopBeans();
this.running = false;
}
@Override
public boolean isRunning() {
return this.running;
}
// 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 || isAutoStartupCandidate(beanName, bean)) {
int phase = getPhase(bean);
phases.computeIfAbsent(
phase,
p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
).add(beanName, bean);
}
});
if (!phases.isEmpty()) {
phases.values().forEach(LifecycleGroup::start);
}
}
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.
* @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
* @param beanName the name of the bean to start
*/
private void doStart(Map lifecycleBeans, String beanName, boolean autoStartupOnly) {
Lifecycle bean = lifecycleBeans.remove(beanName);
if (bean != null && bean != this) {
String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName);
for (String dependency : dependenciesForBean) {
doStart(lifecycleBeans, dependency, autoStartupOnly);
}
if (!bean.isRunning() && (!autoStartupOnly || toBeStarted(beanName, bean))) {
if (logger.isTraceEnabled()) {
logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]");
}
try {
bean.start();
}
catch (Throwable ex) {
throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex);
}
if (logger.isDebugEnabled()) {
logger.debug("Successfully started bean '" + beanName + "'");
}
}
}
}
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());
lifecycleBeans.forEach((beanName, bean) -> {
int shutdownPhase = getPhase(bean);
phases.computeIfAbsent(
shutdownPhase,
p -> new LifecycleGroup(shutdownPhase, this.timeoutPerShutdownPhase, lifecycleBeans, false)
).add(beanName, bean);
});
if (!phases.isEmpty()) {
phases.values().forEach(LifecycleGroup::stop);
}
}
/**
* Stop the specified bean as part of the given set of Lifecycle beans,
* making sure that any beans that depends on it are stopped first.
* @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
* @param beanName the name of the bean to stop
*/
private void doStop(Map lifecycleBeans, final String beanName,
final CountDownLatch latch, final Set countDownBeanNames) {
Lifecycle bean = lifecycleBeans.remove(beanName);
if (bean != null) {
String[] dependentBeans = getBeanFactory().getDependentBeans(beanName);
for (String dependentBean : dependentBeans) {
doStop(lifecycleBeans, dependentBean, latch, countDownBeanNames);
}
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 [" +
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 {
if (logger.isTraceEnabled()) {
logger.trace("Stopping bean '" + beanName + "' of type [" +
bean.getClass().getName() + "]");
}
bean.stop();
if (logger.isDebugEnabled()) {
logger.debug("Successfully stopped bean '" + beanName + "'");
}
}
}
else if (bean instanceof SmartLifecycle) {
// Don't wait for beans that aren't running...
latch.countDown();
}
}
catch (Throwable ex) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to stop bean '" + beanName + "'", ex);
}
}
}
}
// overridable hooks
/**
* Retrieve all applicable Lifecycle beans: all singletons that have already been created,
* as well as all SmartLifecycle beans (even if they are marked as lazy-init).
* @return the Map of applicable beans, with bean names as keys and bean instances as values
*/
protected Map getLifecycleBeans() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
Map beans = new LinkedHashMap<>();
String[] beanNames = beanFactory.getBeanNamesForType(Lifecycle.class, false, false);
for (String beanName : beanNames) {
String beanNameToRegister = BeanFactoryUtils.transformedBeanName(beanName);
boolean isFactoryBean = beanFactory.isFactoryBean(beanNameToRegister);
String beanNameToCheck = (isFactoryBean ? BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
if ((beanFactory.containsSingleton(beanNameToRegister) &&
(!isFactoryBean || matchesBeanType(Lifecycle.class, beanNameToCheck, beanFactory))) ||
matchesBeanType(SmartLifecycle.class, beanNameToCheck, beanFactory)) {
Object bean = beanFactory.getBean(beanNameToCheck);
if (bean != this && bean instanceof Lifecycle lifecycle) {
beans.put(beanNameToRegister, lifecycle);
}
}
}
return beans;
}
private boolean matchesBeanType(Class targetType, String beanName, BeanFactory beanFactory) {
Class beanType = beanFactory.getType(beanName);
return (beanType != null && targetType.isAssignableFrom(beanType));
}
/**
* Determine the lifecycle phase of the given bean.
* The default implementation checks for the {@link Phased} interface, using
* a default of 0 otherwise. Can be overridden to apply other/further policies.
* @param bean the bean to introspect
* @return the phase (an integer value)
* @see Phased#getPhase()
* @see SmartLifecycle
*/
protected int getPhase(Lifecycle bean) {
return (bean instanceof Phased phased ? phased.getPhase() : 0);
}
/**
* Helper class for maintaining a group of Lifecycle beans that should be started
* and stopped together based on their 'phase' value (or the default value of 0).
* The group is expected to be created in an ad-hoc fashion and group members are
* expected to always have the same 'phase' value.
*/
private class LifecycleGroup {
private final int phase;
private final long timeout;
private final Map lifecycleBeans;
private final boolean autoStartupOnly;
private final List members = new ArrayList<>();
private int smartMemberCount;
public LifecycleGroup(
int phase, long timeout, Map lifecycleBeans, boolean autoStartupOnly) {
this.phase = phase;
this.timeout = timeout;
this.lifecycleBeans = lifecycleBeans;
this.autoStartupOnly = autoStartupOnly;
}
public void add(String name, Lifecycle bean) {
this.members.add(new LifecycleGroupMember(name, bean));
if (bean instanceof SmartLifecycle) {
this.smartMemberCount++;
}
}
public void start() {
if (this.members.isEmpty()) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Starting beans in phase " + this.phase);
}
for (LifecycleGroupMember member : this.members) {
doStart(this.lifecycleBeans, member.name, this.autoStartupOnly);
}
}
public void stop() {
if (this.members.isEmpty()) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Stopping beans in phase " + this.phase);
}
CountDownLatch latch = new CountDownLatch(this.smartMemberCount);
Set countDownBeanNames = Collections.synchronizedSet(new LinkedHashSet<>());
Set lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet());
for (LifecycleGroupMember member : this.members) {
if (lifecycleBeanNames.contains(member.name)) {
doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames);
}
else if (member.bean instanceof SmartLifecycle) {
// Already removed: must have been a dependent bean from another phase
latch.countDown();
}
}
try {
latch.await(this.timeout, TimeUnit.MILLISECONDS);
if (latch.getCount() > 0 && !countDownBeanNames.isEmpty() && logger.isInfoEnabled()) {
logger.info("Failed to shut down " + countDownBeanNames.size() + " bean" +
(countDownBeanNames.size() > 1 ? "s" : "") + " with phase value " +
this.phase + " within timeout of " + this.timeout + "ms: " + countDownBeanNames);
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
/**
* A simple record of a LifecycleGroup member.
*/
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 Object registerResource() {
logger.debug("Registering JVM checkpoint/restore callback for Spring-managed lifecycle beans");
CracResourceAdapter resourceAdapter = new CracResourceAdapter();
org.crac.Core.getGlobalContext().register(resourceAdapter);
return resourceAdapter;
}
public void checkpointRestore() {
logger.info("Triggering JVM checkpoint/restore");
try {
Core.checkpointRestore();
}
catch (UnsupportedOperationException ex) {
throw new ApplicationContextException("CRaC checkpoint not supported on current JVM", ex);
}
catch (CheckpointException ex) {
throw new ApplicationContextException("Failed to take CRaC checkpoint on refresh", ex);
}
catch (RestoreException ex) {
throw new ApplicationContextException("Failed to restore CRaC checkpoint on refresh", ex);
}
}
}
/**
* Resource adapter for Project CRaC, triggering a stop-and-restart cycle
* for Spring-managed lifecycle beans around a JVM checkpoint/restore.
* @since 6.1
* @see #stopForRestart()
* @see #restartAfterStop()
*/
private class CracResourceAdapter implements org.crac.Resource {
@Nullable
private CyclicBarrier barrier;
@Override
public void beforeCheckpoint(org.crac.Context context) {
// A non-daemon thread for preventing an accidental JVM shutdown before the checkpoint
this.barrier = new CyclicBarrier(2);
Thread thread = new Thread(() -> {
awaitPreventShutdownBarrier();
// Checkpoint happens here
awaitPreventShutdownBarrier();
}, "prevent-shutdown");
thread.setDaemon(false);
thread.start();
awaitPreventShutdownBarrier();
logger.debug("Stopping Spring-managed lifecycle beans before JVM checkpoint");
stopForRestart();
}
@Override
public void afterRestore(org.crac.Context context) {
logger.info("Restarting Spring-managed lifecycle beans after JVM restore");
restartAfterStop();
// Barrier for prevent-shutdown thread not needed anymore
this.barrier = null;
if (!checkpointOnRefresh) {
logger.info("Spring-managed lifecycle restart completed (restored JVM running for " +
CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore() + " ms)");
}
}
private void awaitPreventShutdownBarrier() {
try {
if (this.barrier != null) {
this.barrier.await();
}
}
catch (Exception ex) {
logger.trace("Exception from prevent-shutdown barrier", ex);
}
}
}
}