From f0987da4bb96dfb2d474e53de0de38014bf61ee0 Mon Sep 17 00:00:00 2001 From: Kosuke Yanagihara <11373191+ksky8864@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:20:12 +0900 Subject: [PATCH] Back off DevTools' JDBC and R2DBC support when modules are missing DevToolsDataSourceAutoConfiguration and DevToolsR2dbcAutoConfiguration were only guarded by classes from the JDK and r2dbc-spi respectively. When a single user-defined DataSource or ConnectionFactory bean was present, their conditions went on to reference classes from spring-boot-jdbc or spring-boot-r2dbc. Both are optional dependencies of spring-boot-devtools, so startup failed with a NoClassDefFoundError when they were absent. This commit adds a class from each module to the class conditions. Without the modules the conditions could never match, so this only turns the failure into a back-off. See gh-51872 Signed-off-by: Kosuke Yanagihara <11373191+ksky8864@users.noreply.github.com> --- .../DevToolsDataSourceAutoConfiguration.java | 2 +- .../DevToolsR2dbcAutoConfiguration.java | 2 +- ...nfigurationWithoutSpringBootJdbcTests.java | 71 +++++++++++++++++++ .../DevToolsR2dbcAutoConfigurationTests.java | 32 +++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationWithoutSpringBootJdbcTests.java diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java index fbf29e60864..e8758a0dd02 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java @@ -61,7 +61,7 @@ import org.springframework.util.Assert; * @author Andy Wilkinson * @since 1.3.3 */ -@ConditionalOnClass(DataSource.class) +@ConditionalOnClass({ DataSource.class, DataSourceProperties.class }) @ConditionalOnEnabledDevTools @Conditional(DevToolsDataSourceCondition.class) @AutoConfiguration(afterName = "org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration") diff --git a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfiguration.java b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfiguration.java index d544d5d077a..21f2000362a 100644 --- a/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfiguration.java +++ b/module/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfiguration.java @@ -51,7 +51,7 @@ import org.springframework.util.Assert; * @author Phillip Webb * @since 2.5.6 */ -@ConditionalOnClass(ConnectionFactory.class) +@ConditionalOnClass({ ConnectionFactory.class, R2dbcAutoConfiguration.class }) @ConditionalOnEnabledDevTools @Conditional(DevToolsConnectionFactoryCondition.class) @AutoConfiguration(afterName = "org.springframework.boot.r2dbc.autoconfigure.R2dbcAutoConfiguration") diff --git a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationWithoutSpringBootJdbcTests.java b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationWithoutSpringBootJdbcTests.java new file mode 100644 index 00000000000..9794a523d1e --- /dev/null +++ b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationWithoutSpringBootJdbcTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.devtools.autoconfigure; + +import java.util.concurrent.FutureTask; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DevToolsDataSourceAutoConfiguration} when {@code spring-boot-jdbc} is + * not on the classpath. + * + * @author Kosuke Yanagihara + */ +@ClassPathExclusions(packages = "org.springframework.boot.jdbc.autoconfigure") +class DevToolsDataSourceAutoConfigurationWithoutSpringBootJdbcTests { + + @Test + void backsOffWithSingleManuallyConfiguredDataSource() throws Exception { + // DevTools is disabled when JUnit is on the stack so refresh on a new thread + FutureTask task = new FutureTask<>(() -> { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(SingleDataSourceConfiguration.class); + context.refresh(); + return context; + }); + new Thread(task).start(); + try (ConfigurableApplicationContext context = task.get()) { + assertThat(context.getBeansOfType(DataSource.class)).hasSize(1); + assertThat(context.containsBean("inMemoryDatabaseShutdownExecutor")).isFalse(); + } + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(DevToolsDataSourceAutoConfiguration.class) + static class SingleDataSourceConfiguration { + + @Bean + DataSource dataSource() { + return mock(DataSource.class); + } + + } + +} diff --git a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfigurationTests.java b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfigurationTests.java index 61c0983f006..b2836bc3233 100644 --- a/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfigurationTests.java +++ b/module/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfigurationTests.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.FutureTask; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -30,6 +31,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.devtools.autoconfigure.DevToolsR2dbcAutoConfiguration.R2dbcDatabaseShutdownEvent; import org.springframework.boot.r2dbc.SimpleConnectionFactoryProvider.SimpleTestConnectionFactory; import org.springframework.boot.r2dbc.autoconfigure.R2dbcAutoConfiguration; @@ -40,6 +42,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -166,6 +169,35 @@ class DevToolsR2dbcAutoConfigurationTests { } + @Nested + @ClassPathExclusions(packages = "org.springframework.boot.r2dbc.autoconfigure") + class WithoutSpringBootR2dbc { + + @Test + void backsOffWithSingleManuallyConfiguredConnectionFactory() throws Exception { + // DevTools is disabled when JUnit is on the stack so refresh on a new thread + FutureTask task = new FutureTask<>(() -> { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(SingleConnectionFactoryWithDevToolsConfiguration.class); + context.refresh(); + return context; + }); + new Thread(task).start(); + try (ConfigurableApplicationContext context = task.get()) { + assertThat(context.getBeansOfType(ConnectionFactory.class)).hasSize(1); + assertThat(context.containsBean("inMemoryR2dbcDatabaseShutdownExecutor")).isFalse(); + } + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(SingleConnectionFactoryConfiguration.class) + @ImportAutoConfiguration(DevToolsR2dbcAutoConfiguration.class) + static class SingleConnectionFactoryWithDevToolsConfiguration { + + } + @Configuration(proxyBeanMethods = false) static class SingleConnectionFactoryConfiguration {