Skip to content

Commit 5e2c16c

Browse files
committed
Merge branch '6.2.x'
# Conflicts: # spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java
2 parents 7bf628c + 48009c8 commit 5e2c16c

File tree

4 files changed

+205
-65
lines changed

4 files changed

+205
-65
lines changed

Diff for: spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525
* analogous to an InvocationTargetException.
2626
*
2727
* @author Rod Johnson
28+
* @author Juergen Hoeller
2829
*/
2930
@SuppressWarnings("serial")
3031
public class MethodInvocationException extends PropertyAccessException {
@@ -41,7 +42,9 @@ public class MethodInvocationException extends PropertyAccessException {
4142
* @param cause the Throwable raised by the invoked method
4243
*/
4344
public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, @Nullable Throwable cause) {
44-
super(propertyChangeEvent, "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception", cause);
45+
super(propertyChangeEvent,
46+
"Property '" + propertyChangeEvent.getPropertyName() + "' threw exception: " + cause,
47+
cause);
4548
}
4649

4750
@Override

Diff for: spring-beans/src/main/java/org/springframework/beans/factory/support/CglibSubclassingInstantiationStrategy.java

-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,6 @@ public ReplaceOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanF
280280
public @Nullable Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
281281
ReplaceOverride ro = (ReplaceOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
282282
Assert.state(ro != null, "ReplaceOverride not found");
283-
// TODO could cache if a singleton for minor performance optimization
284283
MethodReplacer mr = this.owner.getBean(ro.getMethodReplacerBeanName(), MethodReplacer.class);
285284
return processReturnType(method, mr.reimplement(obj, method, args));
286285
}

Diff for: spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java

+136-41
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,9 +26,12 @@
2626
import java.util.Map;
2727
import java.util.Set;
2828
import java.util.TreeMap;
29+
import java.util.concurrent.CompletableFuture;
2930
import java.util.concurrent.ConcurrentHashMap;
3031
import java.util.concurrent.CountDownLatch;
3132
import java.util.concurrent.CyclicBarrier;
33+
import java.util.concurrent.ExecutionException;
34+
import java.util.concurrent.Executor;
3235
import java.util.concurrent.TimeUnit;
3336

3437
import org.apache.commons.logging.Log;
@@ -52,6 +55,7 @@
5255
import org.springframework.core.SpringProperties;
5356
import org.springframework.util.Assert;
5457
import org.springframework.util.ClassUtils;
58+
import org.springframework.util.CollectionUtils;
5559

5660
/**
5761
* Spring's default implementation of the {@link LifecycleProcessor} strategy.
@@ -61,12 +65,23 @@
6165
* interactions on a {@link org.springframework.context.ConfigurableApplicationContext}.
6266
*
6367
* <p>As of 6.1, this also includes support for JVM checkpoint/restore (Project CRaC)
64-
* when the {@code org.crac:crac} dependency on the classpath.
68+
* when the {@code org.crac:crac} dependency is on the classpath. All running beans
69+
* will get stopped and restarted according to the CRaC checkpoint/restore callbacks.
70+
*
71+
* <p>As of 6.2, this processor can be configured with custom timeouts for specific
72+
* shutdown phases, applied to {@link SmartLifecycle#stop(Runnable)} implementations.
73+
* As of 6.2.6, there is also support for the concurrent startup of specific phases
74+
* with individual timeouts, triggering the {@link SmartLifecycle#start()} callbacks
75+
* of all associated beans asynchronously and then waiting for all of them to return,
76+
* as an alternative to the default sequential startup of beans without a timeout.
6577
*
6678
* @author Mark Fisher
6779
* @author Juergen Hoeller
6880
* @author Sebastien Deleuze
6981
* @since 3.0
82+
* @see SmartLifecycle#getPhase()
83+
* @see #setConcurrentStartupForPhase
84+
* @see #setTimeoutForShutdownPhase
7085
*/
7186
public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware {
7287

@@ -102,6 +117,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
102117

103118
private final Log logger = LogFactory.getLog(getClass());
104119

120+
private final Map<Integer, Long> concurrentStartupForPhases = new ConcurrentHashMap<>();
121+
105122
private final Map<Integer, Long> timeoutsForShutdownPhases = new ConcurrentHashMap<>();
106123

107124
private volatile long timeoutPerShutdownPhase = 10000;
@@ -127,20 +144,59 @@ else if (checkpointOnRefresh) {
127144
}
128145

129146

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+
130186
/**
131187
* Specify the maximum time allotted for the shutdown of each given phase
132188
* (group of {@link SmartLifecycle} beans with the same 'phase' value).
133189
* <p>In case of no specific timeout configured, the default timeout per
134190
* 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
136192
* {@link SmartLifecycle#getPhase()}) and corresponding timeout values
137193
* (in milliseconds)
138194
* @since 6.2
139195
* @see SmartLifecycle#getPhase()
140196
* @see #setTimeoutPerShutdownPhase
141197
*/
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);
144200
}
145201

