1
1
/*
2
- * Copyright 2002-2024 the original author or authors.
2
+ * Copyright 2002-2025 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
26
26
import java .util .Map ;
27
27
import java .util .Set ;
28
28
import java .util .TreeMap ;
29
+ import java .util .concurrent .CompletableFuture ;
29
30
import java .util .concurrent .ConcurrentHashMap ;
30
31
import java .util .concurrent .CountDownLatch ;
31
32
import java .util .concurrent .CyclicBarrier ;
33
+ import java .util .concurrent .ExecutionException ;
34
+ import java .util .concurrent .Executor ;
32
35
import java .util .concurrent .TimeUnit ;
33
36
34
37
import org .apache .commons .logging .Log ;
52
55
import org .springframework .core .SpringProperties ;
53
56
import org .springframework .util .Assert ;
54
57
import org .springframework .util .ClassUtils ;
58
+ import org .springframework .util .CollectionUtils ;
55
59
56
60
/**
57
61
* Spring's default implementation of the {@link LifecycleProcessor} strategy.
61
65
* interactions on a {@link org.springframework.context.ConfigurableApplicationContext}.
62
66
*
63
67
* <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.
65
77
*
66
78
* @author Mark Fisher
67
79
* @author Juergen Hoeller
68
80
* @author Sebastien Deleuze
69
81
* @since 3.0
82
+ * @see SmartLifecycle#getPhase()
83
+ * @see #setConcurrentStartupForPhase
84
+ * @see #setTimeoutForShutdownPhase
70
85
*/
71
86
public class DefaultLifecycleProcessor implements LifecycleProcessor , BeanFactoryAware {
72
87
@@ -102,6 +117,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
102
117
103
118
private final Log logger = LogFactory .getLog (getClass ());
104
119
120
+ private final Map <Integer , Long > concurrentStartupForPhases = new ConcurrentHashMap <>();
121
+
105
122
private final Map <Integer , Long > timeoutsForShutdownPhases = new ConcurrentHashMap <>();
106
123
107
124
private volatile long timeoutPerShutdownPhase = 10000 ;
@@ -127,20 +144,59 @@ else if (checkpointOnRefresh) {
127
144
}
128
145
129
146
147
+ /**
148
+ * Switch to concurrent startup for each given phase (group of {@link SmartLifecycle}
149
+ * beans with the same 'phase' value) with corresponding timeouts.
150
+ * <p><b>Note: By default, the startup for every phase will be sequential without
151
+ * a timeout. Calling this setter with timeouts for the given phases switches to a
152
+ * mode where the beans in these phases will be started concurrently, cancelling
153
+ * the startup if the corresponding timeout is not met for any of these phases.</b>
154
+ * <p>For an actual concurrent startup, a bootstrap {@code Executor} needs to be
155
+ * set for the application context, typically through a "bootstrapExecutor" bean.
156
+ * @param phasesWithTimeouts a map of phase values (matching
157
+ * {@link SmartLifecycle#getPhase()}) and corresponding timeout values
158
+ * (in milliseconds)
159
+ * @since 6.2.6
160
+ * @see SmartLifecycle#getPhase()
161
+ * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor()
162
+ */
163
+ public void setConcurrentStartupForPhases (Map <Integer , Long > phasesWithTimeouts ) {
164
+ this .concurrentStartupForPhases .putAll (phasesWithTimeouts );
165
+ }
166
+
167
+ /**
168
+ * Switch to concurrent startup for a specific phase (group of {@link SmartLifecycle}
169
+ * beans with the same 'phase' value) with a corresponding timeout.
170
+ * <p><b>Note: By default, the startup for every phase will be sequential without
171
+ * a timeout. Calling this setter with a timeout for the given phase switches to a
172
+ * mode where the beans in this phase will be started concurrently, cancelling
173
+ * the startup if the corresponding timeout is not met for this phase.</b>
174
+ * <p>For an actual concurrent startup, a bootstrap {@code Executor} needs to be
175
+ * set for the application context, typically through a "bootstrapExecutor" bean.
176
+ * @param phase the phase value (matching {@link SmartLifecycle#getPhase()})
177
+ * @param timeout the corresponding timeout value (in milliseconds)
178
+ * @since 6.2.6
179
+ * @see SmartLifecycle#getPhase()
180
+ * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor()
181
+ */
182
+ public void setConcurrentStartupForPhase (int phase , long timeout ) {
183
+ this .concurrentStartupForPhases .put (phase , timeout );
184
+ }
185
+
130
186
/**
131
187
* Specify the maximum time allotted for the shutdown of each given phase
132
188
* (group of {@link SmartLifecycle} beans with the same 'phase' value).
133
189
* <p>In case of no specific timeout configured, the default timeout per
134
190
* shutdown phase will apply: 10000 milliseconds (10 seconds) as of 6.2.
135
- * @param timeoutsForShutdownPhases a map of phase values (matching
191
+ * @param phasesWithTimeouts a map of phase values (matching
136
192
* {@link SmartLifecycle#getPhase()}) and corresponding timeout values
137
193
* (in milliseconds)
138
194
* @since 6.2
139
195
* @see SmartLifecycle#getPhase()
140
196
* @see #setTimeoutPerShutdownPhase
141
197
*/
142
- public void setTimeoutsForShutdownPhases (Map <Integer , Long > timeoutsForShutdownPhases ) {
143
- this .timeoutsForShutdownPhases .putAll (timeoutsForShutdownPhases );
198
+ public void setTimeoutsForShutdownPhases (Map <Integer , Long > phasesWithTimeouts ) {
199
+ this .timeoutsForShutdownPhases .putAll (phasesWithTimeouts );
144
200
}
145
201
146
202
/**
@@ -168,17 +224,15 @@ public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) {
168
224
this .timeoutPerShutdownPhase = timeoutPerShutdownPhase ;
169
225
}
170
226
171
- private long determineTimeout (int phase ) {
172
- Long timeout = this .timeoutsForShutdownPhases .get (phase );
173
- return (timeout != null ? timeout : this .timeoutPerShutdownPhase );
174
- }
175
-
176
227
@ Override
177
228
public void setBeanFactory (BeanFactory beanFactory ) {
178
229
if (!(beanFactory instanceof ConfigurableListableBeanFactory clbf )) {
179
230
throw new IllegalArgumentException (
180
231
"DefaultLifecycleProcessor requires a ConfigurableListableBeanFactory: " + beanFactory );
181
232
}
233
+ if (!this .concurrentStartupForPhases .isEmpty () && clbf .getBootstrapExecutor () == null ) {
234
+ throw new IllegalStateException ("'bootstrapExecutor' needs to be configured for concurrent startup" );
235
+ }
182
236
this .beanFactory = clbf ;
183
237
}
184
238
@@ -188,6 +242,22 @@ private ConfigurableListableBeanFactory getBeanFactory() {
188
242
return beanFactory ;
189
243
}
190
244
245
+ private Executor getBootstrapExecutor () {
246
+ Executor executor = getBeanFactory ().getBootstrapExecutor ();
247
+ Assert .state (executor != null , "No 'bootstrapExecutor' available" );
248
+ return executor ;
249
+ }
250
+
251
+ @ Nullable
252
+ private Long determineConcurrentStartup (int phase ) {
253
+ return this .concurrentStartupForPhases .get (phase );
254
+ }
255
+
256
+ private long determineShutdownTimeout (int phase ) {
257
+ Long timeout = this .timeoutsForShutdownPhases .get (phase );
258
+ return (timeout != null ? timeout : this .timeoutPerShutdownPhase );
259
+ }
260
+
191
261
192
262
// Lifecycle implementation
193
263
@@ -282,9 +352,8 @@ private void startBeans(boolean autoStartupOnly) {
282
352
lifecycleBeans .forEach ((beanName , bean ) -> {
283
353
if (!autoStartupOnly || isAutoStartupCandidate (beanName , bean )) {
284
354
int startupPhase = getPhase (bean );
285
- phases .computeIfAbsent (startupPhase ,
286
- phase -> new LifecycleGroup (phase , determineTimeout (phase ), lifecycleBeans , autoStartupOnly )
287
- ).add (beanName , bean );
355
+ phases .computeIfAbsent (startupPhase , phase -> new LifecycleGroup (phase , lifecycleBeans , autoStartupOnly ))
356
+ .add (beanName , bean );
288
357
}
289
358
});
290
359
@@ -305,30 +374,41 @@ private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) {
305
374
* @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
306
375
* @param beanName the name of the bean to start
307
376
*/
308
- private void doStart (Map <String , ? extends Lifecycle > lifecycleBeans , String beanName , boolean autoStartupOnly ) {
377
+ private void doStart (Map <String , ? extends Lifecycle > lifecycleBeans , String beanName ,
378
+ boolean autoStartupOnly , @ Nullable List <CompletableFuture <?>> futures ) {
379
+
309
380
Lifecycle bean = lifecycleBeans .remove (beanName );
310
381
if (bean != null && bean != this ) {
311
382
String [] dependenciesForBean = getBeanFactory ().getDependenciesForBean (beanName );
312
383
for (String dependency : dependenciesForBean ) {
313
- doStart (lifecycleBeans , dependency , autoStartupOnly );
384
+ doStart (lifecycleBeans , dependency , autoStartupOnly , futures );
314
385
}
315
386
if (!bean .isRunning () && (!autoStartupOnly || toBeStarted (beanName , bean ))) {
316
- if (logger .isTraceEnabled ()) {
317
- logger .trace ("Starting bean '" + beanName + "' of type [" + bean .getClass ().getName () + "]" );
318
- }
319
- try {
320
- bean .start ();
321
- }
322
- catch (Throwable ex ) {
323
- throw new ApplicationContextException ("Failed to start bean '" + beanName + "'" , ex );
387
+ if (futures != null ) {
388
+ futures .add (CompletableFuture .runAsync (() -> doStart (beanName , bean ), getBootstrapExecutor ()));
324
389
}
325
- if ( logger . isDebugEnabled ()) {
326
- logger . debug ( "Successfully started bean '" + beanName + "'" );
390
+ else {
391
+ doStart ( beanName , bean );
327
392
}
328
393
}
329
394
}
330
395
}
331
396
397
+ private void doStart (String beanName , Lifecycle bean ) {
398
+ if (logger .isTraceEnabled ()) {
399
+ logger .trace ("Starting bean '" + beanName + "' of type [" + bean .getClass ().getName () + "]" );
400
+ }
401
+ try {
402
+ bean .start ();
403
+ }
404
+ catch (Throwable ex ) {
405
+ throw new ApplicationContextException ("Failed to start bean '" + beanName + "'" , ex );
406
+ }
407
+ if (logger .isDebugEnabled ()) {
408
+ logger .debug ("Successfully started bean '" + beanName + "'" );
409
+ }
410
+ }
411
+
332
412
private boolean toBeStarted (String beanName , Lifecycle bean ) {
333
413
Set <String > stoppedBeans = this .stoppedBeans ;
334
414
return (stoppedBeans != null ? stoppedBeans .contains (beanName ) :
@@ -341,9 +421,8 @@ private void stopBeans() {
341
421
342
422
lifecycleBeans .forEach ((beanName , bean ) -> {
343
423
int shutdownPhase = getPhase (bean );
344
- phases .computeIfAbsent (shutdownPhase ,
345
- phase -> new LifecycleGroup (phase , determineTimeout (phase ), lifecycleBeans , false )
346
- ).add (beanName , bean );
424
+ phases .computeIfAbsent (shutdownPhase , phase -> new LifecycleGroup (phase , lifecycleBeans , false ))
425
+ .add (beanName , bean );
347
426
});
348
427
349
428
if (!phases .isEmpty ()) {
@@ -414,7 +493,7 @@ else if (bean instanceof SmartLifecycle) {
414
493
}
415
494
416
495
417
- // overridable hooks
496
+ // Overridable hooks
418
497
419
498
/**
420
499
* Retrieve all applicable Lifecycle beans: all singletons that have already been created,
@@ -470,8 +549,6 @@ private class LifecycleGroup {
470
549
471
550
private final int phase ;
472
551
473
- private final long timeout ;
474
-
475
552
private final Map <String , ? extends Lifecycle > lifecycleBeans ;
476
553
477
554
private final boolean autoStartupOnly ;
@@ -480,11 +557,8 @@ private class LifecycleGroup {
480
557
481
558
private int smartMemberCount ;
482
559
483
- public LifecycleGroup (
484
- int phase , long timeout , Map <String , ? extends Lifecycle > lifecycleBeans , boolean autoStartupOnly ) {
485
-
560
+ public LifecycleGroup (int phase , Map <String , ? extends Lifecycle > lifecycleBeans , boolean autoStartupOnly ) {
486
561
this .phase = phase ;
487
- this .timeout = timeout ;
488
562
this .lifecycleBeans = lifecycleBeans ;
489
563
this .autoStartupOnly = autoStartupOnly ;
490
564
}
@@ -503,8 +577,26 @@ public void start() {
503
577
if (logger .isDebugEnabled ()) {
504
578
logger .debug ("Starting beans in phase " + this .phase );
505
579
}
580
+ Long concurrentStartup = determineConcurrentStartup (this .phase );
581
+ List <CompletableFuture <?>> futures = (concurrentStartup != null ? new ArrayList <>() : null );
506
582
for (LifecycleGroupMember member : this .members ) {
507
- doStart (this .lifecycleBeans , member .name , this .autoStartupOnly );
583
+ doStart (this .lifecycleBeans , member .name , this .autoStartupOnly , futures );
584
+ }
585
+ if (concurrentStartup != null && !CollectionUtils .isEmpty (futures )) {
586
+ try {
587
+ CompletableFuture .allOf (futures .toArray (new CompletableFuture <?>[0 ]))
588
+ .get (concurrentStartup , TimeUnit .MILLISECONDS );
589
+ }
590
+ catch (Exception ex ) {
591
+ if (ex instanceof ExecutionException exEx ) {
592
+ Throwable cause = exEx .getCause ();
593
+ if (cause instanceof ApplicationContextException acEx ) {
594
+ throw acEx ;
595
+ }
596
+ }
597
+ throw new ApplicationContextException ("Failed to start beans in phase " + this .phase +
598
+ " within timeout of " + concurrentStartup + "ms" , ex );
599
+ }
508
600
}
509
601
}
510
602
@@ -528,11 +620,14 @@ else if (member.bean instanceof SmartLifecycle) {
528
620
}
529
621
}
530
622
try {
531
- latch .await (this .timeout , TimeUnit .MILLISECONDS );
532
- if (latch .getCount () > 0 && !countDownBeanNames .isEmpty () && logger .isInfoEnabled ()) {
533
- logger .info ("Shutdown phase " + this .phase + " ends with " + countDownBeanNames .size () +
534
- " bean" + (countDownBeanNames .size () > 1 ? "s" : "" ) +
535
- " still running after timeout of " + this .timeout + "ms: " + countDownBeanNames );
623
+ long shutdownTimeout = determineShutdownTimeout (this .phase );
624
+ if (!latch .await (shutdownTimeout , TimeUnit .MILLISECONDS )) {
625
+ // Count is still >0 after timeout
626
+ if (!countDownBeanNames .isEmpty () && logger .isInfoEnabled ()) {
627
+ logger .info ("Shutdown phase " + this .phase + " ends with " + countDownBeanNames .size () +
628
+ " bean" + (countDownBeanNames .size () > 1 ? "s" : "" ) +
629
+ " still running after timeout of " + shutdownTimeout + "ms: " + countDownBeanNames );
630
+ }
536
631
}
537
632
}
538
633
catch (InterruptedException ex ) {
0 commit comments