From 18ea43c905ba1f8ee94177199ef4f8b44ccd2bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 4 Jan 2024 16:32:05 +0100 Subject: [PATCH] Allow customizations of embedded database connections This commit allows EmbeddedDatabaseConfigurer instances to be further customized if necessary. EmbeddedDatabaseBuilder has a way now to set a DatabaseConfigurer rather than just a type to provide full control, or customize an existing supported database type using the new EmbeddedDatabaseConfigurers#customizeConfigurer callback. Closes gh-21160 --- .../jdbc/embedded-database-support.adoc | 68 +++++++++++++++++++ .../embedded/EmbeddedDatabaseBuilder.java | 18 ++++- .../EmbeddedDatabaseConfigurerDelegate.java | 39 +++++++++++ ....java => EmbeddedDatabaseConfigurers.java} | 34 +++++++--- .../embedded/EmbeddedDatabaseFactory.java | 23 +++++-- .../EmbeddedDatabaseBuilderTests.java | 17 +++++ .../EmbeddedDatabaseFactoryTests.java | 45 +++++++++++- 7 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java rename spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/{EmbeddedDatabaseConfigurerFactory.java => EmbeddedDatabaseConfigurers.java} (60%) diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc index f9855a33c84..238579144ad 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/embedded-database-support.adoc @@ -168,6 +168,74 @@ attribute of the `embedded-database` tag to `DERBY`. If you use the builder API, call the `setType(EmbeddedDatabaseType)` method with `EmbeddedDatabaseType.DERBY`. +[[jdbc-embedded-database-types-custom]] +== Customizing the Embedded Database Type + +While each supported type comes with default connection settings, it is possible +to customize them if necessary. The following example uses H2 with a custom driver: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @Configuration + public class DataSourceConfig { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setDatabaseConfigurer(EmbeddedDatabaseConfigurers + .customizeConfigurer(H2, this::customize)) + .addScript("schema.sql") + .build(); + } + + private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) { + return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, databaseName); + properties.setDriverClass(CustomDriver.class); + } + }; + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @Configuration + class DataSourceConfig { + + @Bean + fun dataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .setDatabaseConfigurer(EmbeddedDatabaseConfigurers + .customizeConfigurer(EmbeddedDatabaseType.H2) { this.customize(it) }) + .addScript("schema.sql") + .build() + } + + private fun customize(defaultConfigurer: EmbeddedDatabaseConfigurer): EmbeddedDatabaseConfigurer { + return object : EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + override fun configureConnectionProperties( + properties: ConnectionProperties, + databaseName: String + ) { + super.configureConnectionProperties(properties, databaseName) + properties.setDriverClass(CustomDriver::class.java) + } + } + } + } +---- +====== + + [[jdbc-embedded-database-dao-testing]] == Testing Data Access Logic with an Embedded Database diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java index 9e021a6e34a..ea5558d693d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -112,7 +112,8 @@ public class EmbeddedDatabaseBuilder { } /** - * Set the type of embedded database. + * Set the type of embedded database. Consider using {@link #setDatabaseConfigurer} + * if customization of the connections properties is necessary. *