146202
/**
@@ -168,17 +224,15 @@ public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) {
168224
this.timeoutPerShutdownPhase = timeoutPerShutdownPhase;
169225
}
170226

171-
private long determineTimeout(int phase) {
172-
Long timeout = this.timeoutsForShutdownPhases.get(phase);
173-
return (timeout != null ? timeout : this.timeoutPerShutdownPhase);
174-
}
175-
176227
@Override
177228
public void setBeanFactory(BeanFactory beanFactory) {
178229
if (!(beanFactory instanceof ConfigurableListableBeanFactory clbf)) {
179230
throw new IllegalArgumentException(
180231
"DefaultLifecycleProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
181232
}
233+
if (!this.concurrentStartupForPhases.isEmpty() && clbf.getBootstrapExecutor() == null) {
234+
throw new IllegalStateException("'bootstrapExecutor' needs to be configured for concurrent startup");
235+
}
182236
this.beanFactory = clbf;
183237
}
184238

@@ -188,6 +242,22 @@ private ConfigurableListableBeanFactory getBeanFactory() {
188242
return beanFactory;
189243
}
190244

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+
191261

192262
// Lifecycle implementation
193263

@@ -282,9 +352,8 @@ private void startBeans(boolean autoStartupOnly) {
282352
lifecycleBeans.forEach((beanName, bean) -> {
283353
if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
284354
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);
288357
}
289358
});
290359

@@ -305,30 +374,41 @@ private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) {
305374
* @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
306375
* @param beanName the name of the bean to start
307376
*/
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+
309380
Lifecycle bean = lifecycleBeans.remove(beanName);
310381
if (bean != null && bean != this) {
311382
String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName);
312383
for (String dependency : dependenciesForBean) {
313-
doStart(lifecycleBeans, dependency, autoStartupOnly);
384+
doStart(lifecycleBeans, dependency, autoStartupOnly, futures);
314385
}
315386
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()));
324389
}
325-
if (logger.isDebugEnabled()) {
326-
logger.debug("Successfully started bean '" + beanName + "'");
390+
else {
391+
doStart(beanName, bean);
327392
}
328393
}
329394
}
330395
}
331396

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+
332412
private boolean toBeStarted(String beanName, Lifecycle bean) {
333413
Set<String> stoppedBeans = this.stoppedBeans;
334414
return (stoppedBeans != null ? stoppedBeans.contains(beanName) :
@@ -341,9 +421,8 @@ private void stopBeans() {
341421

342422
lifecycleBeans.forEach((beanName, bean) -> {
343423
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);
347426
});
348427

349428
if (!phases.isEmpty()) {
@@ -414,7 +493,7 @@ else if (bean instanceof SmartLifecycle) {
414493
}
415494

416495

417-
// overridable hooks
496+
// Overridable hooks
418497

419498
/**
420499
* Retrieve all applicable Lifecycle beans: all singletons that have already been created,
@@ -470,8 +549,6 @@ private class LifecycleGroup {
470549

471550
private final int phase;
472551

473-
private final long timeout;
474-
475552
private final Map<String, ? extends Lifecycle> lifecycleBeans;
476553

477554
private final boolean autoStartupOnly;
@@ -480,11 +557,8 @@ private class LifecycleGroup {
480557

481558
private int smartMemberCount;
482559

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) {
486561
this.phase = phase;
487-
this.timeout = timeout;
488562
this.lifecycleBeans = lifecycleBeans;
489563
this.autoStartupOnly = autoStartupOnly;
490564
}
@@ -503,8 +577,26 @@ public void start() {
503577
if (logger.isDebugEnabled()) {
504578
logger.debug("Starting beans in phase " + this.phase);
505579
}
580+
Long concurrentStartup = determineConcurrentStartup(this.phase);
581+
List<CompletableFuture<?>> futures = (concurrentStartup != null ? new ArrayList<>() : null);
506582
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+
}
508600
}
509601
}
510602

@@ -528,11 +620,14 @@ else if (member.bean instanceof SmartLifecycle) {
528620
}
529621
}
530622
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+
}
536631
}
537632
}
538633
catch (InterruptedException ex) {

0 commit comments

Comments
 (0)