diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index e74e5f23f29..46f5b08907d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.apache.commons.logging.Log; @@ -418,14 +419,29 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable { String destroyMethodName = beanDefinition.resolvedDestroyMethodName; if (destroyMethodName == null) { destroyMethodName = beanDefinition.getDestroyMethodName(); - boolean autoCloseable = (AutoCloseable.class.isAssignableFrom(target)); + boolean autoCloseable = AutoCloseable.class.isAssignableFrom(target); + boolean executorService = ExecutorService.class.isAssignableFrom(target); if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) || - (destroyMethodName == null && autoCloseable)) { + (destroyMethodName == null && (autoCloseable || executorService))) { // Only perform destroy method inference in case of the bean // not explicitly implementing the DisposableBean interface destroyMethodName = null; if (!(DisposableBean.class.isAssignableFrom(target))) { - if (autoCloseable) { + if (executorService) { + destroyMethodName = SHUTDOWN_METHOD_NAME; + try { + // On JDK 19+, avoid the ExecutorService-level AutoCloseable default implementation + // which awaits task termination for 1 day, even for delayed tasks such as cron jobs. + // Custom close() implementations in ExecutorService subclasses are still accepted. + if (target.getMethod(CLOSE_METHOD_NAME).getDeclaringClass() != ExecutorService.class) { + destroyMethodName = CLOSE_METHOD_NAME; + } + } + catch (NoSuchMethodException ex) { + // Ignore - stick with shutdown() + } + } + else if (autoCloseable) { destroyMethodName = CLOSE_METHOD_NAME; } else { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java index 056597442ba..3d71b6d2420 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; import org.junit.jupiter.api.Test; @@ -59,13 +60,38 @@ class RootBeanDefinitionTests { } @Test - void resolveDestroyMethodWithMatchingCandidateReplacedInferredVaue() { + void resolveDestroyMethodWithMatchingCandidateReplacedForCloseMethod() { RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithCloseMethod.class); beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); beanDefinition.resolveDestroyMethodIfNecessary(); assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close"); } + @Test + void resolveDestroyMethodWithMatchingCandidateReplacedForShutdownMethod() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithShutdownMethod.class); + beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); + beanDefinition.resolveDestroyMethodIfNecessary(); + assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown"); + } + + @Test + void resolveDestroyMethodWithMatchingCandidateReplacedForExecutorService() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorService.class); + beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); + beanDefinition.resolveDestroyMethodIfNecessary(); + assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown"); + // even on JDK 19+ where the ExecutorService interface declares a default AutoCloseable implementation + } + + @Test + void resolveDestroyMethodWithMatchingCandidateReplacedForAutoCloseableExecutorService() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorServiceAndAutoCloseable.class); + beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); + beanDefinition.resolveDestroyMethodIfNecessary(); + assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close"); + } + @Test void resolveDestroyMethodWithNoCandidateSetDestroyMethodNameToNull() { RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithNoDestroyMethod.class); @@ -90,6 +116,25 @@ class RootBeanDefinitionTests { } + static class BeanWithShutdownMethod { + + public void shutdown() { + } + } + + + abstract static class BeanImplementingExecutorService implements ExecutorService { + } + + + abstract static class BeanImplementingExecutorServiceAndAutoCloseable implements ExecutorService, AutoCloseable { + + @Override + public void close() { + } + } + + static class BeanWithNoDestroyMethod { }