• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 16164694424

09 Jul 2025 08:46AM UTC coverage: 81.772% (-0.002%) from 81.774%
16164694424

push

github

TAKETODAY
:sparkles: Introduce ConfigurableApplicationContext.restart() method

59417 of 77611 branches covered (76.56%)

Branch coverage included in aggregate %.

140692 of 167106 relevant lines covered (84.19%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.35
today-context/src/main/java/infra/context/support/DefaultLifecycleProcessor.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.context.support;
19

20
import org.crac.CheckpointException;
21
import org.crac.Core;
22
import org.crac.RestoreException;
23
import org.crac.management.CRaCMXBean;
24

25
import java.util.ArrayList;
26
import java.util.Collections;
27
import java.util.Comparator;
28
import java.util.HashSet;
29
import java.util.LinkedHashMap;
30
import java.util.LinkedHashSet;
31
import java.util.List;
32
import java.util.Map;
33
import java.util.Set;
34
import java.util.TreeMap;
35
import java.util.concurrent.ConcurrentHashMap;
36
import java.util.concurrent.CountDownLatch;
37
import java.util.concurrent.CyclicBarrier;
38
import java.util.concurrent.ExecutionException;
39
import java.util.concurrent.Executor;
40
import java.util.concurrent.TimeUnit;
41

42
import infra.beans.factory.BeanFactory;
43
import infra.beans.factory.BeanFactoryAware;
44
import infra.beans.factory.BeanFactoryUtils;
45
import infra.beans.factory.config.ConfigurableBeanFactory;
46
import infra.context.ApplicationContextException;
47
import infra.context.Lifecycle;
48
import infra.context.LifecycleProcessor;
49
import infra.context.Phased;
50
import infra.context.SmartLifecycle;
51
import infra.core.NativeDetector;
52
import infra.lang.Assert;
53
import infra.lang.Nullable;
54
import infra.lang.TodayStrategies;
55
import infra.logging.Logger;
56
import infra.logging.LoggerFactory;
57
import infra.util.ClassUtils;
58
import infra.util.CollectionUtils;
59
import infra.util.concurrent.Future;
60

61
/**
62
 * Default implementation of the {@link LifecycleProcessor} strategy.
63
 *
64
 * <p>Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in
65
 * groups for specific phases, on startup/shutdown as well as for explicit start/stop
66
 * interactions on a {@link infra.context.ConfigurableApplicationContext}.
67
 *
68
 * <p>Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in
69
 * groups for specific phases, on startup/shutdown as well as for explicit start/stop
70
 * interactions on a {@link infra.context.ConfigurableApplicationContext}.
71
 *
72
 * <p>this also includes support for JVM checkpoint/restore (Project CRaC)
73
 * when the {@code org.crac:crac} dependency on the classpath.
74
 *
75
 * @author Mark Fisher
76
 * @author Juergen Hoeller
77
 * @author Sebastien Deleuze
78
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
79
 * @since 4.0
80
 */
81
public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware {
82

83
  private static final Logger log = LoggerFactory.getLogger(DefaultLifecycleProcessor.class);
3✔
84

85
  /**
86
   * Property name for a common context checkpoint: {@value}.
87
   *
88
   * @see #ON_REFRESH_VALUE
89
   * @see org.crac.Core#checkpointRestore()
90
   */
91
  public static final String CHECKPOINT_PROPERTY_NAME = "infra.context.checkpoint";
92

93
  /**
94
   * Property name for terminating the JVM when the context reaches a specific phase: {@value}.
95
   *
96
   * @see #ON_REFRESH_VALUE
97
   */
98
  public static final String EXIT_PROPERTY_NAME = "infra.context.exit";
99

100
  /**
101
   * Recognized value for the context checkpoint and exit properties: {@value}.
102
   *
103
   * @see #CHECKPOINT_PROPERTY_NAME
104
   * @see #EXIT_PROPERTY_NAME
105
   */
106
  public static final String ON_REFRESH_VALUE = "onRefresh";
107

108
  private static boolean checkpointOnRefresh =
2✔
109
          ON_REFRESH_VALUE.equalsIgnoreCase(TodayStrategies.getProperty(CHECKPOINT_PROPERTY_NAME));
3✔
110

111
  private static final boolean exitOnRefresh =
3✔
112
          ON_REFRESH_VALUE.equalsIgnoreCase(TodayStrategies.getProperty(EXIT_PROPERTY_NAME));
3✔
113

114
  private final ConcurrentHashMap<Integer, Long> concurrentStartupForPhases = new ConcurrentHashMap<>();
5✔
115

116
  private final ConcurrentHashMap<Integer, Long> timeoutsForShutdownPhases = new ConcurrentHashMap<>();
5✔
117

118
  private volatile long timeoutPerShutdownPhase = 10000;
3✔
119

120
  private volatile boolean running;
121

122
  @Nullable
123
  private volatile ConfigurableBeanFactory beanFactory;
124

125
  @Nullable
126
  private volatile Set<String> stoppedBeans;
127

128
  // Just for keeping a strong reference to the registered CRaC Resource, if any
129

130
  @Nullable
131
  private Object cracResource;
132

133
  public DefaultLifecycleProcessor() {
2✔
134
    if (!NativeDetector.inNativeImage() && ClassUtils.isPresent("org.crac.Core", getClass().getClassLoader())) {
8✔
135
      this.cracResource = new CracDelegate().registerResource();
8✔
136
    }
137
    else if (checkpointOnRefresh) {
2!
138
      throw new IllegalStateException(
×
139
              "Checkpoint on refresh requires a CRaC-enabled JVM and 'org.crac:crac' on the classpath");
140
    }
141
  }
1✔
142

143
  /**
144
   * Switch to concurrent startup for each given phase (group of {@link SmartLifecycle}
145
   * beans with the same 'phase' value) with corresponding timeouts.
146
   * <p><b>Note: By default, the startup for every phase will be sequential without
147
   * a timeout. Calling this setter with timeouts for the given phases switches to a
148
   * mode where the beans in these phases will be started concurrently, cancelling
149
   * the startup if the corresponding timeout is not met for any of these phases.</b>
150
   * <p>For an actual concurrent startup, a bootstrap {@code Executor} needs to be
151
   * set for the application context, typically through a "bootstrapExecutor" bean.
152
   *
153
   * @param phasesWithTimeouts a map of phase values (matching
154
   * {@link SmartLifecycle#getPhase()}) and corresponding timeout values
155
   * (in milliseconds)
156
   * @see SmartLifecycle#getPhase()
157
   * @see infra.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor()
158
   * @since 5.0
159
   */
160
  public void setConcurrentStartupForPhases(Map<Integer, Long> phasesWithTimeouts) {
161
    this.concurrentStartupForPhases.putAll(phasesWithTimeouts);
4✔
162
  }
1✔
163

164
  /**
165
   * Switch to concurrent startup for a specific phase (group of {@link SmartLifecycle}
166
   * beans with the same 'phase' value) with a corresponding timeout.
167
   * <p><b>Note: By default, the startup for every phase will be sequential without
168
   * a timeout. Calling this setter with a timeout for the given phase switches to a
169
   * mode where the beans in this phase will be started concurrently, cancelling
170
   * the startup if the corresponding timeout is not met for this phase.</b>
171
   * <p>For an actual concurrent startup, a bootstrap {@code Executor} needs to be
172
   * set for the application context, typically through a "bootstrapExecutor" bean.
173
   *
174
   * @param phase the phase value (matching {@link SmartLifecycle#getPhase()})
175
   * @param timeout the corresponding timeout value (in milliseconds)
176
   * @see SmartLifecycle#getPhase()
177
   * @see infra.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor()
178
   * @since 5.0
179
   */
180
  public void setConcurrentStartupForPhase(int phase, long timeout) {
181
    this.concurrentStartupForPhases.put(phase, timeout);
8✔
182
  }
1✔
183

184
  /**
185
   * Specify the maximum time allotted for the shutdown of each given phase
186
   * (group of {@link SmartLifecycle} beans with the same 'phase' value).
187
   * <p>In case of no specific timeout configured, the default timeout per
188
   * shutdown phase will apply: 10000 milliseconds (10 seconds).
189
   *
190
   * @param phasesWithTimeouts a map of phase values (matching
191
   * {@link SmartLifecycle#getPhase()}) and corresponding timeout values
192
   * (in milliseconds)
193
   * @see SmartLifecycle#getPhase()
194
   * @see #setTimeoutPerShutdownPhase
195
   * @since 5.0
196
   */
197
  public void setTimeoutsForShutdownPhases(Map<Integer, Long> phasesWithTimeouts) {
198
    this.timeoutsForShutdownPhases.putAll(phasesWithTimeouts);
4✔
199
  }
1✔
200

201
  /**
202
   * Specify the maximum time allotted for the shutdown of a specific phase
203
   * (group of {@link SmartLifecycle} beans with the same 'phase' value).
204
   * <p>In case of no specific timeout configured, the default timeout per
205
   * shutdown phase will apply: 10000 milliseconds (10 seconds).
206
   *
207
   * @param phase the phase value (matching {@link SmartLifecycle#getPhase()})
208
   * @param timeout the corresponding timeout value (in milliseconds)
209
   * @see SmartLifecycle#getPhase()
210
   * @see #setTimeoutPerShutdownPhase
211
   * @since 5.0
212
   */
213
  public void setTimeoutForShutdownPhase(int phase, long timeout) {
214
    this.timeoutsForShutdownPhases.put(phase, timeout);
8✔
215
  }
1✔
216

217
  /**
218
   * Specify the maximum time allotted in milliseconds for the shutdown of any
219
   * phase (group of {@link SmartLifecycle} beans with the same 'phase' value).
220
   * <p>The default value is 10000 milliseconds (10 seconds).
221
   *
222
   * @see SmartLifecycle#getPhase()
223
   */
224
  public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) {
225
    this.timeoutPerShutdownPhase = timeoutPerShutdownPhase;
3✔
226
  }
1✔
227

228
  @Override
229
  public void setBeanFactory(BeanFactory beanFactory) {
230
    if (!(beanFactory instanceof ConfigurableBeanFactory cbf)) {
7✔
231
      throw new IllegalArgumentException(
7✔
232
              "DefaultLifecycleProcessor requires a ConfigurableBeanFactory: " + beanFactory);
233
    }
234
    if (!this.concurrentStartupForPhases.isEmpty() && cbf.getBootstrapExecutor() == null) {
7✔
235
      throw new IllegalStateException("'bootstrapExecutor' needs to be configured for concurrent startup");
5✔
236
    }
237
    this.beanFactory = cbf;
3✔
238
  }
1✔
239

240
  private ConfigurableBeanFactory getBeanFactory() {
241
    ConfigurableBeanFactory beanFactory = this.beanFactory;
3✔
242
    Assert.state(beanFactory != null, "No BeanFactory available");
7✔
243
    return beanFactory;
2✔
244
  }
245

246
  private Executor getBootstrapExecutor() {
247
    Executor executor = getBeanFactory().getBootstrapExecutor();
4✔
248
    Assert.state(executor != null, "No 'bootstrapExecutor' available");
6!
249
    return executor;
2✔
250
  }
251

252
  @Nullable
253
  private Long determineConcurrentStartup(int phase) {
254
    return this.concurrentStartupForPhases.get(phase);
7✔
255
  }
256

257
  private long determineShutdownTimeout(int phase) {
258
    Long timeout = this.timeoutsForShutdownPhases.get(phase);
7✔
259
    return timeout != null ? timeout : this.timeoutPerShutdownPhase;
5!
260
  }
261

262
  // Lifecycle implementation
263

264
  /**
265
   * Start all registered beans that implement {@link Lifecycle} and are <i>not</i>
266
   * already running. Any bean that implements {@link SmartLifecycle} will be
267
   * started within its 'phase', and all phases will be ordered from lowest to
268
   * highest value. All beans that do not implement {@link SmartLifecycle} will be
269
   * started in the default phase 0. A bean declared as a dependency of another bean
270
   * will be started before the dependent bean regardless of the declared phase.
271
   */
272
  @Override
273
  public void start() {
274
    this.stoppedBeans = null;
3✔
275
    startBeans(false);
3✔
276
    // If any bean failed to explicitly start, the exception propagates here.
277
    // The caller may choose to subsequently call stop() if appropriate.
278
    this.running = true;
3✔
279
  }
1✔
280

281
  /**
282
   * Stop all registered beans that implement {@link Lifecycle} and <i>are</i>
283
   * currently running. Any bean that implements {@link SmartLifecycle} will be
284
   * stopped within its 'phase', and all phases will be ordered from highest to
285
   * lowest value. All beans that do not implement {@link SmartLifecycle} will be
286
   * stopped in the default phase 0. A bean declared as dependent on another bean
287
   * will be stopped before the dependency bean regardless of the declared phase.
288
   */
289
  @Override
290
  public void stop() {
291
    stopBeans();
2✔
292
    this.running = false;
3✔
293
  }
1✔
294

295
  @Override
296
  public void onRefresh() {
297
    if (checkpointOnRefresh) {
2!
298
      checkpointOnRefresh = false;
×
299
      new CracDelegate().checkpointRestore();
×
300
    }
301
    if (exitOnRefresh) {
2!
302
      Runtime.getRuntime().halt(0);
×
303
    }
304

305
    this.stoppedBeans = null;
3✔
306
    try {
307
      startBeans(true);
3✔
308
    }
309
    catch (ApplicationContextException ex) {
1✔
310
      // Some bean failed to auto-start within context refresh:
311
      // stop already started beans on context refresh failure.
312
      stopBeans();
2✔
313
      throw ex;
2✔
314
    }
1✔
315
    this.running = true;
3✔
316
  }
1✔
317

318
  @Override
319
  public void onClose() {
320
    stopBeans();
2✔
321
    this.running = false;
3✔
322
  }
1✔
323

324
  @Override
325
  public void onRestart() {
326
    this.stoppedBeans = null;
3✔
327
    if (this.running) {
3!
328
      stopBeans();
2✔
329
    }
330
    startBeans(true);
3✔
331
    this.running = true;
3✔
332
  }
1✔
333

334
  @Override
335
  public boolean isRunning() {
336
    return this.running;
3✔
337
  }
338

339
  // Internal helpers
340

341
  void stopForRestart() {
342
    if (this.running) {
3✔
343
      this.stoppedBeans = ConcurrentHashMap.newKeySet();
3✔
344
      stopBeans();
2✔
345
      this.running = false;
3✔
346
    }
347
  }
1✔
348

349
  void restartAfterStop() {
350
    if (this.stoppedBeans != null) {
3!
351
      startBeans(true);
3✔
352
      this.stoppedBeans = null;
3✔
353
      this.running = true;
3✔
354
    }
355
  }
1✔
356

357
  private void startBeans(boolean autoStartupOnly) {
358
    var lifecycleBeans = getLifecycleBeans();
3✔
359
    var phases = new TreeMap<Integer, LifecycleGroup>();
4✔
360

361
    for (var entry : lifecycleBeans.entrySet()) {
11✔
362
      String beanName = entry.getKey();
4✔
363
      Lifecycle bean = entry.getValue();
4✔
364
      if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
7✔
365
        int startupPhase = getPhase(bean);
4✔
366
        phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly))
20✔
367
                .add(beanName, bean);
1✔
368
      }
