From b6bccc13789224e80328057506c31b9d2dee9026 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 10 Mar 2025 14:28:45 +0000 Subject: [PATCH] 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 --- .../jdbc/DataSourceAutoConfiguration.java | 8 ++ ...toConfigurationWithoutSpringJdbcTests.java | 77 +++++++++++++++++++ .../boot/jdbc/DataSourceBuilder.java | 9 --- .../boot/jdbc/EmbeddedDatabaseConnection.java | 15 ++-- 4 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationWithoutSpringJdbcTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index df6712f741..5fd4bda9e3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -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()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationWithoutSpringJdbcTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationWithoutSpringJdbcTests.java new file mode 100644 index 0000000000..df6f394e95 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationWithoutSpringJdbcTests.java @@ -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 hideConnectionPools() { + return (runner) -> runner.withClassLoader(new FilteredClassLoader("org.apache.tomcat", "com.zaxxer.hikari", + "org.apache.commons.dbcp2", "oracle.ucp.jdbc", "com.mchange")); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java index 697363e30c..c71a224b6e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java @@ -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 { * @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)); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java index 9a1e4e79e8..143e23a22f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java @@ -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 { + private static final class IsEmbedded implements ThrowingFunction { @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) {