Defaults to HSQL if not called. * @param databaseType the type of embedded database to build * @return {@code this}, to facilitate method chaining @@ -122,6 +123,19 @@ public class EmbeddedDatabaseBuilder { return this; } + /** + * Set the {@linkplain EmbeddedDatabaseConfigurer configurer} to use to + * configure the embedded database, as an alternative to {@link #setType}. + * @param configurer the configurer of the embedded database + * @return {@code this}, to facilitate method chaining + * @since 6.2 + * @see EmbeddedDatabaseConfigurers + */ + public EmbeddedDatabaseBuilder setDatabaseConfigurer(EmbeddedDatabaseConfigurer configurer) { + this.databaseFactory.setDatabaseConfigurer(configurer); + return this; + } + /** * Set the factory to use to create the {@link DataSource} instance that * connects to the embedded database. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java new file mode 100644 index 00000000000..252757868e1 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerDelegate.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2024 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.jdbc.datasource.embedded; + +/** + * A {@link EmbeddedDatabaseConfigurer} delegate that can be used to customize + * the embedded database. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class EmbeddedDatabaseConfigurerDelegate extends AbstractEmbeddedDatabaseConfigurer { + + private final EmbeddedDatabaseConfigurer target; + + public EmbeddedDatabaseConfigurerDelegate(EmbeddedDatabaseConfigurer target) { + this.target = target; + } + + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + this.target.configureConnectionProperties(properties, databaseName); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java similarity index 60% rename from spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java rename to spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java index f9b728acd02..59373ac4aa8 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -16,6 +16,8 @@ package org.springframework.jdbc.datasource.embedded; +import java.util.function.UnaryOperator; + import org.springframework.util.Assert; /** @@ -25,28 +27,24 @@ import org.springframework.util.Assert; * @author Keith Donald * @author Oliver Gierke * @author Sam Brannen - * @since 3.0 + * @author Stephane Nicoll + * @since 6.2 */ -final class EmbeddedDatabaseConfigurerFactory { - - private EmbeddedDatabaseConfigurerFactory() { - } - +public abstract class EmbeddedDatabaseConfigurers { /** * Return a configurer instance for the given embedded database type. - * @param type the embedded database type (HSQL, H2 or Derby) + * @param type the {@linkplain EmbeddedDatabaseType embedded database type} * @return the configurer instance * @throws IllegalStateException if the driver for the specified database type is not available */ - public static EmbeddedDatabaseConfigurer getConfigurer(EmbeddedDatabaseType type) throws IllegalStateException { + public static EmbeddedDatabaseConfigurer getConfigurer(EmbeddedDatabaseType type) { Assert.notNull(type, "EmbeddedDatabaseType is required"); try { return switch (type) { case HSQL -> HsqlEmbeddedDatabaseConfigurer.getInstance(); case H2 -> H2EmbeddedDatabaseConfigurer.getInstance(); case DERBY -> DerbyEmbeddedDatabaseConfigurer.getInstance(); - default -> throw new UnsupportedOperationException("Embedded database type [" + type + "] is not supported"); }; } catch (ClassNotFoundException | NoClassDefFoundError ex) { @@ -54,4 +52,20 @@ final class EmbeddedDatabaseConfigurerFactory { } } + /** + * Customize the default configurer for the given embedded database type. The + * {@code customizer} operator typically uses + * {@link EmbeddedDatabaseConfigurerDelegate} to customize things as necessary. + * @param type the {@linkplain EmbeddedDatabaseType embedded database type} + * @param customizer the customizer to return based on the default + * @return the customized configurer instance + * @throws IllegalStateException if the driver for the specified database type is not available + */ + public static EmbeddedDatabaseConfigurer customizeConfigurer( + EmbeddedDatabaseType type, UnaryOperator customizer) { + + EmbeddedDatabaseConfigurer defaultConfigurer = getConfigurer(type); + return customizer.apply(defaultConfigurer); + } + } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java index abdbfe26af3..357e3f2eb06 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -45,9 +45,11 @@ import org.springframework.util.Assert; * for the database. *

  • Call {@link #setDatabaseName} to set an explicit name for the database. *
  • Call {@link #setDatabaseType} to set the database type if you wish to - * use one of the supported types. + * use one of the supported types with their default settings. *
  • Call {@link #setDatabaseConfigurer} to configure support for a custom - * embedded database type. + * embedded database type, or + * {@linkplain EmbeddedDatabaseConfigurers#customizeConfigurer customize} the + * default of a supported types. *
  • Call {@link #setDatabasePopulator} to change the algorithm used to * populate the database. *
  • Call {@link #setDataSourceFactory} to change the type of @@ -60,6 +62,7 @@ import org.springframework.util.Assert; * @author Keith Donald * @author Juergen Hoeller * @author Sam Brannen + * @author Stephane Nicoll * @since 3.0 */ public class EmbeddedDatabaseFactory { @@ -124,17 +127,23 @@ public class EmbeddedDatabaseFactory { /** * Set the type of embedded database to use. - *

    Call this when you wish to configure one of the pre-supported types. + *

    Call this when you wish to configure one of the pre-supported types + * with their default settings. *

    Defaults to HSQL. * @param type the database type */ public void setDatabaseType(EmbeddedDatabaseType type) { - this.databaseConfigurer = EmbeddedDatabaseConfigurerFactory.getConfigurer(type); + this.databaseConfigurer = EmbeddedDatabaseConfigurers.getConfigurer(type); } /** * Set the strategy that will be used to configure the embedded database instance. - *

    Call this when you wish to use an embedded database type not already supported. + *

    Call this with + * {@linkplain EmbeddedDatabaseConfigurers#customizeConfigurer customizeConfigurer} + * when you wish to customize the settings of one of the pre-supported types. + * Alternatively, use this when you wish to use an embedded database type not + * already supported. + * @since 6.2 */ public void setDatabaseConfigurer(EmbeddedDatabaseConfigurer configurer) { this.databaseConfigurer = configurer; @@ -178,7 +187,7 @@ public class EmbeddedDatabaseFactory { // Create the embedded database first if (this.databaseConfigurer == null) { - this.databaseConfigurer = EmbeddedDatabaseConfigurerFactory.getConfigurer(EmbeddedDatabaseType.HSQL); + this.databaseConfigurer = EmbeddedDatabaseConfigurers.getConfigurer(EmbeddedDatabaseType.HSQL); } this.databaseConfigurer.configureConnectionProperties( this.dataSourceFactory.getConnectionProperties(), this.databaseName); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java index 6dbe8d6f89d..eb1c41d27ac 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java @@ -121,6 +121,23 @@ class EmbeddedDatabaseBuilderTests { }); } + @Test + void setTypeConfigurerToCustomH2() { + doTwice(() -> { + EmbeddedDatabase db = builder + .setDatabaseConfigurer(EmbeddedDatabaseConfigurers.customizeConfigurer(H2, defaultConfigurer -> + new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, databaseName); + } + })) + .addScripts("db-schema.sql", "db-test-data.sql")// + .build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + @Test void setTypeToDerbyAndIgnoreFailedDrops() { doTwice(() -> { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java index ecb41f3e783..2486518b9ed 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java @@ -17,6 +17,7 @@ package org.springframework.jdbc.datasource.embedded; import java.sql.Connection; +import java.sql.SQLException; import org.junit.jupiter.api.Test; @@ -25,11 +26,14 @@ import org.springframework.jdbc.datasource.init.DatabasePopulator; import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link EmbeddedDatabaseFactory}. + * * @author Keith Donald + * @author Stephane Nicoll */ class EmbeddedDatabaseFactoryTests { - private EmbeddedDatabaseFactory factory = new EmbeddedDatabaseFactory(); + private final EmbeddedDatabaseFactory factory = new EmbeddedDatabaseFactory(); @Test @@ -41,6 +45,45 @@ class EmbeddedDatabaseFactoryTests { db.shutdown(); } + @Test + void customizeConfigurerWithAnotherDatabaseName() throws SQLException { + this.factory.setDatabaseName("original-db-mame"); + this.factory.setDatabaseConfigurer(EmbeddedDatabaseConfigurers.customizeConfigurer( + EmbeddedDatabaseType.H2, defaultConfigurer -> + new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, "custom-db-name"); + } + })); + EmbeddedDatabase db = this.factory.getDatabase(); + try (Connection connection = db.getConnection()) { + assertThat(connection.getMetaData().getURL()).contains("custom-db-name") + .doesNotContain("original-db-mame"); + } + db.shutdown(); + } + + @Test + void customizeConfigurerWithCustomizedUrl() throws SQLException { + this.factory.setDatabaseName("original-db-mame"); + this.factory.setDatabaseConfigurer(EmbeddedDatabaseConfigurers.customizeConfigurer( + EmbeddedDatabaseType.H2, defaultConfigurer -> + new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) { + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + super.configureConnectionProperties(properties, databaseName); + properties.setUrl("jdbc:h2:mem:custom-db-name;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=MariaDB"); + } + })); + EmbeddedDatabase db = this.factory.getDatabase(); + try (Connection connection = db.getConnection()) { + assertThat(connection.getMetaData().getURL()).contains("custom-db-name") + .doesNotContain("original-db-mame"); + } + db.shutdown(); + } + private static class StubDatabasePopulator implements DatabasePopulator {