11/*
2- * Copyright 2002-2024 the original author or authors.
2+ * Copyright 2002-2025 the original author or authors.
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
2626import java .util .Map ;
2727import java .util .Set ;
2828import java .util .TreeMap ;
29+ import java .util .concurrent .CompletableFuture ;
2930import java .util .concurrent .ConcurrentHashMap ;
3031import java .util .concurrent .CountDownLatch ;
3132import java .util .concurrent .CyclicBarrier ;
33+ import java .util .concurrent .ExecutionException ;
34+ import java .util .concurrent .Executor ;
3235import java .util .concurrent .TimeUnit ;
3336
3437import org .apache .commons .logging .Log ;
5255import org .springframework .lang .Nullable ;
5356import org .springframework .util .Assert ;
5457import org .springframework .util .ClassUtils ;
58+ import org .springframework .util .CollectionUtils ;
5559
5660/**
5761 * Spring's default implementation of the {@link LifecycleProcessor} strategy.
6165 * interactions on a {@link org.springframework.context.ConfigurableApplicationContext}.
6266 *
6367 * <p>As of 6.1, this also includes support for JVM checkpoint/restore (Project CRaC)
64- * when the {@code org.crac:crac} dependency on the classpath.
68+ * when the {@code org.crac:crac} dependency is on the classpath. All running beans
69+ * will get stopped and restarted according to the CRaC checkpoint/restore callbacks.
70+ *
71+ * <p>As of 6.2, this processor can be configured with custom timeouts for specific
72+ * shutdown phases, applied to {@link SmartLifecycle#stop(Runnable)} implementations.
73+ * As of 6.2.6, there is also support for the concurrent startup of specific phases
74+ * with individual timeouts, triggering the {@link SmartLifecycle#start()} callbacks
75+ * of all associated beans asynchronously and then waiting for all of them to return,
76+ * as an alternative to the default sequential startup of beans without a timeout.
6577 *
6678 * @author Mark Fisher
6779 * @author Juergen Hoeller
6880 * @author Sebastien Deleuze
6981 * @since 3.0
82+ * @see SmartLifecycle#getPhase()
83+ * @see #setConcurrentStartupForPhase
84+ * @see #setTimeoutForShutdownPhase
7085 */
7186public class DefaultLifecycleProcessor implements LifecycleProcessor , BeanFactoryAware {
7287
@@ -102,6 +117,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
102117
103118 private final Log logger = LogFactory .getLog (getClass ());
104119
120+ private final Map <Integer , Long > concurrentStartupForPhases = new ConcurrentHashMap <>();
121+
105122 private final Map <Integer , Long > timeoutsForShutdownPhases = new ConcurrentHashMap <>();
106123
107124 private volatile long timeoutPerShutdownPhase = 10000 ;
@@ -130,20 +147,59 @@ else if (checkpointOnRefresh) {
130147 }
131148
132149
150+ /**
151+ * Switch to concurrent startup for each given phase (group of {@link SmartLifecycle}
152+ * beans with the same 'phase' value) with corresponding timeouts.
153+ * <p><b>Note: By default, the startup for every phase will be sequential without
154+ * a timeout. Calling this setter with timeouts for the given phases switches to a
155+ * mode where the beans in these phases will be started concurrently, cancelling
156+ * the startup if the corresponding timeout is not met for any of these phases.</b>
157+ * <p>For an actual concurrent startup, a bootstrap {@code Executor} needs to be
158+ * set for the application context, typically through a "bootstrapExecutor" bean.
159+ * @param phasesWithTimeouts a map of phase values (matching
160+ * {@link SmartLifecycle#getPhase()}) and corresponding timeout values
161+ * (in milliseconds)
162+ * @since 6.2.6
163+ * @see SmartLifecycle#getPhase()
164+ * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor()
165+ */
166+ public void setConcurrentStartupForPhases (Map <Integer , Long > phasesWithTimeouts ) {
167+ this .concurrentStartupForPhases .putAll (phasesWithTimeouts );
168+ }
169+
170+ /**
171+ * Switch to concurrent startup for a specific phase (group of {@link SmartLifecycle}
172+ * beans with the same 'phase' value) with a corresponding timeout.
173+ * <p><b>Note: By default, the startup for every phase will be sequential without
174+ * a timeout. Calling this setter with a timeout for the given phase switches to a
175+ * mode where the beans in this phase will be started concurrently, cancelling
176+ * the startup if the corresponding timeout is not met for this phase.</b>
177+ * <p>For an actual concurrent startup, a bootstrap {@code Executor} needs to be
178+ * set for the application context, typically through a "bootstrapExecutor" bean.
179+ * @param phase the phase value (matching {@link SmartLifecycle#getPhase()})
180+ * @param timeout the corresponding timeout value (in milliseconds)
181+ * @since 6.2.6
182+ * @see SmartLifecycle#getPhase()
183+ * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor()
184+ */
185+ public void setConcurrentStartupForPhase (int phase , long timeout ) {
186+ this .concurrentStartupForPhases .put (phase , timeout );
187+ }
188+
133189 /**
134190 * Specify the maximum time allotted for the shutdown of each given phase
135191 * (group of {@link SmartLifecycle} beans with the same 'phase' value).
136192 * <p>In case of no specific timeout configured, the default timeout per
137193 * shutdown phase will apply: 10000 milliseconds (10 seconds) as of 6.2.
138- * @param timeoutsForShutdownPhases a map of phase values (matching
194+ * @param phasesWithTimeouts a map of phase values (matching
139195 * {@link SmartLifecycle#getPhase()}) and corresponding timeout values
140196 * (in milliseconds)
141197 * @since 6.2
142198 * @see SmartLifecycle#getPhase()
143199 * @see #setTimeoutPerShutdownPhase
144200 */
145- public void setTimeoutsForShutdownPhases (Map <Integer , Long > timeoutsForShutdownPhases ) {
146- this .timeoutsForShutdownPhases .putAll (timeoutsForShutdownPhases );
201+ public void setTimeoutsForShutdownPhases (Map <Integer , Long > phasesWithTimeouts ) {
202+ this .timeoutsForShutdownPhases .putAll (phasesWithTimeouts );
147203 }
148204
149205 /**
@@ -171,17 +227,15 @@ public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) {
171227 this .timeoutPerShutdownPhase = timeoutPerShutdownPhase ;
172228 }
173229
174- private long determineTimeout (int phase ) {
175- Long timeout = this .timeoutsForShutdownPhases .get (phase );
176- return (timeout != null ? timeout : this .timeoutPerShutdownPhase );
177- }
178-
179230 @ Override
180231 public void setBeanFactory (BeanFactory beanFactory ) {
181232 if (!(beanFactory instanceof ConfigurableListableBeanFactory clbf )) {
182233 throw new IllegalArgumentException (
183234 "DefaultLifecycleProcessor requires a ConfigurableListableBeanFactory: " + beanFactory );
184235 }
236+ if (!this .concurrentStartupForPhases .isEmpty () && clbf .getBootstrapExecutor () == null ) {
237+ throw new IllegalStateException ("'bootstrapExecutor' needs to be configured for concurrent startup" );
238+ }
185239 this .beanFactory = clbf ;
186240 }
187241
@@ -191,6 +245,22 @@ private ConfigurableListableBeanFactory getBeanFactory() {
191245 return beanFactory ;
192246 }
193247
248+ private Executor getBootstrapExecutor () {
249+ Executor executor = getBeanFactory ().getBootstrapExecutor ();
250+ Assert .state (executor != null , "No 'bootstrapExecutor' available" );
251+ return executor ;
252+ }
253+
254+ @ Nullable
255+ private Long determineConcurrentStartup (int phase ) {
256+ return this .concurrentStartupForPhases .get (phase );
257+ }
258+
259+ private long determineShutdownTimeout (int phase ) {
260+ Long timeout = this .timeoutsForShutdownPhases .get (phase );
261+ return (timeout != null ? timeout : this .timeoutPerShutdownPhase );
262+ }
263+
194264
195265 // Lifecycle implementation
196266
@@ -285,9 +355,8 @@ private void startBeans(boolean autoStartupOnly) {
285355 lifecycleBeans .forEach ((beanName , bean ) -> {
286356 if (!autoStartupOnly || isAutoStartupCandidate (beanName , bean )) {
287357 int startupPhase = getPhase (bean );
288- phases .computeIfAbsent (startupPhase ,
289- phase -> new LifecycleGroup (phase , determineTimeout (phase ), lifecycleBeans , autoStartupOnly )
290- ).add (beanName , bean );
358+ phases .computeIfAbsent (startupPhase , phase -> new LifecycleGroup (phase , lifecycleBeans , autoStartupOnly ))
359+ .add (beanName , bean );
291360 }
292361 });
293362
@@ -308,30 +377,41 @@ private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) {
308377 * @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
309378 * @param beanName the name of the bean to start
310379 */
311- private void doStart (Map <String , ? extends Lifecycle > lifecycleBeans , String beanName , boolean autoStartupOnly ) {
380+ private void doStart (Map <String , ? extends Lifecycle > lifecycleBeans , String beanName ,
381+ boolean autoStartupOnly , @ Nullable List <CompletableFuture <?>> futures ) {
382+
312383 Lifecycle bean = lifecycleBeans .remove (beanName );
313384 if (bean != null && bean != this ) {
314385 String [] dependenciesForBean = getBeanFactory ().getDependenciesForBean (beanName );
315386 for (String dependency : dependenciesForBean ) {
316- doStart (lifecycleBeans , dependency , autoStartupOnly );
387+ doStart (lifecycleBeans , dependency , autoStartupOnly , futures );
317388 }
318389 if (!bean .isRunning () && (!autoStartupOnly || toBeStarted (beanName , bean ))) {
319- if (logger . isTraceEnabled () ) {
320- logger . trace ( "Starting bean '" + beanName + "' of type [" + bean . getClass (). getName () + "]" );
390+ if (futures != null ) {
391+ futures . add ( CompletableFuture . runAsync (() -> doStart ( beanName , bean ), getBootstrapExecutor ()) );
321392 }
322- try {
323- bean .start ();
324- }
325- catch (Throwable ex ) {
326- throw new ApplicationContextException ("Failed to start bean '" + beanName + "'" , ex );
327- }
328- if (logger .isDebugEnabled ()) {
329- logger .debug ("Successfully started bean '" + beanName + "'" );
393+ else {
394+ doStart (beanName , bean );
330395 }
331396 }
332397 }
333398 }
334399
400+ private void doStart (String beanName , Lifecycle bean ) {
401+ if (logger .isTraceEnabled ()) {
402+ logger .trace ("Starting bean '" + beanName + "' of type [" + bean .getClass ().getName () + "]" );
403+ }
404+ try {
405+ bean .start ();
406+ }
407+ catch (Throwable ex ) {
408+ throw new ApplicationContextException ("Failed to start bean '" + beanName + "'" , ex );
409+ }
410+ if (logger .isDebugEnabled ()) {
411+ logger .debug ("Successfully started bean '" + beanName + "'" );
412+ }
413+ }
414+
335415 private boolean toBeStarted (String beanName , Lifecycle bean ) {
336416 Set <String > stoppedBeans = this .stoppedBeans ;
337417 return (stoppedBeans != null ? stoppedBeans .contains (beanName ) :
@@ -344,9 +424,8 @@ private void stopBeans() {
344424
345425 lifecycleBeans .forEach ((beanName , bean ) -> {
346426 int shutdownPhase = getPhase (bean );
347- phases .computeIfAbsent (shutdownPhase ,
348- phase -> new LifecycleGroup (phase , determineTimeout (phase ), lifecycleBeans , false )
349- ).add (beanName , bean );
427+ phases .computeIfAbsent (shutdownPhase , phase -> new LifecycleGroup (phase , lifecycleBeans , false ))
428+ .add (beanName , bean );
350429 });
351430
352431 if (!phases .isEmpty ()) {
@@ -417,7 +496,7 @@ else if (bean instanceof SmartLifecycle) {
417496 }
418497
419498
420- // overridable hooks
499+ // Overridable hooks
421500
422501 /**
423502 * Retrieve all applicable Lifecycle beans: all singletons that have already been created,
@@ -473,8 +552,6 @@ private class LifecycleGroup {
473552
474553 private final int phase ;
475554
476- private final long timeout ;
477-
478555 private final Map <String , ? extends Lifecycle > lifecycleBeans ;
479556
480557 private final boolean autoStartupOnly ;
@@ -483,11 +560,8 @@ private class LifecycleGroup {
483560
484561 private int smartMemberCount ;
485562
486- public LifecycleGroup (
487- int phase , long timeout , Map <String , ? extends Lifecycle > lifecycleBeans , boolean autoStartupOnly ) {
488-
563+ public LifecycleGroup (int phase , Map <String , ? extends Lifecycle > lifecycleBeans , boolean autoStartupOnly ) {
489564 this .phase = phase ;
490- this .timeout = timeout ;
491565 this .lifecycleBeans = lifecycleBeans ;
492566 this .autoStartupOnly = autoStartupOnly ;
493567 }
@@ -506,8 +580,26 @@ public void start() {
506580 if (logger .isDebugEnabled ()) {
507581 logger .debug ("Starting beans in phase " + this .phase );
508582 }
583+ Long concurrentStartup = determineConcurrentStartup (this .phase );
584+ List <CompletableFuture <?>> futures = (concurrentStartup != null ? new ArrayList <>() : null );
509585 for (LifecycleGroupMember member : this .members ) {
510- doStart (this .lifecycleBeans , member .name , this .autoStartupOnly );
586+ doStart (this .lifecycleBeans , member .name , this .autoStartupOnly , futures );
587+ }
588+ if (concurrentStartup != null && !CollectionUtils .isEmpty (futures )) {
589+ try {
590+ CompletableFuture .allOf (futures .toArray (new CompletableFuture <?>[0 ]))
591+ .get (concurrentStartup , TimeUnit .MILLISECONDS );
592+ }
593+ catch (Exception ex ) {
594+ if (ex instanceof ExecutionException exEx ) {
595+ Throwable cause = exEx .getCause ();
596+ if (cause instanceof ApplicationContextException acEx ) {
597+ throw acEx ;
598+ }
599+ }
600+ throw new ApplicationContextException ("Failed to start beans in phase " + this .phase +
601+ " within timeout of " + concurrentStartup + "ms" , ex );
602+ }
511603 }
512604 }
513605
@@ -531,11 +623,14 @@ else if (member.bean instanceof SmartLifecycle) {
531623 }
532624 }
533625 try {
534- latch .await (this .timeout , TimeUnit .MILLISECONDS );
535- if (latch .getCount () > 0 && !countDownBeanNames .isEmpty () && logger .isInfoEnabled ()) {
536- logger .info ("Shutdown phase " + this .phase + " ends with " + countDownBeanNames .size () +
537- " bean" + (countDownBeanNames .size () > 1 ? "s" : "" ) +
538- " still running after timeout of " + this .timeout + "ms: " + countDownBeanNames );
626+ long shutdownTimeout = determineShutdownTimeout (this .phase );
627+ if (!latch .await (shutdownTimeout , TimeUnit .MILLISECONDS )) {
628+ // Count is still >0 after timeout
629+ if (!countDownBeanNames .isEmpty () && logger .isInfoEnabled ()) {
630+ logger .info ("Shutdown phase " + this .phase + " ends with " + countDownBeanNames .size () +
631+ " bean" + (countDownBeanNames .size () > 1 ? "s" : "" ) +
632+ " still running after timeout of " + shutdownTimeout + "ms: " + countDownBeanNames );
633+ }
539634 }
540635 }
541636 catch (InterruptedException ex ) {
0 commit comments