369
    }
1✔
370

371
    if (!phases.isEmpty()) {
3✔
372
      for (LifecycleGroup group : phases.values()) {
11✔
373
        group.start();
2✔
374
      }
1✔
375
    }
376
  }
1✔
377

378
  private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) {
379
    Set<String> stoppedBeans = this.stoppedBeans;
3✔
380
    return stoppedBeans != null ? stoppedBeans.contains(beanName) :
7✔
381
            (bean instanceof SmartLifecycle sl && sl.isAutoStartup());
12✔
382
  }
383

384
  /**
385
   * Start the specified bean as part of the given set of Lifecycle beans,
386
   * making sure that any beans that it depends on are started first.
387
   *
388
   * @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
389
   * @param beanName the name of the bean to start
390
   */
391
  private void doStart(Map<String, ? extends Lifecycle> lifecycleBeans, String beanName,
392
          boolean autoStartupOnly, @Nullable List<Future<?>> futures) {
393

394
    Lifecycle bean = lifecycleBeans.remove(beanName);
5✔
395
    if (bean != null && bean != this) {
5!
396
      String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName);
5✔
397
      for (String dependency : dependenciesForBean) {
16✔
398
        doStart(lifecycleBeans, dependency, autoStartupOnly, futures);
6✔
399
      }
400

401
      if (!bean.isRunning() && (!autoStartupOnly || toBeStarted(beanName, bean))) {
10✔
402
        if (futures != null) {
2✔
403
          futures.add(Future.run(() -> doStart(beanName, bean), getBootstrapExecutor()));
16✔
404
        }
405
        else {
406
          doStart(beanName, bean);
4✔
407
        }
408
      }
409
    }
