Skip to content

Commit a1f5f6a

Browse files
committed
Merge branch '6.2.x'
2 parents 9e9d716 + f62519b commit a1f5f6a

File tree

3 files changed

+115
-13
lines changed

3 files changed

+115
-13
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.List;
2424
import java.util.concurrent.CountDownLatch;
2525
import java.util.concurrent.ExecutionException;
26+
import java.util.concurrent.ExecutorService;
2627
import java.util.concurrent.Future;
2728

2829
import org.apache.commons.logging.Log;
@@ -412,14 +413,29 @@ public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefin
412413
String destroyMethodName = beanDefinition.resolvedDestroyMethodName;
413414
if (destroyMethodName == null) {
414415
destroyMethodName = beanDefinition.getDestroyMethodName();
415-
boolean autoCloseable = (AutoCloseable.class.isAssignableFrom(target));
416+
boolean autoCloseable = AutoCloseable.class.isAssignableFrom(target);
417+
boolean executorService = ExecutorService.class.isAssignableFrom(target);
416418
if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||
417-
(destroyMethodName == null && autoCloseable)) {
419+
(destroyMethodName == null && (autoCloseable || executorService))) {
418420
// Only perform destroy method inference in case of the bean
419421
// not explicitly implementing the DisposableBean interface
420422
destroyMethodName = null;
421423
if (!(DisposableBean.class.isAssignableFrom(target))) {
422-
if (autoCloseable) {
424+
if (executorService) {
425+
destroyMethodName = SHUTDOWN_METHOD_NAME;
426+
try {
427+
// On JDK 19+, avoid the ExecutorService-level AutoCloseable default implementation
428+
// which awaits task termination for 1 day, even for delayed tasks such as cron jobs.
429+
// Custom close() implementations in ExecutorService subclasses are still accepted.
430+
if (target.getMethod(CLOSE_METHOD_NAME).getDeclaringClass() != ExecutorService.class) {
431+
destroyMethodName = CLOSE_METHOD_NAME;
432+
}
433+
}
434+
catch (NoSuchMethodException ex) {
435+
// Ignore - stick with shutdown()
436+
}
437+
}
438+
else if (autoCloseable) {
423439
destroyMethodName = CLOSE_METHOD_NAME;
424440
}
425441
else {

spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.beans.factory.support;
1818

1919
import java.lang.reflect.Method;
20+
import java.util.concurrent.ExecutorService;
2021

2122
import org.junit.jupiter.api.Test;
2223

@@ -59,13 +60,38 @@ void setInstanceDoesNotOverrideResolvedFactoryMethodWithNull() {
5960
}
6061

6162
@Test
62-
void resolveDestroyMethodWithMatchingCandidateReplacedInferredVaue() {
63+
void resolveDestroyMethodWithMatchingCandidateReplacedForCloseMethod() {
6364
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithCloseMethod.class);
6465
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
6566
beanDefinition.resolveDestroyMethodIfNecessary();
6667
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close");
6768
}
6869

70+
@Test
71+
void resolveDestroyMethodWithMatchingCandidateReplacedForShutdownMethod() {
72+
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithShutdownMethod.class);
73+
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
74+
beanDefinition.resolveDestroyMethodIfNecessary();
75+
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown");
76+
}
77+
78+
@Test
79+
void resolveDestroyMethodWithMatchingCandidateReplacedForExecutorService() {
80+
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorService.class);
81+
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
82+
beanDefinition.resolveDestroyMethodIfNecessary();
83+
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown");
84+
// even on JDK 19+ where the ExecutorService interface declares a default AutoCloseable implementation
85+
}
86+
87+
@Test
88+
void resolveDestroyMethodWithMatchingCandidateReplacedForAutoCloseableExecutorService() {
89+
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorServiceAndAutoCloseable.class);
90+
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
91+
beanDefinition.resolveDestroyMethodIfNecessary();
92+
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close");
93+
}
94+
6995
@Test
7096
void resolveDestroyMethodWithNoCandidateSetDestroyMethodNameToNull() {
7197
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithNoDestroyMethod.class);
@@ -90,6 +116,25 @@ public void close() {
90116
}
91117

92118

119+
static class BeanWithShutdownMethod {
120+
121+
public void shutdown() {
122+
}
123+
}
124+
125+
126+
abstract static class BeanImplementingExecutorService implements ExecutorService {
127+
}
128+
129+
130+
abstract static class BeanImplementingExecutorServiceAndAutoCloseable implements ExecutorService, AutoCloseable {
131+
132+
@Override
133+
public void close() {
134+
}
135+
}
136+
137+
93138
static class BeanWithNoDestroyMethod {
94139
}
95140

spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
8787

8888
private @Nullable Set<Thread> activeThreads;
8989

90+
private boolean cancelRemainingTasksOnClose = false;
91+
9092
private boolean rejectTasksWhenLimitReached = false;
9193

9294
private volatile boolean active = true;
@@ -178,12 +180,33 @@ public void setTaskDecorator(TaskDecorator taskDecorator) {
178180
* @param timeout the timeout in milliseconds
179181
* @since 6.1
180182
* @see #close()
183+
* @see #setCancelRemainingTasksOnClose
181184
* @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#setAwaitTerminationMillis
182185
*/
183186
public void setTaskTerminationTimeout(long timeout) {
184187
Assert.isTrue(timeout >= 0, "Timeout value must be >=0");
185188
this.taskTerminationTimeout = timeout;
186-
this.activeThreads = (timeout > 0 ? ConcurrentHashMap.newKeySet() : null);
189+
trackActiveThreadsIfNecessary();
190+
}
191+
192+
/**
193+
* Specify whether to cancel remaining tasks on close: that is, whether to
194+
* interrupt any active threads at the time of the {@link #close()} call.
195+
* <p>The default is {@code false}, not tracking active threads at all or
196+
* just interrupting any remaining threads that still have not finished after
197+
* the specified {@link #setTaskTerminationTimeout taskTerminationTimeout}.
198+
* Switch this to {@code true} for immediate interruption on close, either in
199+
* combination with a subsequent termination timeout or without any waiting
200+
* at all, depending on whether a {@code taskTerminationTimeout} has been
201+
* specified as well.
202+
* @since 6.2.11
203+
* @see #close()
204+
* @see #setTaskTerminationTimeout
205+
* @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#setWaitForTasksToCompleteOnShutdown
206+
*/
207+
public void setCancelRemainingTasksOnClose(boolean cancelRemainingTasksOnClose) {
208+
this.cancelRemainingTasksOnClose = cancelRemainingTasksOnClose;
209+
trackActiveThreadsIfNecessary();
187210
}
188211

189212
/**
@@ -243,6 +266,15 @@ public boolean isActive() {
243266
return this.active;
244267
}
245268

269+
/**
270+
* Track active threads only when a task termination timeout has been
271+
* specified or interruption of remaining threads has been requested.
272+
*/
273+
private void trackActiveThreadsIfNecessary() {
274+
this.activeThreads = (this.taskTerminationTimeout > 0 || this.cancelRemainingTasksOnClose ?
275+
ConcurrentHashMap.newKeySet() : null);
276+
}
277+
246278

247279
/**
248280
* Executes the given task, within a concurrency throttle
@@ -331,7 +363,7 @@ protected Thread newThread(Runnable task) {
331363
}
332364

333365
/**
334-
* This close methods tracks the termination of active threads if a concrete
366+
* This close method tracks the termination of active threads if a concrete
335367
* {@link #setTaskTerminationTimeout task termination timeout} has been set.
336368
* Otherwise, it is not necessary to close this executor.
337369
* @since 6.1
@@ -342,17 +374,26 @@ public void close() {
342374
this.active = false;
343375
Set<Thread> threads = this.activeThreads;
344376
if (threads != null) {
345-
synchronized (threads) {
346-
try {
347-
if (!threads.isEmpty()) {
348-
threads.wait(this.taskTerminationTimeout);
377+
if (this.cancelRemainingTasksOnClose) {
378+
// Early interrupt for remaining tasks on close
379+
threads.forEach(Thread::interrupt);
380+
}
381+
if (this.taskTerminationTimeout > 0) {
382+
synchronized (threads) {
383+
try {
384+
if (!threads.isEmpty()) {
385+
threads.wait(this.taskTerminationTimeout);
386+
}
387+
}
388+
catch (InterruptedException ex) {
389+
Thread.currentThread().interrupt();
349390
}
350391
}
351-
catch (InterruptedException ex) {
352-
Thread.currentThread().interrupt();
392+
if (!this.cancelRemainingTasksOnClose) {
393+
// Late interrupt for remaining tasks after timeout
394+
threads.forEach(Thread::interrupt);
353395
}
354396
}
355-
threads.forEach(Thread::interrupt);
356397
}
357398
}
358399
}

0 commit comments

Comments
 (0)