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

TAKETODAY / today-infrastructure / 17175530323

23 Aug 2025 12:36PM UTC coverage: 81.854% (+0.009%) from 81.845%
17175530323

push

github

TAKETODAY
:art:

59686 of 77901 branches covered (76.62%)

Branch coverage included in aggregate %.

141140 of 167446 relevant lines covered (84.29%)

3.6 hits per line

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

74.83
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);
×
182
  }
×
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);
×
199
  }
×
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);
×
215
  }
×
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(
×
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");
×
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");
6!
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(false);
3✔
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(false);
3✔
313
      throw ex;
2✔
314
    }
1✔
315
    this.running = true;
3✔
316
  }
1✔
317

318
  @Override
319
  public void onClose() {
320
    stopBeans(false);
3✔
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(true);
3✔
329
    }
330
    startBeans(true);
3✔
331
    this.running = true;
3✔
332
  }
1✔
333

334
  @Override
335
  public void onPause() {
336
    if (this.running) {
3!
337
      stopBeans(true);
3✔
338
      this.running = false;
3✔
339
    }
340
  }
1✔
341

342
  @Override
343
  public boolean isRunning() {
344
    return this.running;
3✔
345
  }
346

347
  // Internal helpers
348

349
  void stopForRestart() {
350
    if (this.running) {
3!
351
      this.stoppedBeans = ConcurrentHashMap.newKeySet();
3✔
352
      stopBeans(false);
3✔
353
      this.running = false;
3✔
354
    }
355
  }
1✔
356

357
  void restartAfterStop() {
358
    if (this.stoppedBeans != null) {
3!
359
      startBeans(true);
3✔
360
      this.stoppedBeans = null;
3✔
361
      this.running = true;
3✔
362
    }
363
  }
1✔
364

365
  private void startBeans(boolean autoStartupOnly) {
366
    var lifecycleBeans = getLifecycleBeans();
3✔
367
    var phases = new TreeMap<Integer, LifecycleGroup>();
4✔
368

369
    for (var entry : lifecycleBeans.entrySet()) {
11✔
370
      String beanName = entry.getKey();
4✔
371
      Lifecycle bean = entry.getValue();
4✔
372
      if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
7✔
373
        int startupPhase = getPhase(bean);
4✔
374
        phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly, false))
21✔
375
                .add(beanName, bean);
1✔
376
      }
377
    }
1✔
378

379
    if (!phases.isEmpty()) {
3✔
380
      for (LifecycleGroup group : phases.values()) {
11✔
381
        group.start();
2✔
382
      }
1✔
383
    }
384
  }
1✔
385

386
  private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) {
387
    Set<String> stoppedBeans = this.stoppedBeans;
3✔
388
    return stoppedBeans != null ? stoppedBeans.contains(beanName) :
7✔
389
            (bean instanceof SmartLifecycle sl && sl.isAutoStartup());
12✔
390
  }
391

392
  /**
393
   * Start the specified bean as part of the given set of Lifecycle beans,
394
   * making sure that any beans that it depends on are started first.
395
   *
396
   * @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
397
   * @param beanName the name of the bean to start
398
   */
399
  private void doStart(Map<String, ? extends Lifecycle> lifecycleBeans, String beanName,
400
          boolean autoStartupOnly, @Nullable List<Future<?>> futures) {
401

402
    Lifecycle bean = lifecycleBeans.remove(beanName);
5✔
403
    if (bean != null && bean != this) {
5!
404
      String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName);
5✔
405
      for (String dependency : dependenciesForBean) {
16✔
406
        doStart(lifecycleBeans, dependency, autoStartupOnly, futures);
6✔
407
      }
408

409
      if (!bean.isRunning() && (!autoStartupOnly || toBeStarted(beanName, bean))) {
10✔
410
        if (futures != null) {
2✔
411
          futures.add(Future.run(() -> doStart(beanName, bean), getBootstrapExecutor()));
16✔
412
        }
413
        else {
414
          doStart(beanName, bean);
4✔
415
        }
416
      }
417
    }
418
  }