410
  }
1✔
411

412
  private void doStart(String beanName, Lifecycle bean) {
413
    if (log.isTraceEnabled()) {
3!
414
      log.trace("Starting bean '{}' of type [{}]", beanName, bean.getClass().getName());
×
415
    }
416
    try {
417
      bean.start();
2✔
418
    }
419
    catch (Throwable ex) {
1✔
420
      throw new ApplicationContextException("Failed to start bean '%s'".formatted(beanName), ex);
13✔
421
    }
1✔
422

423
    log.debug("Successfully started bean '{}'", beanName);
4✔
424
  }
1✔
425

426
  private boolean toBeStarted(String beanName, Lifecycle bean) {
427
    Set<String> stoppedBeans = this.stoppedBeans;
3✔
428
    return (stoppedBeans != null ? stoppedBeans.contains(beanName) :
7✔
429
            (!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup()));
12✔
430
  }
431

432
  private void stopBeans() {
433
    Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
3✔
434
    Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder());
5✔
435

436
    lifecycleBeans.forEach((beanName, bean) -> {
6✔
437
      int shutdownPhase = getPhase(bean);
4✔
438
      phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false))
19✔
439
              .add(beanName, bean);
1✔
440
    });
1✔
441

442
    if (!phases.isEmpty()) {
3✔
443
      phases.values().forEach(LifecycleGroup::stop);
4✔
444
    }
