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>
This commit is contained in:
Kosuke Yanagihara
2026-09-28 12:44:47 +02:00
committed by Stéphane Nicoll
parent 8c7c372dcb
commit f0987da4bb
4 changed files with 105 additions and 2 deletions
@@ -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")
@@ -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")
@@ -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<ConfigurableApplicationContext> 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);
}
}
}
@@ -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<ConfigurableApplicationContext> 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 {