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 .lang .Nullable ;
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 ;
@@ -130,20 +147,59 @@ else if (checkpointOnRefresh) {
130
147
}
131
148
132
149
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
+
133
189
/**
134
190
* Specify the maximum time allotted for the shutdown of each given phase
135
191
* (group of {@link SmartLifecycle} beans with the same 'phase' value).
136
192
* <p>In case of no specific timeout configured, the default timeout per
137
193
* 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
139
195
* {@link SmartLifecycle#getPhase()}) and corresponding timeout values
140
196
* (in milliseconds)
141
197
* @since 6.2
142
198
* @see SmartLifecycle#getPhase()
143
199
* @see #setTimeoutPerShutdownPhase
144
200
*/
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 );
147
203
}
148
204
149
205
/**
@@ -171,17 +227,15 @@ public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) {
171
227
this .timeoutPerShutdownPhase = timeoutPerShutdownPhase ;
172
228
}
173
229
174
- private long determineTimeout (int phase ) {
175
- Long timeout = this .timeoutsForShutdownPhases .get (phase );
176
- return (timeout != null ? timeout : this .timeoutPerShutdownPhase );
177
- }
178
-
179
230
@ Override
180
231
public void setBeanFactory (BeanFactory beanFactory ) {
181
232
if (!(beanFactory instanceof ConfigurableListableBeanFactory clbf )) {
182
233
throw new IllegalArgumentException (
183
234
"DefaultLifecycleProcessor requires a ConfigurableListableBeanFactory: " + beanFactory );
184
235
}
236
+ if (!this .concurrentStartupForPhases .isEmpty () && clbf .getBootstrapExecutor () == null ) {
237
+ throw new IllegalStateException ("'bootstrapExecutor' needs to be configured for concurrent startup" );
238
+ }
185
239
this .beanFactory = clbf ;
186
240
}
187
241
@@ -191,6 +245,22 @@ private ConfigurableListableBeanFactory getBeanFactory() {
191
245
return beanFactory ;
192
246
}
193
247
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
+
194
264
195
265
// Lifecycle implementation
196
266
@@ -285,9 +355,8 @@ private void startBeans(boolean autoStartupOnly) {
285
355
lifecycleBeans .forEach ((beanName , bean ) -> {
286
356
if (!autoStartupOnly || isAutoStartupCandidate (beanName , bean )) {
287
357
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 );
291
360
}
292
361
});
293
362
@@ -308,30 +377,41 @@ private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) {
308
377
* @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
309
378
* @param beanName the name of the bean to start
310
379
*/
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
+
312
383
Lifecycle bean = lifecycleBeans .remove (beanName );
313
384
if (bean != null && bean != this ) {
314
385
String [] dependenciesForBean = getBeanFactory ().getDependenciesForBean (beanName );
315
386
for (String dependency : dependenciesForBean ) {
316
- doStart (lifecycleBeans , dependency , autoStartupOnly );
387
+ doStart (lifecycleBeans , dependency , autoStartupOnly , futures );
317
388
}
318
389
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 ()) );
321
392
}
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 );
330
395
}
331
396
}
332
397
}
333
398
}
334
399
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
+
335
415
private boolean toBeStarted (String beanName , Lifecycle bean ) {
336
416
Set <String > stoppedBeans = this .stoppedBeans ;
337
417
return (stoppedBeans != null ? stoppedBeans .contains (beanName ) :
@@ -344,9 +424,8 @@ private void stopBeans() {
344
424
345
425
lifecycleBeans .forEach ((beanName , bean ) -> {
346
426
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 );
350
429
});
351
430
352
431
if (!phases .isEmpty ()) {
@@ -417,7 +496,7 @@ else if (bean instanceof SmartLifecycle) {
417
496
}
418
497
419
498
420
- // overridable hooks
499
+ // Overridable hooks
421
500
422
501
/**
423
502
* Retrieve all applicable Lifecycle beans: all singletons that have already been created,
@@ -473,8 +552,6 @@ private class LifecycleGroup {
473
552
474
553
private final int phase ;
475
554
476
- private final long timeout ;
477
-
478
555
private final Map <String , ? extends Lifecycle > lifecycleBeans ;
479
556
480
557
private final boolean autoStartupOnly ;
@@ -483,11 +560,8 @@ private class LifecycleGroup {
483
560
484
561
private int smartMemberCount ;
485
562
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 ) {
489
564
this .phase = phase ;
490
- this .timeout = timeout ;
491
565
this .lifecycleBeans = lifecycleBeans ;
492
566
this .autoStartupOnly = autoStartupOnly ;
493
567
}
@@ -506,8 +580,26 @@ public void start() {
506
580
if (logger .isDebugEnabled ()) {
507
581
logger .debug ("Starting beans in phase " + this .phase );
508
582
}
583
+ Long concurrentStartup = determineConcurrentStartup (this .phase );
584
+ List <CompletableFuture <?>> futures = (concurrentStartup != null ? new ArrayList <>() : null );
509
585
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
+ }
511
603
}
512
604
}
513
605
@@ -531,11 +623,14 @@ else if (member.bean instanceof SmartLifecycle) {
531
623
}
532
624
}
533
625
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
+ }
539
634
}
540
635
}
541
636
catch (InterruptedException ex ) {
0 commit comments