445
  }
1✔
446

447
  /**
448
   * Stop the specified bean as part of the given set of Lifecycle beans,
449
   * making sure that any beans that depends on it are stopped first.
450
   *
451
   * @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
452
   * @param beanName the name of the bean to stop
453
   */
454
  private void doStop(Map<String, ? extends Lifecycle> lifecycleBeans, final String beanName,
455
          final CountDownLatch latch, final Set<String> countDownBeanNames) {
456

457
    Lifecycle bean = lifecycleBeans.remove(beanName);
5✔
458
    if (bean != null) {
2✔
459
      String[] dependentBeans = getBeanFactory().getDependentBeans(beanName);
5✔
460
      for (String dependentBean : dependentBeans) {
16✔
461
        doStop(lifecycleBeans, dependentBean, latch, countDownBeanNames);
6✔
462
      }
463
      try {
464
        if (bean.isRunning()) {
3✔
465
          Set<String> stoppedBeans = this.stoppedBeans;
3✔
466
          if (stoppedBeans != null) {
2✔
467
            stoppedBeans.add(beanName);
4✔
468
          }
469
          if (bean instanceof SmartLifecycle smartLifecycle) {
6✔
470
            if (log.isTraceEnabled()) {
3!
471
              log.trace("Asking bean '{}' of type [{}] to stop", beanName, bean.getClass().getName());
×
472
            }
473
            countDownBeanNames.add(beanName);
4✔
474
            smartLifecycle.stop(() -> {
7✔
475
              latch.countDown();
2✔
476
              countDownBeanNames.remove(beanName);
4✔
477
              if (log.isDebugEnabled()) {
3!
478
                log.debug("Bean '{}' completed its stop procedure", beanName);
×
479
              }
480
            });
1✔
481
          }
482
          else {
483
            if (log.isTraceEnabled()) {
3!
484
              log.trace("Stopping bean '{}' of type [{}]", beanName, bean.getClass().getName());
×
485
            }
486
            bean.stop();
2✔
487
            log.debug("Successfully stopped bean '{}'", beanName);
4✔
488
          }
489
        }
1✔
490
        else if (bean instanceof SmartLifecycle) {
3✔
491
          // Don't wait for beans that aren't running...
492
          latch.countDown();
2✔
493
        }
494
      }
495
      catch (Throwable ex) {
×
496
        log.warn("Failed to stop bean '{}'", beanName, ex);
×
497
        if (bean instanceof SmartLifecycle) {
×
498
          latch.countDown();
×
499
        }
500
      }
1✔
501
    }
502
  }
