Commit 5c174feb authored by Stephane Nicoll's avatar Stephane Nicoll

Add auto-configuration for R2DBC's ConnectionFactory

This commit adds auto-configuration for R2DBC. If R2DBC is on the
classpath, a `ConnectionFactory` is created similarly to the algorithm
used to create a `DataSource`.

If an url is specified, it is used to determine the R2DBC driver and
database location. If not, an embedded database is started (with only
support of H2 via r2dbc-h2). If none of those succeed, an exception is
thrown that is handled by a dedicated FailureAnalyzer.

To clearly separate reactive from imperative access, a `DataSource` is
not auto-configured if a `ConnectionFactory` is present. This makes sure
that any auto-configuration that relies on the presence of a
`DataSource` backs off.

There is no dedicated database initialization at the moment but it is
possible to configure flyway or liquibase to create a local `DataSource`
for the duration of the migration. Alternatively, if Spring Data R2DBC
is on the classpath, a `ResourceDatabasePopulator` bean can be defined
with the scripts to execute on startup.

See gh-19988
Co-authored-by: 's avatarMark Paluch <mpaluch@pivotal.io>
parent 4c2ff9c3
...@@ -78,7 +78,7 @@ public class DocumentConfigurationProperties extends AbstractTask { ...@@ -78,7 +78,7 @@ public class DocumentConfigurationProperties extends AbstractTask {
.addSection("data-migration").withKeyPrefixes("spring.flyway", "spring.liquibase").addSection("data") .addSection("data-migration").withKeyPrefixes("spring.flyway", "spring.liquibase").addSection("data")
.withKeyPrefixes("spring.couchbase", "spring.elasticsearch", "spring.h2", "spring.influx", .withKeyPrefixes("spring.couchbase", "spring.elasticsearch", "spring.h2", "spring.influx",
"spring.mongodb", "spring.redis", "spring.dao", "spring.data", "spring.datasource", "spring.mongodb", "spring.redis", "spring.dao", "spring.data", "spring.datasource",
"spring.jooq", "spring.jdbc", "spring.jpa") "spring.jooq", "spring.jdbc", "spring.jpa", "spring.r2dbc")
.addOverride("spring.datasource.dbcp2", "Commons DBCP2 specific settings") .addOverride("spring.datasource.dbcp2", "Commons DBCP2 specific settings")
.addOverride("spring.datasource.tomcat", "Tomcat datasource specific settings") .addOverride("spring.datasource.tomcat", "Tomcat datasource specific settings")
.addOverride("spring.datasource.hikari", "Hikari specific settings").addSection("transaction") .addOverride("spring.datasource.hikari", "Hikari specific settings").addSection("transaction")
......
...@@ -33,6 +33,8 @@ dependencies { ...@@ -33,6 +33,8 @@ dependencies {
optional("de.flapdoodle.embed:de.flapdoodle.embed.mongo") optional("de.flapdoodle.embed:de.flapdoodle.embed.mongo")
optional("io.lettuce:lettuce-core") optional("io.lettuce:lettuce-core")
optional("io.projectreactor.netty:reactor-netty") optional("io.projectreactor.netty:reactor-netty")
optional("io.r2dbc:r2dbc-spi")
optional("io.r2dbc:r2dbc-pool")
optional("io.rsocket:rsocket-core") optional("io.rsocket:rsocket-core")
optional("io.rsocket:rsocket-transport-netty") optional("io.rsocket:rsocket-transport-netty")
optional("io.undertow:undertow-servlet") { optional("io.undertow:undertow-servlet") {
...@@ -156,6 +158,7 @@ dependencies { ...@@ -156,6 +158,7 @@ dependencies {
testImplementation("com.jayway.jsonpath:json-path") testImplementation("com.jayway.jsonpath:json-path")
testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("com.squareup.okhttp3:mockwebserver")
testImplementation("com.sun.xml.messaging.saaj:saaj-impl") testImplementation("com.sun.xml.messaging.saaj:saaj-impl")
testImplementation("io.r2dbc:r2dbc-h2")
testImplementation("jakarta.json:jakarta.json-api") testImplementation("jakarta.json:jakarta.json-api")
testImplementation("jakarta.xml.ws:jakarta.xml.ws-api") testImplementation("jakarta.xml.ws:jakarta.xml.ws-api")
testImplementation("mysql:mysql-connector-java") testImplementation("mysql:mysql-connector-java")
......
...@@ -51,6 +51,7 @@ import org.springframework.util.StringUtils; ...@@ -51,6 +51,7 @@ import org.springframework.util.StringUtils;
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class) @EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class }) @Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration { public class DataSourceAutoConfiguration {
......
/*
* Copyright 2012-2020 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 org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBuilder.ConnectionFactoryBeanCreationException;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* An {@link AbstractFailureAnalyzer} for failures caused by a
* {@link ConnectionFactoryBeanCreationException}.
*
* @author Mark Paluch
*/
class ConnectionFactoryBeanCreationFailureAnalyzer
extends AbstractFailureAnalyzer<ConnectionFactoryBeanCreationException> implements EnvironmentAware {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
protected FailureAnalysis analyze(Throwable rootFailure, ConnectionFactoryBeanCreationException cause) {
return getFailureAnalysis(cause);
}
private FailureAnalysis getFailureAnalysis(ConnectionFactoryBeanCreationException cause) {
String description = getDescription(cause);
String action = getAction(cause);
return new FailureAnalysis(description, action, cause);
}
private String getDescription(ConnectionFactoryBeanCreationException cause) {
StringBuilder description = new StringBuilder();
description.append("Failed to configure a ConnectionFactory: ");
if (!StringUtils.hasText(cause.getProperties().getUrl())) {
description.append("'url' attribute is not specified and ");
}
description.append(String.format("no embedded database could be configured.%n"));
description.append(String.format("%nReason: %s%n", cause.getMessage()));
return description.toString();
}
private String getAction(ConnectionFactoryBeanCreationException cause) {
StringBuilder action = new StringBuilder();
action.append(String.format("Consider the following:%n"));
if (EmbeddedDatabaseConnection.NONE == cause.getEmbeddedDatabaseConnection()) {
action.append(String.format("\tIf you want an embedded database (H2), please put it on the classpath.%n"));
}
else {
action.append(String.format("\tReview the configuration of %s%n.", cause.getEmbeddedDatabaseConnection()));
}
action.append("\tIf you have database settings to be loaded from a particular "
+ "profile you may need to activate it").append(getActiveProfiles());
return action.toString();
}
private String getActiveProfiles() {
StringBuilder message = new StringBuilder();
String[] profiles = this.environment.getActiveProfiles();
if (ObjectUtils.isEmpty(profiles)) {
message.append(" (no profiles are currently active).");
}
else {
message.append(" (the profiles ");
message.append(StringUtils.arrayToCommaDelimitedString(profiles));
message.append(" are currently active).");
}
return message.toString();
}
}
/*
* Copyright 2012-2020 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 java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
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.util.StringUtils;
/**
* Builder for {@link ConnectionFactory}.
*
* @author Mark Paluch
* @author Tadaya Tsuyukubo
* @author Stephane Nicoll
* @since 2.3.0
*/
public final class ConnectionFactoryBuilder {
private final ConnectionFactoryOptions.Builder optionsBuilder;
private ConnectionFactoryBuilder(ConnectionFactoryOptions.Builder optionsBuilder) {
this.optionsBuilder = optionsBuilder;
}
/**
* Initialize a new {@link ConnectionFactoryBuilder} based on the specified
* {@link R2dbcProperties}. If no url is specified, the
* {@link EmbeddedDatabaseConnection} supplier is invoked to determine if an embedded
* database can be configured instead.
* @param properties the properties to use to initialize the builder
* @param embeddedDatabaseConnection a supplier for an
* {@link EmbeddedDatabaseConnection}
* @return a new builder initialized with the settings defined in
* {@link R2dbcProperties}
*/
public static ConnectionFactoryBuilder of(R2dbcProperties properties,
Supplier<EmbeddedDatabaseConnection> embeddedDatabaseConnection) {
return new ConnectionFactoryBuilder(
new ConnectionFactoryOptionsInitializer().initializeOptions(properties, embeddedDatabaseConnection));
}
/**
* Configure additional options.
* @param options a {@link Consumer} to customize the options
* @return this for method chaining
*/
public ConnectionFactoryBuilder configure(Consumer<Builder> options) {
options.accept(this.optionsBuilder);
return this;
}
/**
* Configure the {@linkplain ConnectionFactoryOptions#USER username}.
* @param username the connection factory username
* @return this for method chaining
*/
public ConnectionFactoryBuilder username(String username) {
return configure((options) -> options.option(ConnectionFactoryOptions.USER, username));
}
/**
* Configure the {@linkplain ConnectionFactoryOptions#PASSWORD password}.
* @param password the connection factory password
* @return this for method chaining
*/
public ConnectionFactoryBuilder password(CharSequence password) {
return configure((options) -> options.option(ConnectionFactoryOptions.PASSWORD, password));
}
/**
* Configure the {@linkplain ConnectionFactoryOptions#HOST host name}.
* @param host the connection factory hostname
* @return this for method chaining
*/
public ConnectionFactoryBuilder hostname(String host) {
return configure((options) -> options.option(ConnectionFactoryOptions.HOST, host));
}
/**
* Configure the {@linkplain ConnectionFactoryOptions#PORT port}.
* @param port the connection factory port
* @return this for method chaining
*/
public ConnectionFactoryBuilder port(int port) {
return configure((options) -> options.option(ConnectionFactoryOptions.PORT, port));
}
/**
* Configure the {@linkplain ConnectionFactoryOptions#DATABASE database}.
* @param database the connection factory database
* @return this for method chaining
*/
public ConnectionFactoryBuilder database(String database) {
return configure((options) -> options.option(ConnectionFactoryOptions.DATABASE, database));
}
/**
* Build a {@link ConnectionFactory} based on the state of this builder.
* @return a connection factory
*/
public ConnectionFactory build() {
return ConnectionFactories.get(buildOptions());
}
/**
* Build a {@link ConnectionFactoryOptions} based on the state of this builder.
* @return the options
*/
public ConnectionFactoryOptions buildOptions() {
return this.optionsBuilder.build();
}
static class ConnectionFactoryOptionsInitializer {
/**
* Initialize a {@link ConnectionFactoryOptions.Builder} using the specified
* properties.
* @param properties the properties 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 initializeOptions(R2dbcProperties properties,
Supplier<EmbeddedDatabaseConnection> embeddedDatabaseConnection) {
if (StringUtils.hasText(properties.getUrl())) {
return initializeRegularOptions(properties);
}
EmbeddedDatabaseConnection embeddedConnection = embeddedDatabaseConnection.get();
if (embeddedConnection != EmbeddedDatabaseConnection.NONE) {
return initializeEmbeddedOptions(properties, embeddedConnection);
}
throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL",
properties, 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 ConnectionFactoryOptions.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);
}
Builder builder = ConnectionFactoryOptions.parse(url).mutate();
String username = determineEmbeddedUsername(properties);
if (StringUtils.hasText(username)) {
builder.option(ConnectionFactoryOptions.USER, username);
}
if (StringUtils.hasText(properties.getPassword())) {
builder.option(ConnectionFactoryOptions.PASSWORD, properties.getPassword());
}
return builder;
}
private String determineDatabaseName(R2dbcProperties properties) {
if (properties.isGenerateUniqueName()) {
return properties.determineUniqueName();
}
if (StringUtils.hasLength(properties.getName())) {
return properties.getName();
}
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 <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,
R2dbcProperties properties, EmbeddedDatabaseConnection embeddedDatabaseConnection) {
return new ConnectionFactoryBeanCreationException(message, properties, embeddedDatabaseConnection);
}
private String ifHasText(String candidate) {
return (StringUtils.hasText(candidate)) ? candidate : null;
}
}
static class ConnectionFactoryBeanCreationException extends BeanCreationException {
private final R2dbcProperties properties;
private final EmbeddedDatabaseConnection embeddedDatabaseConnection;
ConnectionFactoryBeanCreationException(String message, R2dbcProperties properties,
EmbeddedDatabaseConnection embeddedDatabaseConnection) {
super(message);
this.properties = properties;
this.embeddedDatabaseConnection = embeddedDatabaseConnection;
}
EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() {
return this.embeddedDatabaseConnection;
}
R2dbcProperties getProperties() {
return this.properties;
}
}
}
/*
* Copyright 2012-2020 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 java.util.List;
import java.util.stream.Collectors;
import io.r2dbc.pool.ConnectionPool;
import io.r2dbc.pool.ConnectionPoolConfiguration;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
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.SpringBootCondition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.StringUtils;
/**
* Actual {@link ConnectionFactory} configurations.
*
* @author Mark Paluch
* @author Stephane Nicoll
*/
abstract class ConnectionFactoryConfigurations {
protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties, ClassLoader classLoader,
List<ConnectionFactoryOptionsBuilderCustomizer> optionsCustomizers) {
return ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.get(classLoader))
.configure((options) -> {
for (ConnectionFactoryOptionsBuilderCustomizer optionsCustomizer : optionsCustomizers) {
optionsCustomizer.customize(options);
}
}).build();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ConnectionPool.class)
@Conditional(PooledConnectionFactoryCondition.class)
@ConditionalOnMissingBean(ConnectionFactory.class)
static class Pool {
@Bean(destroyMethod = "dispose")
ConnectionPool connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader,
ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers) {
ConnectionFactory connectionFactory = createConnectionFactory(properties, resourceLoader.getClassLoader(),
customizers.orderedStream().collect(Collectors.toList()));
R2dbcProperties.Pool pool = properties.getPool();
ConnectionPoolConfiguration.Builder builder = ConnectionPoolConfiguration.builder(connectionFactory)
.maxSize(pool.getMaxSize()).initialSize(pool.getInitialSize()).maxIdleTime(pool.getMaxIdleTime());
if (StringUtils.hasText(pool.getValidationQuery())) {
builder.validationQuery(pool.getValidationQuery());
}
return new ConnectionPool(builder.build());
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.r2dbc.pool", value = "enabled", havingValue = "false",
matchIfMissing = true)
@ConditionalOnMissingBean(ConnectionFactory.class)
static class Generic {
@Bean
ConnectionFactory connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader,
ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers) {
return createConnectionFactory(properties, resourceLoader.getClassLoader(),
customizers.orderedStream().collect(Collectors.toList()));
}
}
/**
* {@link Condition} that checks that a {@link ConnectionPool} is requested. The
* condition matches if pooling was opt-in via configuration and the r2dbc url does
* not contain pooling-related options.
*/
static class PooledConnectionFactoryCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean poolEnabled = context.getEnvironment().getProperty("spring.r2dbc.pool.enabled", Boolean.class,
true);
if (poolEnabled) {
// Make sure the URL does not have pool options
String url = context.getEnvironment().getProperty("spring.r2dbc.url");
boolean pooledUrl = StringUtils.hasText(url) && url.contains(":pool:");
if (pooledUrl) {
return ConditionOutcome.noMatch("R2DBC Connection URL contains pooling-related options");
}
return ConditionOutcome
.match("Pooling is enabled and R2DBC Connection URL does not contain pooling-related options");
}
return ConditionOutcome.noMatch("Pooling is disabled");
}
}
}
/*
* Copyright 2012-2020 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 io.r2dbc.spi.ConnectionFactoryOptions.Builder;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link ConnectionFactoryOptions} via a {@link Builder} whilst retaining default
* auto-configuration.whilst retaining default auto-configuration.
*
* @author Mark Paluch
* @since 2.3.0
*/
@FunctionalInterface
public interface ConnectionFactoryOptionsBuilderCustomizer {
/**
* Customize the {@link Builder}.
* @param builder the builder to customize
*/
void customize(Builder builder);
}
/*
* Copyright 2012-2020 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 org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Connection details for embedded databases compatible with r2dbc.
*
* @author Mark Paluch
* @author Stephane Nicoll
* @since 2.3.0
*/
public enum EmbeddedDatabaseConnection {
/**
* No Connection.
*/
NONE(null, null, null),
/**
* H2 Database Connection.
*/
H2("H2", "io.r2dbc.h2.H2ConnectionFactoryProvider",
"r2dbc:h2:mem://in-memory/%s?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
private final String type;
private final String driverClassName;
private final String url;
EmbeddedDatabaseConnection(String type, String driverClassName, String url) {
this.type = type;
this.driverClassName = driverClassName;
this.url = url;
}
/**
* Returns the driver class name.
* @return the driver class name
*/
public String getDriverClassName() {
return this.driverClassName;
}
/**
* Returns the embedded database type name for the connection.
* @return the database type
*/
public String getType() {
return this.type;
}
/**
* Returns the R2DBC URL for the connection using the specified {@code databaseName}.
* @param databaseName the name of the database
* @return the connection URL
*/
public String getUrl(String databaseName) {
Assert.hasText(databaseName, "DatabaseName must not be empty");
return (this.url != null) ? String.format(this.url, databaseName) : null;
}
/**
* Returns the most suitable {@link EmbeddedDatabaseConnection} for the given class
* loader.
* @param classLoader the class loader used to check for classes
* @return an {@link EmbeddedDatabaseConnection} or {@link #NONE}.
*/
public static EmbeddedDatabaseConnection get(ClassLoader classLoader) {
for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) {
if (candidate != NONE && ClassUtils.isPresent(candidate.getDriverClassName(), classLoader)) {
return candidate;
}
}
return NONE;
}
}
/*
* Copyright 2012-2020 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.ConnectionFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* {@link EnableAutoConfiguration Auto-configuration} for R2DBC.
*
* @author Mark Paluch
* @author Stephane Nicoll
* @since 2.3.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ConnectionFactory.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(R2dbcProperties.class)
@Import({ ConnectionFactoryConfigurations.Pool.class, ConnectionFactoryConfigurations.Generic.class })
public class R2dbcAutoConfiguration {
}
/*
* Copyright 2012-2020 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 java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for R2DBC.
*
* @author Mark Paluch
* @author Andreas Killaitis
* @author Stephane Nicoll
* @since 2.3.0
*/
@ConfigurationProperties(prefix = "spring.r2dbc")
public class R2dbcProperties {
/**
* Database name. Set if no name is specified in the url. Default to "testdb" when
* using an embedded database.
*/
private String name;
/**
* Whether to generate a random database name. Ignore any configured name when
* enabled.
*/
private boolean generateUniqueName;
/**
* R2DBC URL of the database. database name, username, password and pooling options
* specified in the url take precedence over individual options.
*/
private String url;
/**
* Login username of the database. Set if no username is specified in the url.
*/
private String username;
/**
* Login password of the database. Set if no password is specified in the url.
*/
private String password;
/**
* Additional R2DBC options.
*/
private final Map<String, String> properties = new LinkedHashMap<>();
private final Pool pool = new Pool();
private String uniqueName;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public boolean isGenerateUniqueName() {
return this.generateUniqueName;
}
public void setGenerateUniqueName(boolean generateUniqueName) {
this.generateUniqueName = generateUniqueName;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public Map<String, String> getProperties() {
return this.properties;
}
public Pool getPool() {
return this.pool;
}
/**
* Provide a unique name specific to this instance. Calling this method several times
* return the same unique name.
* @return a unique name for this instance
*/
public String determineUniqueName() {
if (this.uniqueName == null) {
this.uniqueName = UUID.randomUUID().toString();
}
return this.uniqueName;
}
public static class Pool {
/**
* Idle timeout.
*/
private Duration maxIdleTime = Duration.ofMinutes(30);
/**
* Initial connection pool size.
*/
private int initialSize = 10;
/**
* Maximal connection pool size.
*/
private int maxSize = 10;
/**
* Validation query.
*/
private String validationQuery;
public Duration getMaxIdleTime() {
return this.maxIdleTime;
}
public void setMaxIdleTime(Duration maxIdleTime) {
this.maxIdleTime = maxIdleTime;
}
public int getInitialSize() {
return this.initialSize;
}
public void setInitialSize(int initialSize) {
this.initialSize = initialSize;
}
public int getMaxSize() {
return this.maxSize;
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public String getValidationQuery() {
return this.validationQuery;
}
public void setValidationQuery(String validationQuery) {
this.validationQuery = validationQuery;
}
}
}
/*
* Copyright 2012-2020 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.
*/
/**
* Auto-Configuration for R2DBC.
*/
package org.springframework.boot.autoconfigure.r2dbc;
...@@ -813,6 +813,11 @@ ...@@ -813,6 +813,11 @@
"name": "spring.quartz.scheduler-name", "name": "spring.quartz.scheduler-name",
"defaultValue": "quartzScheduler" "defaultValue": "quartzScheduler"
}, },
{
"name": "spring.r2dbc.pool.enabled",
"type": "java.lang.Boolean",
"description": "Whether pooling is enabled. Enabled automatically if \"r2dbc-pool\" is on the classpath."
},
{ {
"name": "spring.rabbitmq.cache.connection.mode", "name": "spring.rabbitmq.cache.connection.mode",
"defaultValue": "channel" "defaultValue": "channel"
......
...@@ -97,6 +97,7 @@ org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\ ...@@ -97,6 +97,7 @@ org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\ org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\ org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
...@@ -147,6 +148,7 @@ org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinition ...@@ -147,6 +148,7 @@ org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinition
org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\ org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer
# Template availability providers # Template availability providers
......
...@@ -33,6 +33,7 @@ import java.util.logging.Logger; ...@@ -33,6 +33,7 @@ import java.util.logging.Logger;
import javax.sql.DataSource; import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import io.r2dbc.spi.ConnectionFactory;
import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.dbcp2.BasicDataSource;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
...@@ -94,6 +95,12 @@ class DataSourceAutoConfigurationTests { ...@@ -94,6 +95,12 @@ class DataSourceAutoConfigurationTests {
.hasMessageContaining("org.none.jdbcDriver")); .hasMessageContaining("org.none.jdbcDriver"));
} }
@Test
void datasourceWhenConnectionFactoryPresentIsNotAutoConfigured() {
this.contextRunner.withBean(ConnectionFactory.class, () -> mock(ConnectionFactory.class))
.run((context) -> assertThat(context).doesNotHaveBean(DataSource.class));
}
@Test @Test
void hikariValidatesConnectionByDefault() { void hikariValidatesConnectionByDefault() {
assertDataSource(HikariDataSource.class, Collections.singletonList("org.apache.tomcat"), (dataSource) -> assertDataSource(HikariDataSource.class, Collections.singletonList("org.apache.tomcat"), (dataSource) ->
......
/*
* Copyright 2012-2020 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 org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConnectionFactoryBeanCreationFailureAnalyzer}.
*
* @author Mark Paluch
*/
class ConnectionFactoryBeanCreationFailureAnalyzerTests {
private final MockEnvironment environment = new MockEnvironment();
@Test
void failureAnalysisIsPerformed() {
FailureAnalysis failureAnalysis = performAnalysis(TestConfiguration.class);
assertThat(failureAnalysis.getDescription()).contains("'url' attribute is not specified",
"no embedded database could be configured");
assertThat(failureAnalysis.getAction()).contains(
"If you want an embedded database (H2), please put it on the classpath",
"If you have database settings to be loaded from a particular profile you may need to activate it",
"(no profiles are currently active)");
}
@Test
void failureAnalysisIsPerformedWithActiveProfiles() {
this.environment.setActiveProfiles("first", "second");
FailureAnalysis failureAnalysis = performAnalysis(TestConfiguration.class);
assertThat(failureAnalysis.getAction()).contains("(the profiles first,second are currently active)");
}
private FailureAnalysis performAnalysis(Class<?> configuration) {
BeanCreationException failure = createFailure(configuration);
assertThat(failure).isNotNull();
ConnectionFactoryBeanCreationFailureAnalyzer failureAnalyzer = new ConnectionFactoryBeanCreationFailureAnalyzer();
failureAnalyzer.setEnvironment(this.environment);
return failureAnalyzer.analyze(failure);
}
private BeanCreationException createFailure(Class<?> configuration) {
try {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.setClassLoader(new FilteredClassLoader("io.r2dbc.h2", "io.r2dbc.pool"));
context.setEnvironment(this.environment);
context.register(configuration);
context.refresh();
context.close();
return null;
}
catch (BeanCreationException ex) {
return ex;
}
}
@Configuration(proxyBeanMethods = false)
@ImportAutoConfiguration(R2dbcAutoConfiguration.class)
static class TestConfiguration {
}
}
/*
* Copyright 2012-2020 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.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryMetadata;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.ConnectionFactoryProvider;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
/**
* Simple driver to capture {@link ConnectionFactoryOptions}.
*
* @author Mark Paluch
*/
public class SimpleConnectionFactoryProvider implements ConnectionFactoryProvider {
@Override
public ConnectionFactory create(ConnectionFactoryOptions connectionFactoryOptions) {
return new SimpleTestConnectionFactory(connectionFactoryOptions);
}
@Override
public boolean supports(ConnectionFactoryOptions connectionFactoryOptions) {
return connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.DRIVER).equals("simple");
}
@Override
public String getDriver() {
return "simple";
}
public static class SimpleTestConnectionFactory implements ConnectionFactory {
final ConnectionFactoryOptions options;
SimpleTestConnectionFactory(ConnectionFactoryOptions options) {
this.options = options;
}
@Override
public Publisher<? extends Connection> create() {
return Mono.error(new UnsupportedOperationException());
}
@Override
public ConnectionFactoryMetadata getMetadata() {
return SimpleConnectionFactoryProvider.class::getName;
}
public ConnectionFactoryOptions getOptions() {
return this.options;
}
}
}
...@@ -1360,6 +1360,13 @@ bom { ...@@ -1360,6 +1360,13 @@ bom {
] ]
} }
} }
library("R2DBC Bom", "Arabba-SR2") {
group("io.r2dbc") {
imports = [
"r2dbc-bom"
]
}
}
library("Rabbit AMQP Client", "5.8.0") { library("Rabbit AMQP Client", "5.8.0") {
group("com.rabbitmq") { group("com.rabbitmq") {
modules = [ modules = [
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment