From 61e9fe8cd44554f7a563112378755d0c664c3ef8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Mar 2023 23:23:00 -0700 Subject: [PATCH] Add ConnectionDetail support to R2DBC auto-configuration Update R2DBC auto-configuration so that `R2dbcConnectionDetails` beans may be optionally used to provide connection details. See gh-34657 Co-Authored-By: Mortitz Halbritter Co-Authored-By: Phillip Webb --- .../spring-boot-autoconfigure/build.gradle | 1 + ...ionFactoryBeanCreationFailureAnalyzer.java | 2 +- .../ConnectionFactoryConfigurations.java | 21 +++-- .../ConnectionFactoryOptionsInitializer.java | 74 ++++++----------- .../r2dbc/R2dbcAutoConfiguration.java | 71 +++++++++++++++- .../r2dbc/R2dbcConnectionDetails.java | 39 +++++++++ .../r2dbc/R2dbcAutoConfigurationTests.java | 82 ++++++++++++++++++- 7 files changed, 231 insertions(+), 59 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcConnectionDetails.java diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index f24f620518..dc5e917042 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -237,6 +237,7 @@ dependencies { testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-junit-jupiter") testImplementation("org.postgresql:postgresql") + testImplementation("org.postgresql:r2dbc-postgresql") testImplementation("org.skyscreamer:jsonassert") testImplementation("org.springframework:spring-test") testImplementation("org.springframework:spring-core-test") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java index c2833a6b26..31f9a46111 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java @@ -53,7 +53,7 @@ class ConnectionFactoryBeanCreationFailureAnalyzer private String getDescription(ConnectionFactoryBeanCreationException cause) { StringBuilder description = new StringBuilder(); description.append("Failed to configure a ConnectionFactory: "); - if (!StringUtils.hasText(cause.getProperties().getUrl())) { + if (!StringUtils.hasText(cause.getUrl())) { description.append("'url' attribute is not specified and "); } description.append(String.format("no embedded database could be configured.%n")); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java index 419af5ebf1..f1c2e9e213 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java @@ -51,14 +51,18 @@ import org.springframework.util.StringUtils; * @author Mark Paluch * @author Stephane Nicoll * @author Rodolpho S. Couto + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ abstract class ConnectionFactoryConfigurations { - protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties, ClassLoader classLoader, + protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties, + R2dbcConnectionDetails connectionDetails, ClassLoader classLoader, List optionsCustomizers) { try { return org.springframework.boot.r2dbc.ConnectionFactoryBuilder - .withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, + .withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, connectionDetails, () -> EmbeddedDatabaseConnection.get(classLoader))) .configure((options) -> { for (ConnectionFactoryOptionsBuilderCustomizer optionsCustomizer : optionsCustomizers) { @@ -87,10 +91,12 @@ abstract class ConnectionFactoryConfigurations { static class PooledConnectionFactoryConfiguration { @Bean(destroyMethod = "dispose") - ConnectionPool connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader, + ConnectionPool connectionFactory(R2dbcProperties properties, + ObjectProvider connectionDetails, ResourceLoader resourceLoader, ObjectProvider customizers) { ConnectionFactory connectionFactory = createConnectionFactory(properties, - resourceLoader.getClassLoader(), customizers.orderedStream().toList()); + connectionDetails.getIfAvailable(), resourceLoader.getClassLoader(), + customizers.orderedStream().toList()); R2dbcProperties.Pool pool = properties.getPool(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); ConnectionPoolConfiguration.Builder builder = ConnectionPoolConfiguration.builder(connectionFactory); @@ -116,10 +122,11 @@ abstract class ConnectionFactoryConfigurations { static class GenericConfiguration { @Bean - ConnectionFactory connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader, + ConnectionFactory connectionFactory(R2dbcProperties properties, + ObjectProvider connectionDetails, ResourceLoader resourceLoader, ObjectProvider customizers) { - return createConnectionFactory(properties, resourceLoader.getClassLoader(), - customizers.orderedStream().toList()); + return createConnectionFactory(properties, connectionDetails.getIfAvailable(), + resourceLoader.getClassLoader(), customizers.orderedStream().toList()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java index 6a17dfde1c..5bc1eed2cc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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,12 +16,10 @@ package org.springframework.boot.autoconfigure.r2dbc; -import java.util.function.Predicate; import java.util.function.Supplier; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryOptions.Builder; -import io.r2dbc.spi.Option; import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; @@ -31,6 +29,9 @@ import org.springframework.util.StringUtils; * Initialize a {@link Builder} based on {@link R2dbcProperties}. * * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class ConnectionFactoryOptionsInitializer { @@ -38,45 +39,31 @@ class ConnectionFactoryOptionsInitializer { * Initialize a {@link Builder ConnectionFactoryOptions.Builder} using the specified * properties. * @param properties the properties to use to initialize the builder + * @param connectionDetails the connection details to use to initialize the builder * @param embeddedDatabaseConnection the embedded connection to use as a fallback * @return an initialized builder * @throws ConnectionFactoryBeanCreationException if no suitable connection could be * determined */ - ConnectionFactoryOptions.Builder initialize(R2dbcProperties properties, + ConnectionFactoryOptions.Builder initialize(R2dbcProperties properties, R2dbcConnectionDetails connectionDetails, Supplier embeddedDatabaseConnection) { - if (StringUtils.hasText(properties.getUrl())) { - return initializeRegularOptions(properties); + if (connectionDetails != null) { + return connectionDetails.getConnectionFactoryOptions().mutate(); } EmbeddedDatabaseConnection embeddedConnection = embeddedDatabaseConnection.get(); if (embeddedConnection != EmbeddedDatabaseConnection.NONE) { return initializeEmbeddedOptions(properties, embeddedConnection); } - throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", properties, + throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", null, embeddedConnection); } - private ConnectionFactoryOptions.Builder initializeRegularOptions(R2dbcProperties properties) { - ConnectionFactoryOptions urlOptions = ConnectionFactoryOptions.parse(properties.getUrl()); - Builder optionsBuilder = urlOptions.mutate(); - configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.USER, properties::getUsername, - StringUtils::hasText); - configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.PASSWORD, properties::getPassword, - StringUtils::hasText); - configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.DATABASE, - () -> determineDatabaseName(properties), StringUtils::hasText); - if (properties.getProperties() != null) { - properties.getProperties().forEach((key, value) -> optionsBuilder.option(Option.valueOf(key), value)); - } - return optionsBuilder; - } - private Builder initializeEmbeddedOptions(R2dbcProperties properties, EmbeddedDatabaseConnection embeddedDatabaseConnection) { String url = embeddedDatabaseConnection.getUrl(determineEmbeddedDatabaseName(properties)); if (url == null) { - throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", - properties, embeddedDatabaseConnection); + throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", url, + embeddedDatabaseConnection); } Builder builder = ConnectionFactoryOptions.parse(url).mutate(); String username = determineEmbeddedUsername(properties); @@ -89,6 +76,11 @@ class ConnectionFactoryOptionsInitializer { return builder; } + private String determineEmbeddedDatabaseName(R2dbcProperties properties) { + String databaseName = determineDatabaseName(properties); + return (databaseName != null) ? databaseName : "testdb"; + } + private String determineDatabaseName(R2dbcProperties properties) { if (properties.isGenerateUniqueName()) { return properties.determineUniqueName(); @@ -99,30 +91,14 @@ class ConnectionFactoryOptionsInitializer { return null; } - private String determineEmbeddedDatabaseName(R2dbcProperties properties) { - String databaseName = determineDatabaseName(properties); - return (databaseName != null) ? databaseName : "testdb"; - } - private String determineEmbeddedUsername(R2dbcProperties properties) { String username = ifHasText(properties.getUsername()); return (username != null) ? username : "sa"; } - private void configureIf(Builder optionsBuilder, ConnectionFactoryOptions originalOptions, - Option option, Supplier valueSupplier, Predicate setIf) { - if (originalOptions.hasOption(option)) { - return; - } - T value = valueSupplier.get(); - if (setIf.test(value)) { - optionsBuilder.option(option, value); - } - } - private ConnectionFactoryBeanCreationException connectionFactoryBeanCreationException(String message, - R2dbcProperties properties, EmbeddedDatabaseConnection embeddedDatabaseConnection) { - return new ConnectionFactoryBeanCreationException(message, properties, embeddedDatabaseConnection); + String r2dbcUrl, EmbeddedDatabaseConnection embeddedDatabaseConnection) { + return new ConnectionFactoryBeanCreationException(message, r2dbcUrl, embeddedDatabaseConnection); } private String ifHasText(String candidate) { @@ -131,25 +107,25 @@ class ConnectionFactoryOptionsInitializer { static class ConnectionFactoryBeanCreationException extends BeanCreationException { - private final R2dbcProperties properties; + private final String url; private final EmbeddedDatabaseConnection embeddedDatabaseConnection; - ConnectionFactoryBeanCreationException(String message, R2dbcProperties properties, + ConnectionFactoryBeanCreationException(String message, String url, EmbeddedDatabaseConnection embeddedDatabaseConnection) { super(message); - this.properties = properties; + this.url = url; this.embeddedDatabaseConnection = embeddedDatabaseConnection; } + String getUrl() { + return this.url; + } + EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() { return this.embeddedDatabaseConnection; } - R2dbcProperties getProperties() { - return this.properties; - } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java index d320d1ae88..7dc72964c7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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,16 +16,26 @@ package org.springframework.boot.autoconfigure.r2dbc; +import java.util.function.Predicate; +import java.util.function.Supplier; + import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryOptions.Builder; +import io.r2dbc.spi.Option; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for R2DBC. @@ -42,4 +52,63 @@ import org.springframework.context.annotation.Import; ConnectionFactoryConfigurations.GenericConfiguration.class, ConnectionFactoryDependentConfiguration.class }) public class R2dbcAutoConfiguration { + @Bean + @ConditionalOnMissingBean(R2dbcConnectionDetails.class) + @ConditionalOnProperty("spring.r2dbc.url") + PropertiesR2dbcConnectionDetails propertiesR2dbcConnectionDetails(R2dbcProperties properties) { + return new PropertiesR2dbcConnectionDetails(properties); + } + + /** + * Adapts {@link R2dbcProperties} to {@link R2dbcConnectionDetails}. + */ + static class PropertiesR2dbcConnectionDetails implements R2dbcConnectionDetails { + + private final R2dbcProperties properties; + + PropertiesR2dbcConnectionDetails(R2dbcProperties properties) { + this.properties = properties; + } + + @Override + public ConnectionFactoryOptions getConnectionFactoryOptions() { + ConnectionFactoryOptions urlOptions = ConnectionFactoryOptions.parse(this.properties.getUrl()); + Builder optionsBuilder = urlOptions.mutate(); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.USER, this.properties::getUsername, + StringUtils::hasText); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.PASSWORD, this.properties::getPassword, + StringUtils::hasText); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.DATABASE, + () -> determineDatabaseName(this.properties), StringUtils::hasText); + if (this.properties.getProperties() != null) { + this.properties.getProperties() + .forEach((key, value) -> optionsBuilder.option(Option.valueOf(key), value)); + } + return optionsBuilder.build(); + } + + private void configureIf(Builder optionsBuilder, + ConnectionFactoryOptions originalOptions, Option option, Supplier valueSupplier, + Predicate setIf) { + if (originalOptions.hasOption(option)) { + return; + } + T value = valueSupplier.get(); + if (setIf.test(value)) { + optionsBuilder.option(option, value); + } + } + + private String determineDatabaseName(R2dbcProperties properties) { + if (properties.isGenerateUniqueName()) { + return properties.determineUniqueName(); + } + if (StringUtils.hasLength(properties.getName())) { + return properties.getName(); + } + return null; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcConnectionDetails.java new file mode 100644 index 0000000000..c2230a5c61 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcConnectionDetails.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2023 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.r2dbc; + +import io.r2dbc.spi.ConnectionFactoryOptions; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an SQL service using R2DBC. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface R2dbcConnectionDetails extends ConnectionDetails { + + /** + * Connection factory options for connecting to the database. + * @return the connection factory options + */ + ConnectionFactoryOptions getConnectionFactoryOptions(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java index ee564a8954..43aad72589 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java @@ -27,6 +27,7 @@ import io.r2dbc.h2.H2ConnectionFactory; import io.r2dbc.pool.ConnectionPool; import io.r2dbc.pool.PoolMetrics; import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryProvider; import io.r2dbc.spi.Option; import io.r2dbc.spi.Wrapped; @@ -54,6 +55,9 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Mark Paluch * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb */ class R2dbcAutoConfigurationTests { @@ -68,7 +72,7 @@ class R2dbcAutoConfigurationTests { assertThat(context.getBean(ConnectionPool.class)).extracting(ConnectionPool::unwrap) .satisfies((connectionFactory) -> assertThat(connectionFactory) .asInstanceOf(type(OptionsCapableConnectionFactory.class)) - .extracting(Wrapped::unwrap) + .extracting(Wrapped::unwrap) .isExactlyInstanceOf(H2ConnectionFactory.class)); }); } @@ -306,6 +310,64 @@ class R2dbcAutoConfigurationTests { .doesNotHaveBean(DatabaseClient.class)); } + @Test + void shouldUseCustomConnectionDetailsIfAvailable() { + this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false") + .withUserConfiguration(ConnectionDetailsConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class); + OptionsCapableConnectionFactory connectionFactory = context + .getBean(OptionsCapableConnectionFactory.class); + ConnectionFactoryOptions options = connectionFactory.getOptions(); + assertThat(options.getValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql"); + assertThat(options.getValue(ConnectionFactoryOptions.HOST)).isEqualTo("postgres.example.com"); + assertThat(options.getValue(ConnectionFactoryOptions.PORT)).isEqualTo(12345); + assertThat(options.getValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("database-1"); + assertThat(options.getValue(ConnectionFactoryOptions.USER)).isEqualTo("user-1"); + assertThat(options.getValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("password-1"); + }); + } + + @Test + void configureWithUsernamePasswordAndUrlWithoutUserInfoUsesUsernameAndPassword() { + this.contextRunner + .withPropertyValues("spring.r2dbc.pool.enabled=false", + "spring.r2dbc.url:r2dbc:postgresql://postgres.example.com:4321/db", "spring.r2dbc.username=alice", + "spring.r2dbc.password=secret") + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class); + OptionsCapableConnectionFactory connectionFactory = context + .getBean(OptionsCapableConnectionFactory.class); + ConnectionFactoryOptions options = connectionFactory.getOptions(); + assertThat(options.getValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql"); + assertThat(options.getValue(ConnectionFactoryOptions.HOST)).isEqualTo("postgres.example.com"); + assertThat(options.getValue(ConnectionFactoryOptions.PORT)).isEqualTo(4321); + assertThat(options.getValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("db"); + assertThat(options.getValue(ConnectionFactoryOptions.USER)).isEqualTo("alice"); + assertThat(options.getValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + }); + } + + @Test + void configureWithUsernamePasswordAndUrlWithUserInfoUsesUserInfo() { + this.contextRunner + .withPropertyValues("spring.r2dbc.pool.enabled=false", + "spring.r2dbc.url:r2dbc:postgresql://bob:password@postgres.example.com:9876/db", + "spring.r2dbc.username=alice", "spring.r2dbc.password=secret") + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class); + OptionsCapableConnectionFactory connectionFactory = context + .getBean(OptionsCapableConnectionFactory.class); + ConnectionFactoryOptions options = connectionFactory.getOptions(); + assertThat(options.getValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql"); + assertThat(options.getValue(ConnectionFactoryOptions.HOST)).isEqualTo("postgres.example.com"); + assertThat(options.getValue(ConnectionFactoryOptions.PORT)).isEqualTo(9876); + assertThat(options.getValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("db"); + assertThat(options.getValue(ConnectionFactoryOptions.USER)).isEqualTo("bob"); + assertThat(options.getValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("password"); + }); + } + private InstanceOfAssertFactory> type(Class type) { return InstanceOfAssertFactories.type(type); } @@ -342,4 +404,22 @@ class R2dbcAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + R2dbcConnectionDetails r2dbcConnectionDetails() { + return new R2dbcConnectionDetails() { + + @Override + public ConnectionFactoryOptions getConnectionFactoryOptions() { + return ConnectionFactoryOptions + .parse("r2dbc:postgresql://user-1:password-1@postgres.example.com:12345/database-1"); + } + + }; + } + + } + }