1✔
503

504
  // Overridable hooks
505

506
  /**
507
   * Retrieve all applicable Lifecycle beans: all singletons that have already been created,
508
   * as well as all SmartLifecycle beans (even if they are marked as lazy-init).
509
   *
510
   * @return the Map of applicable beans, with bean names as keys and bean instances as values
511
   */
512
  protected Map<String, Lifecycle> getLifecycleBeans() {
513
    ConfigurableBeanFactory beanFactory = getBeanFactory();
3✔
514
    LinkedHashMap<String, Lifecycle> beans = new LinkedHashMap<>();
4✔
515
    var beanNames = beanFactory.getBeanNamesForType(Lifecycle.class, false, false);
6✔
516
    for (String beanName : beanNames) {
10✔
517
      String beanNameToRegister = BeanFactoryUtils.transformedBeanName(beanName);
3✔
518
      boolean isFactoryBean = beanFactory.isFactoryBean(beanNameToRegister);
4✔
519
      String beanNameToCheck = (isFactoryBean ? BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
7✔
520
      if ((beanFactory.containsSingleton(beanNameToRegister)
10✔
521
              && (!isFactoryBean || matchesBeanType(Lifecycle.class, beanNameToCheck, beanFactory)))
6✔
522
              || matchesBeanType(SmartLifecycle.class, beanNameToCheck, beanFactory)) {
2✔
523
        Object bean = beanFactory.getBean(beanNameToCheck);
4✔
524
        if (bean != this && bean instanceof Lifecycle lifecycle) {
9!
525
          beans.put(beanNameToRegister, lifecycle);
5✔
526
        }
527
      }
528
    }
1✔
529
    return beans;
2✔
530
  }
531

532
  private boolean matchesBeanType(Class<?> targetType, String beanName, BeanFactory beanFactory) {
533
    Class<?> beanType = beanFactory.getType(beanName);
4✔
534
    return beanType != null && targetType.isAssignableFrom(beanType);
10!
535
  }
536

537
  /**
538
   * Determine the lifecycle phase of the given bean.
539
   * <p>The default implementation checks for the {@link Phased} interface, using
540
   * a default of 0 otherwise. Can be overridden to apply other/further policies.
541
   *
542
   * @param bean the bean to introspect
543
   * @return the phase (an integer value)
544
   * @see Phased#getPhase()
545
   * @see SmartLifecycle
546
   */
547
  protected int getPhase(Lifecycle bean) {
548
    return bean instanceof Phased phased ? phased.getPhase() : 0;
11✔
549
  }
550

551
  /**
552
   * Helper class for maintaining a group of Lifecycle beans that should be started
553
   * and stopped together based on their 'phase' value (or the default value of 0).
554
   * The group is expected to be created in an ad-hoc fashion and group members are
555
   * expected to always have the same 'phase' value.
556
   */
557
  private class LifecycleGroup {
558

559
    private final int phase;
560

561
    private final Map<String, ? extends Lifecycle> lifecycleBeans;
562

563
    private final boolean autoStartupOnly;
564

565
    private final ArrayList<LifecycleGroupMember> members = new ArrayList<>();
5✔
566

567
    private int smartMemberCount;
568

569
    public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans, boolean autoStartupOnly) {
5✔
570
      this.phase = phase;
3✔
571
      this.lifecycleBeans = lifecycleBeans;
3✔
572
      this.autoStartupOnly = autoStartupOnly;
3✔
573
    }
1✔
574

575
    public void add(String name, Lifecycle bean) {
576
      this.members.add(new LifecycleGroupMember(name, bean));
9✔
577
      if (bean instanceof SmartLifecycle) {
3✔
578
        this.smartMemberCount++;
6✔
579
      }
580
    }
1✔
581

582
    public void start() {
583
      if (members.isEmpty()) {
4!
584
        return;
×
585
      }
586
      log.debug("Starting beans in phase {}", phase);
6✔
587

588
      Long concurrentStartup = determineConcurrentStartup(phase);
6✔
589
      List<Future<?>> futures = concurrentStartup != null ? new ArrayList<>() : null;
8✔
590
      for (LifecycleGroupMember member : members) {
11✔
591
        doStart(lifecycleBeans, member.name, autoStartupOnly, futures);
10✔
592
      }
1✔
593
      if (concurrentStartup != null && CollectionUtils.isNotEmpty(futures)) {
5!
594
        try {
595
          Future.combine(futures).asVoid().get(concurrentStartup, TimeUnit.MILLISECONDS);
8✔
596
        }
597
        catch (Exception ex) {
×
598
          if (ex instanceof ExecutionException exEx) {
×
599
            Throwable cause = exEx.getCause();
×
600
            if (cause instanceof ApplicationContextException acEx) {
×
601
              throw acEx;
×
602
            }
603
          }
604
          throw new ApplicationContextException("Failed to start beans in phase %d within timeout of %dms"
×
605
                  .formatted(this.phase, concurrentStartup), ex);
×
606
        }
1✔
607
      }
608
    }
1✔
609

610
    public void stop() {
611
      if (members.isEmpty()) {
4!
612
        return;
×
613
      }
614
      log.debug("Stopping beans in phase {}", phase);
6✔
615

616
      CountDownLatch latch = new CountDownLatch(smartMemberCount);
6✔
617
      Set<String> countDownBeanNames = Collections.synchronizedSet(new LinkedHashSet<>());
5✔
618
      HashSet<String> lifecycleBeanNames = new HashSet<>(lifecycleBeans.keySet());
7✔
619
      for (LifecycleGroupMember member : members) {
11✔
620
        if (lifecycleBeanNames.contains(member.name)) {
5✔
621
          doStop(lifecycleBeans, member.name, latch, countDownBeanNames);
10✔
622
        }
623
        else if (member.bean instanceof SmartLifecycle) {
4✔
624
          // Already removed: must have been a dependent bean from another phase
625
          latch.countDown();
2✔
626
        }
627
      }
1✔
628
      try {
629
        long shutdownTimeout = determineShutdownTimeout(this.phase);
6✔
630
        if (!latch.await(shutdownTimeout, TimeUnit.MILLISECONDS)) {
5!
631
          // Count is still >0 after timeout
632
          if (!countDownBeanNames.isEmpty() && log.isInfoEnabled()) {
×
633
            log.info("Shutdown phase {} ends with {} bean%s still running after timeout of {}ms: {}",
×
634
                    this.phase, countDownBeanNames.size(), countDownBeanNames.size() > 1 ? "s" : "", shutdownTimeout, countDownBeanNames);
×
635
          }
636
        }
637
      }
638
      catch (InterruptedException ex) {
×
639
        Thread.currentThread().interrupt();
×
640
      }
1✔
641
    }
1✔
642
  }
