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 <mkammerer@vmware.com>
Co-Authored-By: Phillip Webb <pwebb@vmware.com>
This commit is contained in:
Andy Wilkinson 2023-03-23 23:23:00 -07:00
parent d09ac00824
commit 61e9fe8cd4
7 changed files with 231 additions and 59 deletions

View File

@ -237,6 +237,7 @@ dependencies {
testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-core")
testImplementation("org.mockito:mockito-junit-jupiter") testImplementation("org.mockito:mockito-junit-jupiter")
testImplementation("org.postgresql:postgresql") testImplementation("org.postgresql:postgresql")
testImplementation("org.postgresql:r2dbc-postgresql")
testImplementation("org.skyscreamer:jsonassert") testImplementation("org.skyscreamer:jsonassert")
testImplementation("org.springframework:spring-test") testImplementation("org.springframework:spring-test")
testImplementation("org.springframework:spring-core-test") testImplementation("org.springframework:spring-core-test")

View File

@ -53,7 +53,7 @@ class ConnectionFactoryBeanCreationFailureAnalyzer
private String getDescription(ConnectionFactoryBeanCreationException cause) { private String getDescription(ConnectionFactoryBeanCreationException cause) {
StringBuilder description = new StringBuilder(); StringBuilder description = new StringBuilder();
description.append("Failed to configure a ConnectionFactory: "); 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("'url' attribute is not specified and ");
} }
description.append(String.format("no embedded database could be configured.%n")); description.append(String.format("no embedded database could be configured.%n"));

View File

@ -51,14 +51,18 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch * @author Mark Paluch
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Rodolpho S. Couto * @author Rodolpho S. Couto
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/ */
abstract class ConnectionFactoryConfigurations { abstract class ConnectionFactoryConfigurations {
protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties, ClassLoader classLoader, protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties,
R2dbcConnectionDetails connectionDetails, ClassLoader classLoader,
List<ConnectionFactoryOptionsBuilderCustomizer> optionsCustomizers) { List<ConnectionFactoryOptionsBuilderCustomizer> optionsCustomizers) {
try { try {
return org.springframework.boot.r2dbc.ConnectionFactoryBuilder return org.springframework.boot.r2dbc.ConnectionFactoryBuilder
.withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, .withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, connectionDetails,
() -> EmbeddedDatabaseConnection.get(classLoader))) () -> EmbeddedDatabaseConnection.get(classLoader)))
.configure((options) -> { .configure((options) -> {
for (ConnectionFactoryOptionsBuilderCustomizer optionsCustomizer : optionsCustomizers) { for (ConnectionFactoryOptionsBuilderCustomizer optionsCustomizer : optionsCustomizers) {
@ -87,10 +91,12 @@ abstract class ConnectionFactoryConfigurations {
static class PooledConnectionFactoryConfiguration { static class PooledConnectionFactoryConfiguration {
@Bean(destroyMethod = "dispose") @Bean(destroyMethod = "dispose")
ConnectionPool connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader, ConnectionPool connectionFactory(R2dbcProperties properties,
ObjectProvider<R2dbcConnectionDetails> connectionDetails, ResourceLoader resourceLoader,
ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers) { ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers) {
ConnectionFactory connectionFactory = createConnectionFactory(properties, ConnectionFactory connectionFactory = createConnectionFactory(properties,
resourceLoader.getClassLoader(), customizers.orderedStream().toList()); connectionDetails.getIfAvailable(), resourceLoader.getClassLoader(),
customizers.orderedStream().toList());
R2dbcProperties.Pool pool = properties.getPool(); R2dbcProperties.Pool pool = properties.getPool();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
ConnectionPoolConfiguration.Builder builder = ConnectionPoolConfiguration.builder(connectionFactory); ConnectionPoolConfiguration.Builder builder = ConnectionPoolConfiguration.builder(connectionFactory);
@ -116,10 +122,11 @@ abstract class ConnectionFactoryConfigurations {
static class GenericConfiguration { static class GenericConfiguration {
@Bean @Bean
ConnectionFactory connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader, ConnectionFactory connectionFactory(R2dbcProperties properties,
ObjectProvider<R2dbcConnectionDetails> connectionDetails, ResourceLoader resourceLoader,
ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers) { ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers) {
return createConnectionFactory(properties, resourceLoader.getClassLoader(), return createConnectionFactory(properties, connectionDetails.getIfAvailable(),
customizers.orderedStream().toList()); resourceLoader.getClassLoader(), customizers.orderedStream().toList());
} }
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,12 +16,10 @@
package org.springframework.boot.autoconfigure.r2dbc; package org.springframework.boot.autoconfigure.r2dbc;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.ConnectionFactoryOptions.Builder; import io.r2dbc.spi.ConnectionFactoryOptions.Builder;
import io.r2dbc.spi.Option;
import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection;
@ -31,6 +29,9 @@ import org.springframework.util.StringUtils;
* Initialize a {@link Builder} based on {@link R2dbcProperties}. * Initialize a {@link Builder} based on {@link R2dbcProperties}.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/ */
class ConnectionFactoryOptionsInitializer { class ConnectionFactoryOptionsInitializer {
@ -38,45 +39,31 @@ class ConnectionFactoryOptionsInitializer {
* Initialize a {@link Builder ConnectionFactoryOptions.Builder} using the specified * Initialize a {@link Builder ConnectionFactoryOptions.Builder} using the specified
* properties. * properties.
* @param properties the properties to use to initialize the builder * @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 * @param embeddedDatabaseConnection the embedded connection to use as a fallback
* @return an initialized builder * @return an initialized builder
* @throws ConnectionFactoryBeanCreationException if no suitable connection could be * @throws ConnectionFactoryBeanCreationException if no suitable connection could be
* determined * determined
*/ */
ConnectionFactoryOptions.Builder initialize(R2dbcProperties properties, ConnectionFactoryOptions.Builder initialize(R2dbcProperties properties, R2dbcConnectionDetails connectionDetails,
Supplier<EmbeddedDatabaseConnection> embeddedDatabaseConnection) { Supplier<EmbeddedDatabaseConnection> embeddedDatabaseConnection) {
if (StringUtils.hasText(properties.getUrl())) { if (connectionDetails != null) {
return initializeRegularOptions(properties); return connectionDetails.getConnectionFactoryOptions().mutate();
} }
EmbeddedDatabaseConnection embeddedConnection = embeddedDatabaseConnection.get(); EmbeddedDatabaseConnection embeddedConnection = embeddedDatabaseConnection.get();
if (embeddedConnection != EmbeddedDatabaseConnection.NONE) { if (embeddedConnection != EmbeddedDatabaseConnection.NONE) {
return initializeEmbeddedOptions(properties, embeddedConnection); 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); 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, private Builder initializeEmbeddedOptions(R2dbcProperties properties,
EmbeddedDatabaseConnection embeddedDatabaseConnection) { EmbeddedDatabaseConnection embeddedDatabaseConnection) {
String url = embeddedDatabaseConnection.getUrl(determineEmbeddedDatabaseName(properties)); String url = embeddedDatabaseConnection.getUrl(determineEmbeddedDatabaseName(properties));
if (url == null) { if (url == null) {
throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", url,
properties, embeddedDatabaseConnection); embeddedDatabaseConnection);
} }
Builder builder = ConnectionFactoryOptions.parse(url).mutate(); Builder builder = ConnectionFactoryOptions.parse(url).mutate();
String username = determineEmbeddedUsername(properties); String username = determineEmbeddedUsername(properties);
@ -89,6 +76,11 @@ class ConnectionFactoryOptionsInitializer {
return builder; return builder;
} }
private String determineEmbeddedDatabaseName(R2dbcProperties properties) {
String databaseName = determineDatabaseName(properties);
return (databaseName != null) ? databaseName : "testdb";
}
private String determineDatabaseName(R2dbcProperties properties) { private String determineDatabaseName(R2dbcProperties properties) {
if (properties.isGenerateUniqueName()) { if (properties.isGenerateUniqueName()) {
return properties.determineUniqueName(); return properties.determineUniqueName();
@ -99,30 +91,14 @@ class ConnectionFactoryOptionsInitializer {
return null; return null;
} }
private String determineEmbeddedDatabaseName(R2dbcProperties properties) {
String databaseName = determineDatabaseName(properties);
return (databaseName != null) ? databaseName : "testdb";
}
private String determineEmbeddedUsername(R2dbcProperties properties) { private String determineEmbeddedUsername(R2dbcProperties properties) {
String username = ifHasText(properties.getUsername()); String username = ifHasText(properties.getUsername());
return (username != null) ? username : "sa"; return (username != null) ? username : "sa";
} }
private <T extends CharSequence> void configureIf(Builder optionsBuilder, ConnectionFactoryOptions originalOptions,
Option<T> option, Supplier<T> valueSupplier, Predicate<T> setIf) {
if (originalOptions.hasOption(option)) {
return;
}
T value = valueSupplier.get();
if (setIf.test(value)) {
optionsBuilder.option(option, value);
}
}
private ConnectionFactoryBeanCreationException connectionFactoryBeanCreationException(String message, private ConnectionFactoryBeanCreationException connectionFactoryBeanCreationException(String message,
R2dbcProperties properties, EmbeddedDatabaseConnection embeddedDatabaseConnection) { String r2dbcUrl, EmbeddedDatabaseConnection embeddedDatabaseConnection) {
return new ConnectionFactoryBeanCreationException(message, properties, embeddedDatabaseConnection); return new ConnectionFactoryBeanCreationException(message, r2dbcUrl, embeddedDatabaseConnection);
} }
private String ifHasText(String candidate) { private String ifHasText(String candidate) {
@ -131,25 +107,25 @@ class ConnectionFactoryOptionsInitializer {
static class ConnectionFactoryBeanCreationException extends BeanCreationException { static class ConnectionFactoryBeanCreationException extends BeanCreationException {
private final R2dbcProperties properties; private final String url;
private final EmbeddedDatabaseConnection embeddedDatabaseConnection; private final EmbeddedDatabaseConnection embeddedDatabaseConnection;
ConnectionFactoryBeanCreationException(String message, R2dbcProperties properties, ConnectionFactoryBeanCreationException(String message, String url,
EmbeddedDatabaseConnection embeddedDatabaseConnection) { EmbeddedDatabaseConnection embeddedDatabaseConnection) {
super(message); super(message);
this.properties = properties; this.url = url;
this.embeddedDatabaseConnection = embeddedDatabaseConnection; this.embeddedDatabaseConnection = embeddedDatabaseConnection;
} }
String getUrl() {
return this.url;
}
EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() { EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() {
return this.embeddedDatabaseConnection; return this.embeddedDatabaseConnection;
} }
R2dbcProperties getProperties() {
return this.properties;
}
} }
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,16 +16,26 @@
package org.springframework.boot.autoconfigure.r2dbc; 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.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.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 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.condition.ConditionalOnResource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.util.StringUtils;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for R2DBC. * {@link EnableAutoConfiguration Auto-configuration} for R2DBC.
@ -42,4 +52,63 @@ import org.springframework.context.annotation.Import;
ConnectionFactoryConfigurations.GenericConfiguration.class, ConnectionFactoryDependentConfiguration.class }) ConnectionFactoryConfigurations.GenericConfiguration.class, ConnectionFactoryDependentConfiguration.class })
public class R2dbcAutoConfiguration { 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 <T extends CharSequence> void configureIf(Builder optionsBuilder,
ConnectionFactoryOptions originalOptions, Option<T> option, Supplier<T> valueSupplier,
Predicate<T> 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;
}
}
} }

View File

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

View File

@ -27,6 +27,7 @@ import io.r2dbc.h2.H2ConnectionFactory;
import io.r2dbc.pool.ConnectionPool; import io.r2dbc.pool.ConnectionPool;
import io.r2dbc.pool.PoolMetrics; import io.r2dbc.pool.PoolMetrics;
import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.ConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryProvider;
import io.r2dbc.spi.Option; import io.r2dbc.spi.Option;
import io.r2dbc.spi.Wrapped; import io.r2dbc.spi.Wrapped;
@ -54,6 +55,9 @@ import static org.assertj.core.api.Assertions.assertThat;
* *
* @author Mark Paluch * @author Mark Paluch
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/ */
class R2dbcAutoConfigurationTests { class R2dbcAutoConfigurationTests {
@ -68,7 +72,7 @@ class R2dbcAutoConfigurationTests {
assertThat(context.getBean(ConnectionPool.class)).extracting(ConnectionPool::unwrap) assertThat(context.getBean(ConnectionPool.class)).extracting(ConnectionPool::unwrap)
.satisfies((connectionFactory) -> assertThat(connectionFactory) .satisfies((connectionFactory) -> assertThat(connectionFactory)
.asInstanceOf(type(OptionsCapableConnectionFactory.class)) .asInstanceOf(type(OptionsCapableConnectionFactory.class))
.extracting(Wrapped<ConnectionFactory>::unwrap) .extracting(Wrapped::unwrap)
.isExactlyInstanceOf(H2ConnectionFactory.class)); .isExactlyInstanceOf(H2ConnectionFactory.class));
}); });
} }
@ -306,6 +310,64 @@ class R2dbcAutoConfigurationTests {
.doesNotHaveBean(DatabaseClient.class)); .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 <T> InstanceOfAssertFactory<T, ObjectAssert<T>> type(Class<T> type) { private <T> InstanceOfAssertFactory<T, ObjectAssert<T>> type(Class<T> type) {
return InstanceOfAssertFactories.type(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");
}
};
}
}
} }