Skip to content

Commit 0e2af5d

Browse files
committed
Avoid default AutoCloseable implementation in ExecutorService on JDK 19+
Consistently calls shutdown() unless a specific close() method has been provided in a subclass. Closes gh-35316
1 parent ed7c3d7 commit 0e2af5d

File tree

2 files changed

+65
-4
lines changed

2 files changed

+65
-4
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;
@@ -418,14 +419,29 @@ static String[] inferDestroyMethodsIfNecessary(Class<?> target, RootBeanDefiniti
418419
String destroyMethodName = beanDefinition.resolvedDestroyMethodName;
419420
if (destroyMethodName == null) {
420421
destroyMethodName = beanDefinition.getDestroyMethodName();
421-
boolean autoCloseable = (AutoCloseable.class.isAssignableFrom(target));
422+
boolean autoCloseable = AutoCloseable.class.isAssignableFrom(target);
423+
boolean executorService = ExecutorService.class.isAssignableFrom(target);
422424
if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||
423-
(destroyMethodName == null && autoCloseable)) {
425+
(destroyMethodName == null && (autoCloseable || executorService))) {
424426
// Only perform destroy method inference in case of the bean
425427
// not explicitly implementing the DisposableBean interface
426428
destroyMethodName = null;
427429
if (!(DisposableBean.class.isAssignableFrom(target))) {
428-
if (autoCloseable) {
430+
if (executorService) {
431+
destroyMethodName = SHUTDOWN_METHOD_NAME;
432+
try {
433+
// On JDK 19+, avoid the ExecutorService-level AutoCloseable default implementation
434+
// which awaits task termination for 1 day, even for delayed tasks such as cron jobs.
435+
// Custom close() implementations in ExecutorService subclasses are still accepted.
436+
if (target.getMethod(CLOSE_METHOD_NAME).getDeclaringClass() != ExecutorService.class) {
437+
destroyMethodName = CLOSE_METHOD_NAME;
438+
}
439+
}
440+
catch (NoSuchMethodException ex) {
441+
// Ignore - stick with shutdown()
442+
}
443+
}
444+
else if (autoCloseable) {
429445
destroyMethodName = CLOSE_METHOD_NAME;
430446
}
431447
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

0 commit comments

Comments
 (0)