643

644
  /**
645
   * A simple record of a LifecycleGroup member.
646
   */
647
  private record LifecycleGroupMember(String name, Lifecycle bean) {
9✔
648
  }
649

650
  /**
651
   * Inner class to avoid a hard dependency on Project CRaC at runtime.
652
   *
653
   * @see org.crac.Core
654
   */
655
  private class CracDelegate {
6✔
656

657
    public Object registerResource() {
658
      log.debug("Registering JVM checkpoint/restore callback for Infra-managed lifecycle beans");
3✔
659
      CracResourceAdapter resourceAdapter = new CracResourceAdapter();
6✔
660
      org.crac.Core.getGlobalContext().register(resourceAdapter);
3✔
661
      return resourceAdapter;
2✔
662
    }
663

664
    public void checkpointRestore() {
665
      log.info("Triggering JVM checkpoint/restore");
×
666
      try {
667
        Core.checkpointRestore();
×
668
      }
669
      catch (UnsupportedOperationException ex) {
×
670
        throw new ApplicationContextException("CRaC checkpoint not supported on current JVM", ex);
×
671
      }
672
      catch (CheckpointException ex) {
×
673
        throw new ApplicationContextException("Failed to take CRaC checkpoint on refresh", ex);
×
674
      }
675
      catch (RestoreException ex) {
×
676
        throw new ApplicationContextException("Failed to restore CRaC checkpoint on refresh", ex);
×
677
      }
×
678
    }
×
679
  }
