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
This commit is contained in:
Stéphane Nicoll 2024-01-04 16:32:05 +01:00
parent bcccba50c1
commit 18ea43c905
7 changed files with 224 additions and 20 deletions

View File

@ -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

View File

@ -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.
* <p>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.

View File

@ -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);
}
}

View File

@ -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<EmbeddedDatabaseConfigurer> customizer) {
EmbeddedDatabaseConfigurer defaultConfigurer = getConfigurer(type);
return customizer.apply(defaultConfigurer);
}
}

View File

@ -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.
* <li>Call {@link #setDatabaseName} to set an explicit name for the database.
* <li>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.
* <li>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.
* <li>Call {@link #setDatabasePopulator} to change the algorithm used to
* populate the database.
* <li>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.
* <p>Call this when you wish to configure one of the pre-supported types.
* <p>Call this when you wish to configure one of the pre-supported types
* with their default settings.
* <p>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.
* <p>Call this when you wish to use an embedded database type not already supported.
* <p>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);

View File

@ -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(() -> {

View File

@ -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 {