1✔
419

420
  private void doStart(String beanName, Lifecycle bean) {
421
    if (log.isTraceEnabled()) {
3!
422
      log.trace("Starting bean '{}' of type [{}]", beanName, bean.getClass().getName());
×
423
    }
424
    try {
425
      bean.start();
2✔
426
    }
427
    catch (Throwable ex) {
1✔
428
      throw new ApplicationContextException("Failed to start bean '%s'".formatted(beanName), ex);
13✔
429
    }
1✔
430

431
    log.debug("Successfully started bean '{}'", beanName);
4✔
432
  }
1✔
433

434
  private boolean toBeStarted(String beanName, Lifecycle bean) {
435
    Set<String> stoppedBeans = this.stoppedBeans;
3✔
436
    return (stoppedBeans != null ? stoppedBeans.contains(beanName) :
7✔
437
            (!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup()));
12✔
438
  }
439

440
  private void stopBeans(boolean pausableOnly) {
441
    Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
3✔
442
    Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder());
5✔
443

444
    lifecycleBeans.forEach((beanName, bean) -> {
7✔
445
      int shutdownPhase = getPhase(bean);
4✔
446
      phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false, pausableOnly))
21✔
447
              .add(beanName, bean);
1✔
448
    });
1✔
449

450
    if (!phases.isEmpty()) {
3✔
451
      phases.values().forEach(LifecycleGroup::stop);
4✔
452
    }
453
  }
1✔
454

455
  /**
456
   * Stop the specified bean as part of the given set of Lifecycle beans,
457
   * making sure that any beans that depends on it are stopped first.
458
   *
459
   * @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
460
   * @param beanName the name of the bean to stop
461
   */