680

681
  /**
682
   * Resource adapter for Project CRaC, triggering a stop-and-restart cycle
683
   * for Infra-managed lifecycle beans around a JVM checkpoint/restore.
684
   *
685
   * @see #stopForRestart()
686
   * @see #restartAfterStop()
687
   */
688
  private class CracResourceAdapter implements org.crac.Resource {
6✔
689

690
    @Nullable
691
    private CyclicBarrier barrier;
692

693
    @Override
694
    public void beforeCheckpoint(org.crac.Context<? extends org.crac.Resource> context) {
695
      // A non-daemon thread for preventing an accidental JVM shutdown before the checkpoint
696
      this.barrier = new CyclicBarrier(2);
×
697

698
      Thread thread = new Thread(() -> {
×
699
        awaitPreventShutdownBarrier();
×
700
        // Checkpoint happens here
701
        awaitPreventShutdownBarrier();
×
702
      }, "prevent-shutdown");
×
703

704
      thread.setDaemon(false);
×
705
      thread.start();
×
706
      awaitPreventShutdownBarrier();
×
707

708
      log.debug("Stopping Infra-managed lifecycle beans before JVM checkpoint");
×
709
      stopForRestart();
×
710
    }
×
711

712
    @Override
713
    public void afterRestore(org.crac.Context<? extends org.crac.Resource> context) {
714
      log.info("Restarting Infra-managed lifecycle beans after JVM restore");
×
715
      restartAfterStop();
×
716

717
      // Barrier for prevent-shutdown thread not needed anymore
718
      this.barrier = null;
×
719

720
      if (!checkpointOnRefresh) {
×
721
        log.info("Infra-managed lifecycle restart completed (restored JVM running for {} ms)",
×
722
                CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore());
×
723
      }
724
    }
×
725

726
    private void awaitPreventShutdownBarrier() {
727
      try {
728
        if (this.barrier != null) {
×
729
          this.barrier.await();
×
730
        }
731
      }
732
      catch (Exception ex) {
×
733
        log.trace("Exception from prevent-shutdown barrier", ex);
×
734
      }
×
735
    }
×
736
  }
737

738
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc