Support DataSource auto-configuration without spring-jdbc

Prior to these changes, auto-configured a DataSource required
spring-jdbc to be on the classpath even if the app made no use of any
of its features.

The changes largely remove the use of spring-jdbc during DataSoruce
auto-configure or disable some support (configuring an unpooled,
embedded database) in its absense. The extra unwrapping in
DataSourceBuilder has been removed as it appears to be redundant.
The existing test for deriving from an embedded database continues
to work without it.

Closes gh-43786
This commit is contained in:
Andy Wilkinson
2025-03-10 14:28:45 +00:00
parent 58fab66163
commit b6bccc1378
4 changed files with 93 additions and 16 deletions

View File

@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -42,6 +43,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -132,6 +134,8 @@ public class DataSourceAutoConfiguration {
private static final String DATASOURCE_URL_PROPERTY = "spring.datasource.url";
private static final String EMBEDDED_DATABASE_TYPE = "org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType";
private final SpringBootCondition pooledCondition = new PooledDataSourceCondition();
@Override
@@ -143,6 +147,10 @@ public class DataSourceAutoConfiguration {
if (anyMatches(context, metadata, this.pooledCondition)) {
return ConditionOutcome.noMatch(message.foundExactly("supported pooled data source"));
}
if (!ClassUtils.isPresent(EMBEDDED_DATABASE_TYPE, context.getClassLoader())) {
return ConditionOutcome
.noMatch(message.didNotFind("required class").items(Style.QUOTE, EMBEDDED_DATABASE_TYPE));
}
EmbeddedDatabaseType type = EmbeddedDatabaseConnection.get(context.getClassLoader()).getType();
if (type == null) {
return ConditionOutcome.noMatch(message.didNotFind("embedded database").atAll());

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2025 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.autoconfigure.jdbc;
import java.util.Random;
import java.util.function.Function;
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DataSourceAutoConfiguration} without spring-jdbc on the classpath.
*
* @author Andy Wilkinson
*/
@ClassPathExclusions("spring-jdbc-*.jar")
class DataSourceAutoConfigurationWithoutSpringJdbcTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class));
@Test
void pooledDataSourceCanBeAutoConfigured() {
this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.run((context) -> {
HikariDataSource dataSource = context.getBean(HikariDataSource.class);
assertThat(dataSource.getJdbcUrl()).isNotNull();
assertThat(dataSource.getDriverClassName()).isNotNull();
});
}
@Test
void withoutConnectionPoolsAutoConfigurationBacksOff() {
this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.with(hideConnectionPools())
.run((context) -> assertThat(context).doesNotHaveBean(DataSource.class));
}
@Test
void withUrlAndWithoutConnectionPoolsAutoConfigurationBacksOff() {
this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.with(hideConnectionPools())
.withPropertyValues("spring.datasource.url:jdbc:hsqldb:mem:testdb-" + new Random().nextInt())
.run((context) -> assertThat(context).doesNotHaveBean(DataSource.class));
}
private static Function<ApplicationContextRunner, ApplicationContextRunner> hideConnectionPools() {
return (runner) -> runner.withClassLoader(new FilteredClassLoader("org.apache.tomcat", "com.zaxxer.hikari",
"org.apache.commons.dbcp2", "oracle.ucp.jdbc", "com.mchange"));
}
}

View File

@@ -41,7 +41,6 @@ import org.vibur.dbcp.ViburDBCPDataSource;
import org.springframework.beans.BeanUtils;
import org.springframework.core.ResolvableType;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
@@ -233,14 +232,6 @@ public final class DataSourceBuilder<T extends DataSource> {
* @since 2.5.0
*/
public static DataSourceBuilder<?> derivedFrom(DataSource dataSource) {
if (dataSource instanceof EmbeddedDatabase) {
try {
dataSource = dataSource.unwrap(DataSource.class);
}
catch (SQLException ex) {
throw new IllegalStateException("Unable to unwrap embedded database", ex);
}
}
return new DataSourceBuilder<>(unwrap(dataSource));
}

View File

@@ -25,11 +25,10 @@ import java.util.stream.Stream;
import javax.sql.DataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.function.ThrowingFunction;
/**
* Connection details for {@link EmbeddedDatabaseType embedded databases}.
@@ -165,9 +164,11 @@ public enum EmbeddedDatabaseConnection {
*/
public static boolean isEmbedded(DataSource dataSource) {
try {
return new JdbcTemplate(dataSource).execute(new IsEmbedded());
try (Connection connection = dataSource.getConnection()) {
return new IsEmbedded().apply(connection);
}
}
catch (DataAccessException ex) {
catch (SQLException ex) {
// Could not connect, which means it's not embedded
return false;
}
@@ -189,12 +190,12 @@ public enum EmbeddedDatabaseConnection {
}
/**
* {@link ConnectionCallback} to determine if a connection is embedded.
* Determine if a {@link Connection} is embedded.
*/
private static final class IsEmbedded implements ConnectionCallback<Boolean> {
private static final class IsEmbedded implements ThrowingFunction<Connection, Boolean> {
@Override
public Boolean doInConnection(Connection connection) throws SQLException, DataAccessException {
public Boolean applyWithException(Connection connection) throws SQLException, DataAccessException {
DatabaseMetaData metaData = connection.getMetaData();
String productName = metaData.getDatabaseProductName();
if (productName == null) {