diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java index ecaa0296fde..2730af59225 100644 --- a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java @@ -237,23 +237,7 @@ public final class DataSourceBuilder { * @return a new {@link DataSource} builder */ public static DataSourceBuilder derivedFrom(DataSource dataSource) { - return new DataSourceBuilder<>(unwrap(dataSource)); - } - - private static DataSource unwrap(DataSource dataSource) { - try { - while (dataSource.isWrapperFor(DataSource.class)) { - DataSource unwrapped = dataSource.unwrap(DataSource.class); - if (unwrapped == dataSource) { - return unwrapped; - } - dataSource = unwrapped; - } - } - catch (SQLException ex) { - // Try to continue with the existing, potentially still wrapped, DataSource - } - return dataSource; + return new DataSourceBuilder<>(DataSourceUnwrapper.unwrapRoot(dataSource)); } /** diff --git a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceUnwrapper.java b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceUnwrapper.java index af4cda88066..de1b3e01440 100644 --- a/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceUnwrapper.java +++ b/module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/DataSourceUnwrapper.java @@ -93,6 +93,37 @@ public final class DataSourceUnwrapper { return unwrap(dataSource, target, target); } + /** + * Return the root {@link DataSource} by recursively unwrapping all + * {@link org.springframework.jdbc.datasource.DelegatingDataSource delegating}, proxy, + * and {@link java.sql.Wrapper} layers until no further unwrapping is possible. + * @param dataSource the datasource to unwrap + * @return the root {@link DataSource} + * @since 4.1.0 + */ + public static DataSource unwrapRoot(DataSource dataSource) { + if (DELEGATING_DATA_SOURCE_PRESENT) { + DataSource targetDataSource = DelegatingDataSourceUnwrapper.getTargetDataSource(dataSource); + if (targetDataSource != null) { + return unwrapRoot(targetDataSource); + } + } + if (AopUtils.isAopProxy(dataSource)) { + Object proxyTarget = AopProxyUtils.getSingletonTarget(dataSource); + if (proxyTarget instanceof DataSource proxyDataSource) { + return unwrapRoot(proxyDataSource); + } + } + DataSource unwrapped = safeUnwrap(dataSource); + if (unwrapped != null) { + if (unwrapped == dataSource) { + return unwrapped; + } + return unwrapRoot(unwrapped); + } + return dataSource; + } + private static @Nullable S safeUnwrap(Wrapper wrapper, Class target) { try { if (target.isInterface() && wrapper.isWrapperFor(target)) { @@ -105,6 +136,18 @@ public final class DataSourceUnwrapper { return null; } + private static @Nullable DataSource safeUnwrap(DataSource dataSource) { + try { + if (dataSource.isWrapperFor(DataSource.class)) { + return dataSource.unwrap(DataSource.class); + } + } + catch (Exception ex) { + // continue + } + return null; + } + private static final class DelegatingDataSourceUnwrapper { private static @Nullable DataSource getTargetDataSource(DataSource dataSource) { diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java index ab90f0a35d5..82891618719 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java @@ -46,6 +46,7 @@ import org.postgresql.ds.PGSimpleDataSource; import org.vibur.dbcp.ViburDBCPDataSource; import org.springframework.jdbc.datasource.AbstractDataSource; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; @@ -61,6 +62,7 @@ import static org.assertj.core.api.Assertions.assertThatNoException; * @author Stephane Nicoll * @author Fabio Grassi * @author Phillip Webb + * @author Vedran Pavic */ class DataSourceBuilderTests { @@ -427,6 +429,19 @@ class DataSourceBuilderTests { assertThat(built.getJdbcUrl()).isEqualTo("jdbc:h2:test"); } + @Test + void buildWhenDerivedFromLazyConnectionDataSourceProxy() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setUsername("test"); + dataSource.setPassword("secret"); + dataSource.setJdbcUrl("jdbc:h2:test"); + DataSourceBuilder builder = DataSourceBuilder.derivedFrom(new LazyConnectionDataSourceProxy(dataSource)); + HikariDataSource built = (HikariDataSource) builder.username("test2").password("secret2").build(); + assertThat(built.getUsername()).isEqualTo("test2"); + assertThat(built.getPassword()).isEqualTo("secret2"); + assertThat(built.getJdbcUrl()).isEqualTo("jdbc:h2:test"); + } + @Test // gh-26644 void buildWhenDerivedFromExistingDatabaseWithTypeChange() { HikariDataSource dataSource = new HikariDataSource(); diff --git a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java index 3b8cd79f260..9a00a8dae54 100644 --- a/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java +++ b/module/spring-boot-jdbc/src/test/java/org/springframework/boot/jdbc/DataSourceUnwrapperTests.java @@ -106,6 +106,40 @@ class DataSourceUnwrapperTests { .isSameAs(dataSource); } + @Test + void unwrapRootWithPlainDataSource() { + DataSource dataSource = new HikariDataSource(); + assertThat(DataSourceUnwrapper.unwrapRoot(dataSource)).isSameAs(dataSource); + } + + @Test + void unwrapRootWithDelegate() { + DataSource dataSource = new HikariDataSource(); + DataSource actual = wrapInDelegate(wrapInDelegate(dataSource)); + assertThat(DataSourceUnwrapper.unwrapRoot(actual)).isSameAs(dataSource); + } + + @Test + void unwrapRootWithProxy() { + DataSource dataSource = new HikariDataSource(); + DataSource actual = wrapInProxy(wrapInProxy(dataSource)); + assertThat(DataSourceUnwrapper.unwrapRoot(actual)).isSameAs(dataSource); + } + + @Test + void unwrapRootWithLazyConnectionDataSource() { + DataSource dataSource = new HikariDataSource(); + DataSource actual = new LazyConnectionDataSourceProxy(dataSource); + assertThat(DataSourceUnwrapper.unwrapRoot(actual)).isSameAs(dataSource); + } + + @Test + void unwrapRootWithSeveralLevelOfWrapping() { + DataSource dataSource = new HikariDataSource(); + DataSource actual = wrapInProxy(wrapInDelegate(wrapInDelegate(wrapInProxy(wrapInDelegate(dataSource))))); + assertThat(DataSourceUnwrapper.unwrapRoot(actual)).isSameAs(dataSource); + } + @Test void unwrappingIsNotAttemptedWhenTargetIsNotAnInterface() { DataSource dataSource = mock(DataSource.class); diff --git a/module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfigurationTests.java b/module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfigurationTests.java index bdcff998ca1..70db2902df7 100644 --- a/module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfigurationTests.java +++ b/module/spring-boot-liquibase/src/test/java/org/springframework/boot/liquibase/autoconfigure/LiquibaseAutoConfigurationTests.java @@ -51,6 +51,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; import org.springframework.boot.jdbc.autoconfigure.EmbeddedDataSourceConfiguration; import org.springframework.boot.jdbc.autoconfigure.JdbcConnectionDetails; import org.springframework.boot.jdbc.autoconfigure.JdbcTemplateAutoConfiguration; @@ -90,6 +91,7 @@ import static org.assertj.core.api.Assertions.contentOf; * @author Moritz Halbritter * @author Phillip Webb * @author Ahmed Ashour + * @author Vedran Pavic */ @ExtendWith(OutputCaptureExtension.class) class LiquibaseAutoConfigurationTests { @@ -373,6 +375,20 @@ class LiquibaseAutoConfigurationTests { })); } + @Test + @WithDbChangelogMasterYamlResource + void lazyConnectionDataSource() { + String jdbcUrl = "jdbc:h2:mem:liquibase-" + UUID.randomUUID(); + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.datasource.url:" + jdbcUrl, "spring.datasource.username:not-sa", + "spring.datasource.connection-fetch:lazy", "spring.liquibase.user:sa") + .run(assertLiquibase((liquibase) -> { + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo(jdbcUrl); + assertThat(dataSource.getUsername()).isEqualTo("sa"); + })); + } + @Test @WithDbChangelogMasterYamlResource void overrideUserWhenCustom() {