462
  private void doStop(Map<String, ? extends Lifecycle> lifecycleBeans, final String beanName,
463
          final CountDownLatch latch, final Set<String> countDownBeanNames, boolean pausableOnly) {
464

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

520
  // Overridable hooks
521

522
  /**
523
   * Retrieve all applicable Lifecycle beans: all singletons that have already been created,
524
   * as well as all SmartLifecycle beans (even if they are marked as lazy-init).
525
   *
526
   * @return the Map of applicable beans, with bean names as keys and bean instances as values
527
   */
528
  protected Map<String, Lifecycle> getLifecycleBeans() {
529
    ConfigurableBeanFactory beanFactory = getBeanFactory();
3✔
530
    LinkedHashMap<String, Lifecycle> beans = new LinkedHashMap<>();
4✔
531
    var beanNames = beanFactory.getBeanNamesForType(Lifecycle.class, false, false);
6✔
532
    for (String beanName : beanNames) {
16✔
533
      String beanNameToRegister = BeanFactoryUtils.transformedBeanName(beanName);
3✔
534
      boolean isFactoryBean = beanFactory.isFactoryBean(beanNameToRegister);
4✔
535
      String beanNameToCheck = (isFactoryBean ? BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
7✔
536
      if ((beanFactory.containsSingleton(beanNameToRegister)
10✔
537
              && (!isFactoryBean || matchesBeanType(Lifecycle.class, beanNameToCheck, beanFactory)))
6✔
538
              || matchesBeanType(SmartLifecycle.class, beanNameToCheck, beanFactory)) {
2✔
539
        Object bean = beanFactory.getBean(beanNameToCheck);
4✔
540
        if (bean != this && bean instanceof Lifecycle lifecycle) {
9!
541
          beans.put(beanNameToRegister, lifecycle);
5✔
542
        }
543
      }
544
    }
545
    return beans;
2✔
546
  }
547

548
  private boolean matchesBeanType(Class<?> targetType, String beanName, BeanFactory beanFactory) {
549
    Class<?> beanType = beanFactory.getType(beanName);
4✔
550
    return beanType != null && targetType.isAssignableFrom(beanType);
10!
551
  }
552

553
  /**
554
   * Determine the lifecycle phase of the given bean.
555
   * <p>The default implementation checks for the {@link Phased} interface, using
556
   * a default of 0 otherwise. Can be overridden to apply other/further policies.
557
   *
558
   * @param bean the bean to introspect
559
   * @return the phase (an integer value)
560
   * @see Phased#getPhase()
561
   * @see SmartLifecycle
562
   */
563
  protected int getPhase(Lifecycle bean) {
564
    return bean instanceof Phased phased ? phased.getPhase() : 0;
11✔
565
  }
566

567
  /**
568
   * Helper class for maintaining a group of Lifecycle beans that should be started
569
   * and stopped together based on their 'phase' value (or the default value of 0).
570
   * The group is expected to be created in an ad-hoc fashion and group members are
571
   * expected to always have the same 'phase' value.
572
   */
573
  private class LifecycleGroup {
574

575
    private final int phase;
576

577
    private final Map<String, ? extends Lifecycle> lifecycleBeans;
578

579
    private final boolean autoStartupOnly;
580

581
    private final boolean pausableOnly;
582

583
    private final ArrayList<LifecycleGroupMember> members = new ArrayList<>();
5✔
584

585
    private int smartMemberCount;
586

587
    public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans,
588
            boolean autoStartupOnly, boolean pausableOnly) {
5✔
589
      this.phase = phase;
3✔
590
      this.lifecycleBeans = lifecycleBeans;
3✔
591
      this.autoStartupOnly = autoStartupOnly;
3✔
592
      this.pausableOnly = pausableOnly;
3✔
593
    }
1✔
594

595
    public void add(String name, Lifecycle bean) {
596
      this.members.add(new LifecycleGroupMember(name, bean));
9✔
597
      if (bean instanceof SmartLifecycle) {
3✔
598
        this.smartMemberCount++;
6✔
599
      }
600
    }
1✔
601

602
    public void start() {
603
      if (members.isEmpty()) {
4!
604
        return;
×
605
      }
606
      log.debug("Starting beans in phase {}", phase);
6✔
607

608
      Long concurrentStartup = determineConcurrentStartup(phase);
6✔
609
      List<Future<?>> futures = concurrentStartup != null ? new ArrayList<>() : null;
8✔
610
      for (LifecycleGroupMember member : members) {
11✔
611
        doStart(lifecycleBeans, member.name, autoStartupOnly, futures);
10✔
612
      }
1✔
613
      if (concurrentStartup != null && CollectionUtils.isNotEmpty(futures)) {
5!
614
        try {
615
          Future.combine(futures).asVoid().get(concurrentStartup, TimeUnit.MILLISECONDS);
8✔
616
        }
617
        catch (Exception ex) {
×
618
          if (ex instanceof ExecutionException exEx) {
×
619
            Throwable cause = exEx.getCause();
×
620
            if (cause instanceof ApplicationContextException acEx) {
×
621
              throw acEx;
×
622
            }
623
          }
624
          throw new ApplicationContextException("Failed to start beans in phase %d within timeout of %dms"
×
625
                  .formatted(this.phase, concurrentStartup), ex);
×
626
        }
1✔
627
      }
628
    }
1✔
629

630
    public void stop() {
631
      if (members.isEmpty()) {
4!
632
        return;
×
633
      }
634
      log.debug("Stopping beans in phase {}", phase);
6✔
635

636
      CountDownLatch latch = new CountDownLatch(smartMemberCount);
6✔
637
      Set<String> countDownBeanNames = Collections.synchronizedSet(new LinkedHashSet<>());
5✔
638
      HashSet<String> lifecycleBeanNames = new HashSet<>(lifecycleBeans.keySet());
7✔
639
      for (LifecycleGroupMember member : members) {
11✔
640
        if (lifecycleBeanNames.contains(member.name)) {
5✔
641
          doStop(lifecycleBeans, member.name, latch, countDownBeanNames, pausableOnly);
12✔
642
        }
643
        else if (member.bean instanceof SmartLifecycle) {
4✔
644
          // Already removed: must have been a dependent bean from another phase
645
          latch.countDown();
2✔
646
        }
647
      }
1✔
648
      try {
649
        long shutdownTimeout = determineShutdownTimeout(this.phase);
6✔
650
        if (!latch.await(shutdownTimeout, TimeUnit.MILLISECONDS)) {
5!
651
          // Count is still >0 after timeout
652
          if (!countDownBeanNames.isEmpty() && log.isInfoEnabled()) {
×
653
            log.info("Shutdown phase {} ends with {} bean%s still running after timeout of {}ms: {}",
×
654
                    this.phase, countDownBeanNames.size(), countDownBeanNames.size() > 1 ? "s" : "", shutdownTimeout, countDownBeanNames);
×
655
          }
656
        }
657
      }
658
      catch (InterruptedException ex) {
×
659
        Thread.currentThread().interrupt();
×
660
      }
1✔
661
    }
1✔
662
  }
663

664
  /**
665
   * A simple record of a LifecycleGroup member.
666
   */
667
  private record LifecycleGroupMember(String name, Lifecycle bean) {
9✔
668
  }
669

670
  /**
671
   * Inner class to avoid a hard dependency on Project CRaC at runtime.
672
   *
673
   * @see org.crac.Core
674
   */
675
  private class CracDelegate {
6✔
676

677
    public Object registerResource() {
678
      log.debug("Registering JVM checkpoint/restore callback for Infra-managed lifecycle beans");
3✔
679
      CracResourceAdapter resourceAdapter = new CracResourceAdapter();
6✔
680
      org.crac.Core.getGlobalContext().register(resourceAdapter);
3✔
681
      return resourceAdapter;
2✔
682
    }
683

684
    public void checkpointRestore() {
685
      log.info("Triggering JVM checkpoint/restore");
×
686
      try {
687
        Core.checkpointRestore();
×
688
      }
689
      catch (UnsupportedOperationException ex) {
×
690
        throw new ApplicationContextException("CRaC checkpoint not supported on current JVM", ex);
×
691
      }
692
      catch (CheckpointException ex) {
×
693
        throw new ApplicationContextException("Failed to take CRaC checkpoint on refresh", ex);
×
694
      }
695
      catch (RestoreException ex) {
×
696
        throw new ApplicationContextException("Failed to restore CRaC checkpoint on refresh", ex);
×
697
      }
×
698
    }
×
699
  }
700

701
  /**
702
   * Resource adapter for Project CRaC, triggering a stop-and-restart cycle
703
   * for Infra-managed lifecycle beans around a JVM checkpoint/restore.
704
   *
705
   * @see #stopForRestart()
706
   * @see #restartAfterStop()
707
   */
708
  private class CracResourceAdapter implements org.crac.Resource {
6✔
709

710
    @Nullable
711
    private CyclicBarrier barrier;
712

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

718
      Thread thread = new Thread(() -> {
×
719
        awaitPreventShutdownBarrier();
×
720
        // Checkpoint happens here
721
        awaitPreventShutdownBarrier();
×
722
      }, "prevent-shutdown");
×
723

724
      thread.setDaemon(false);
×
725
      thread.start();
×
726
      awaitPreventShutdownBarrier();
×
727

728
      log.debug("Stopping Infra-managed lifecycle beans before JVM checkpoint");
×
729
      stopForRestart();
×
730
    }
×
731

732
    @Override
733
    public void afterRestore(org.crac.Context<? extends org.crac.Resource> context) {
734
      log.info("Restarting Infra-managed lifecycle beans after JVM restore");
×
735
      restartAfterStop();
×
736

737
      // Barrier for prevent-shutdown thread not needed anymore
738
      this.barrier = null;
×
739

740
      if (!checkpointOnRefresh) {
×
741
        log.info("Infra-managed lifecycle restart completed (restored JVM running for {} ms)",
×
742
                CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore());
×
743
      }
744
    }
×
745

746
    private void awaitPreventShutdownBarrier() {
747
      try {
748
        if (this.barrier != null) {
×
749
          this.barrier.await();
×
750
        }
751
      }
752
      catch (Exception ex) {
×
753
        log.trace("Exception from prevent-shutdown barrier", ex);
×
754
      }
×
755
    }
×
756
  }
757

758
}
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