Browse Source
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: Mark Paluch <mpaluch@pivotal.io>pull/20318/head
20 changed files with 1524 additions and 1 deletions
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
/* |
||||
* 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(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,259 @@
@@ -0,0 +1,259 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,122 @@
@@ -0,0 +1,122 @@
|
||||
/* |
||||
* 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"); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
/* |
||||
* 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); |
||||
|
||||
} |
||||
@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* 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 { |
||||
|
||||
} |
||||
@ -0,0 +1,190 @@
@@ -0,0 +1,190 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
/* |
||||
* 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; |
||||
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
/* |
||||
* 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 { |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,212 @@
@@ -0,0 +1,212 @@
|
||||
/* |
||||
* 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.Option; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBuilder.ConnectionFactoryBeanCreationException; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
import static org.assertj.core.api.Assertions.fail; |
||||
|
||||
/** |
||||
* Tests for {@link ConnectionFactoryBuilder}. |
||||
* |
||||
* @author Mark Paluch |
||||
* @author Tadaya Tsuyukubo |
||||
* @author Stephane Nicoll |
||||
*/ |
||||
class ConnectionFactoryBuilderTests { |
||||
|
||||
@Test |
||||
void propertiesWithoutUrlAndNoAvailableEmbeddedConnectionShouldFail() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
assertThatThrownBy(() -> ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.NONE)) |
||||
.isInstanceOf(ConnectionFactoryBeanCreationException.class) |
||||
.hasMessage("Failed to determine a suitable R2DBC Connection URL"); |
||||
} |
||||
|
||||
@Test |
||||
void connectionFactoryBeanCreationProvidesConnectionAndProperties() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
try { |
||||
ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.NONE); |
||||
fail("Should have thrown a " + ConnectionFactoryBeanCreationException.class.getName()); |
||||
} |
||||
catch (ConnectionFactoryBeanCreationException ex) { |
||||
assertThat(ex.getEmbeddedDatabaseConnection()).isEqualTo(EmbeddedDatabaseConnection.NONE); |
||||
assertThat(ex.getProperties()).isSameAs(properties); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
void regularConnectionIsConfiguredAutomaticallyWithUrl() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setUrl("r2dbc:simple://:pool:"); |
||||
ConnectionFactoryOptions options = ConnectionFactoryBuilder |
||||
.of(properties, () -> EmbeddedDatabaseConnection.NONE).buildOptions(); |
||||
assertThat(options.hasOption(ConnectionFactoryOptions.USER)).isFalse(); |
||||
assertThat(options.hasOption(ConnectionFactoryOptions.PASSWORD)).isFalse(); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("simple"); |
||||
} |
||||
|
||||
@Test |
||||
void regularConnectionShouldInitializeUrlOptions() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setUrl("r2dbc:simple:proto://user:password@myhost:4711/mydatabase"); |
||||
ConnectionFactoryOptions options = buildOptions(properties); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("simple"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PROTOCOL)).isEqualTo("proto"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("user"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("password"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isEqualTo("myhost"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isEqualTo(4711); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase"); |
||||
} |
||||
|
||||
@Test |
||||
void regularConnectionShouldUseUrlOptionsOverProperties() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setUrl("r2dbc:simple://user:password@myhost/mydatabase"); |
||||
properties.setUsername("another-user"); |
||||
properties.setPassword("another-password"); |
||||
properties.setName("another-database"); |
||||
ConnectionFactoryOptions options = buildOptions(properties); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("user"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("password"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase"); |
||||
} |
||||
|
||||
@Test |
||||
void regularConnectionShouldUseDatabaseNameOverRandomName() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setUrl("r2dbc:simple://user:password@myhost/mydatabase"); |
||||
properties.setGenerateUniqueName(true); |
||||
ConnectionFactoryOptions options = buildOptions(properties); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase"); |
||||
} |
||||
|
||||
@Test |
||||
void regularConnectionWithRandomNameShouldIgnoreNameFromProperties() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setUrl("r2dbc:h2://host"); |
||||
properties.setName("test-database"); |
||||
properties.setGenerateUniqueName(true); |
||||
ConnectionFactoryOptions options = buildOptions(properties); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isNotEqualTo("test-database") |
||||
.isNotEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void regularConnectionShouldSetCustomDriverProperties() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setUrl("r2dbc:simple://user:password@myhost"); |
||||
properties.getProperties().put("simpleOne", "one"); |
||||
properties.getProperties().put("simpleTwo", "two"); |
||||
ConnectionFactoryOptions options = buildOptions(properties); |
||||
assertThat(options.getRequiredValue(Option.<String>valueOf("simpleOne"))).isEqualTo("one"); |
||||
assertThat(options.getRequiredValue(Option.<String>valueOf("simpleTwo"))).isEqualTo("two"); |
||||
} |
||||
|
||||
@Test |
||||
void regularConnectionShouldUseBuilderValuesOverProperties() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setUrl("r2dbc:simple://user:password@myhost:47111/mydatabase"); |
||||
properties.setUsername("user"); |
||||
properties.setPassword("password"); |
||||
ConnectionFactoryOptions options = ConnectionFactoryBuilder |
||||
.of(properties, () -> EmbeddedDatabaseConnection.NONE).username("another-user") |
||||
.password("another-password").hostname("another-host").port(1234).database("another-database") |
||||
.buildOptions(); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("another-user"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("another-password"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isEqualTo("another-host"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isEqualTo(1234); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("another-database"); |
||||
} |
||||
|
||||
@Test |
||||
void embeddedConnectionIsConfiguredAutomaticallyWithoutUrl() { |
||||
ConnectionFactoryOptions options = ConnectionFactoryBuilder |
||||
.of(new R2dbcProperties(), () -> EmbeddedDatabaseConnection.H2).buildOptions(); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("sa"); |
||||
assertThat(options.hasOption(ConnectionFactoryOptions.PASSWORD)).isFalse(); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("h2"); |
||||
} |
||||
|
||||
@Test |
||||
void embeddedConnectionWithUsernameAndPassword() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setUsername("embedded"); |
||||
properties.setPassword("secret"); |
||||
ConnectionFactoryOptions options = ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.H2) |
||||
.buildOptions(); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("embedded"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("h2"); |
||||
} |
||||
|
||||
@Test |
||||
void embeddedConnectionUseDefaultDatabaseName() { |
||||
ConnectionFactoryOptions options = ConnectionFactoryBuilder |
||||
.of(new R2dbcProperties(), () -> EmbeddedDatabaseConnection.H2).buildOptions(); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("testdb"); |
||||
} |
||||
|
||||
@Test |
||||
void embeddedConnectionUseNameIfSet() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setName("test-database"); |
||||
ConnectionFactoryOptions options = buildOptions(properties); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("test-database"); |
||||
} |
||||
|
||||
@Test |
||||
void embeddedConnectionCanGenerateUniqueDatabaseName() { |
||||
R2dbcProperties firstProperties = new R2dbcProperties(); |
||||
firstProperties.setGenerateUniqueName(true); |
||||
ConnectionFactoryOptions options11 = buildOptions(firstProperties); |
||||
ConnectionFactoryOptions options12 = buildOptions(firstProperties); |
||||
assertThat(options11.getRequiredValue(ConnectionFactoryOptions.DATABASE)) |
||||
.isEqualTo(options12.getRequiredValue(ConnectionFactoryOptions.DATABASE)); |
||||
R2dbcProperties secondProperties = new R2dbcProperties(); |
||||
firstProperties.setGenerateUniqueName(true); |
||||
ConnectionFactoryOptions options21 = buildOptions(secondProperties); |
||||
ConnectionFactoryOptions options22 = buildOptions(secondProperties); |
||||
assertThat(options21.getRequiredValue(ConnectionFactoryOptions.DATABASE)) |
||||
.isEqualTo(options22.getRequiredValue(ConnectionFactoryOptions.DATABASE)); |
||||
assertThat(options11.getRequiredValue(ConnectionFactoryOptions.DATABASE)) |
||||
.isNotEqualTo(options21.getRequiredValue(ConnectionFactoryOptions.DATABASE)); |
||||
} |
||||
|
||||
@Test |
||||
void embeddedConnectionShouldIgnoreNameIfRandomNameIsRequired() { |
||||
R2dbcProperties properties = new R2dbcProperties(); |
||||
properties.setGenerateUniqueName(true); |
||||
properties.setName("test-database"); |
||||
ConnectionFactoryOptions options = buildOptions(properties); |
||||
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isNotEqualTo("test-database"); |
||||
} |
||||
|
||||
private ConnectionFactoryOptions buildOptions(R2dbcProperties properties) { |
||||
return ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.H2).buildOptions(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,264 @@
@@ -0,0 +1,264 @@
|
||||
/* |
||||
* 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.net.URL; |
||||
import java.net.URLClassLoader; |
||||
import java.util.UUID; |
||||
import java.util.function.Function; |
||||
|
||||
import javax.sql.DataSource; |
||||
|
||||
import io.r2dbc.h2.H2ConnectionFactory; |
||||
import io.r2dbc.pool.ConnectionPool; |
||||
import io.r2dbc.pool.PoolMetrics; |
||||
import io.r2dbc.spi.ConnectionFactory; |
||||
import io.r2dbc.spi.Option; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.beans.factory.BeanCreationException; |
||||
import org.springframework.boot.autoconfigure.AutoConfigurations; |
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.r2dbc.SimpleConnectionFactoryProvider.SimpleTestConnectionFactory; |
||||
import org.springframework.boot.test.context.FilteredClassLoader; |
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link R2dbcAutoConfiguration}. |
||||
* |
||||
* @author Mark Paluch |
||||
* @author Stephane Nicoll |
||||
*/ |
||||
class R2dbcAutoConfigurationTests { |
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() |
||||
.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)); |
||||
|
||||
@Test |
||||
void configureWithUrlCreateConnectionPoolByDefault() { |
||||
this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName()) |
||||
.run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) |
||||
.hasSingleBean(ConnectionPool.class)); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithUrlAndPoolPropertiesApplyProperties() { |
||||
this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName(), |
||||
"spring.r2dbc.pool.max-size=15").run((context) -> { |
||||
assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class); |
||||
PoolMetrics poolMetrics = context.getBean(ConnectionPool.class).getMetrics().get(); |
||||
assertThat(poolMetrics.getMaxAllocatedSize()).isEqualTo(15); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithUrlPoolAndPoolPropertiesApplyUrlPoolOptions() { |
||||
this.contextRunner |
||||
.withPropertyValues("spring.r2dbc.url:r2dbc:pool:h2:mem:///" + randomDatabaseName() + "?maxSize=12", |
||||
"spring.r2dbc.pool.max-size=15") |
||||
.run((context) -> { |
||||
assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class); |
||||
PoolMetrics poolMetrics = context.getBean(ConnectionPool.class).getMetrics().get(); |
||||
assertThat(poolMetrics.getMaxAllocatedSize()).isEqualTo(12); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithPoolEnabledCreateConnectionPool() { |
||||
this.contextRunner |
||||
.withPropertyValues("spring.r2dbc.pool.enabled=true", |
||||
"spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName() |
||||
+ "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") |
||||
.run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) |
||||
.hasSingleBean(ConnectionPool.class)); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithPoolDisabledCreateGenericConnectionFactory() { |
||||
this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false", "spring.r2dbc.url:r2dbc:h2:mem:///" |
||||
+ randomDatabaseName() + "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE").run((context) -> { |
||||
assertThat(context).hasSingleBean(ConnectionFactory.class).doesNotHaveBean(ConnectionPool.class); |
||||
assertThat(context.getBean(ConnectionFactory.class)).isExactlyInstanceOf(H2ConnectionFactory.class); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithoutR2dbcPoolCreateGenericConnectionFactory() { |
||||
this.contextRunner.with(hideConnectionPool()).withPropertyValues("spring.r2dbc.url:r2dbc:h2:mem:///" |
||||
+ randomDatabaseName() + "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE").run((context) -> { |
||||
assertThat(context).hasSingleBean(ConnectionFactory.class); |
||||
ConnectionFactory bean = context.getBean(ConnectionFactory.class); |
||||
assertThat(bean).isExactlyInstanceOf(H2ConnectionFactory.class); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithoutR2dbcPoolAndPoolEnabledDoesNotCreateConnectionFactory() { |
||||
this.contextRunner.with(hideConnectionPool()) |
||||
.withPropertyValues("spring.r2dbc.pool.enabled=true", |
||||
"spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName() |
||||
+ "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") |
||||
.run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactory.class)); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithoutPoolInvokeOptionCustomizer() { |
||||
this.contextRunner |
||||
.withPropertyValues("spring.r2dbc.pool.enabled=false", "spring.r2dbc.url:r2dbc:simple://host/database") |
||||
.withUserConfiguration(CustomizerConfiguration.class).run((context) -> { |
||||
assertThat(context).hasSingleBean(ConnectionFactory.class).doesNotHaveBean(ConnectionPool.class); |
||||
ConnectionFactory bean = context.getBean(ConnectionFactory.class); |
||||
assertThat(bean).isExactlyInstanceOf(SimpleTestConnectionFactory.class); |
||||
SimpleTestConnectionFactory connectionFactory = (SimpleTestConnectionFactory) bean; |
||||
assertThat(connectionFactory.getOptions().getRequiredValue(Option.<Boolean>valueOf("customized"))) |
||||
.isTrue(); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithPoolInvokeOptionCustomizer() { |
||||
this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:simple://host/database") |
||||
.withUserConfiguration(CustomizerConfiguration.class).run((context) -> { |
||||
assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class); |
||||
ConnectionFactory bean = context.getBean(ConnectionFactory.class); |
||||
SimpleTestConnectionFactory connectionFactory = (SimpleTestConnectionFactory) ((ConnectionPool) bean) |
||||
.unwrap(); |
||||
assertThat(connectionFactory.getOptions().getRequiredValue(Option.<Boolean>valueOf("customized"))) |
||||
.isTrue(); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithInvalidUrlThrowsAppropriateException() { |
||||
this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:not-going-to-work") |
||||
.run((context) -> assertThat(context).getFailure().isInstanceOf(BeanCreationException.class)); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithoutSpringJdbcCreateConnectionFactory() { |
||||
this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false", "spring.r2dbc.url:r2dbc:simple://foo") |
||||
.withClassLoader(new FilteredClassLoader("org.springframework.jdbc")).run((context) -> { |
||||
assertThat(context).hasSingleBean(ConnectionFactory.class); |
||||
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); |
||||
assertThat(connectionFactory).isInstanceOf(SimpleTestConnectionFactory.class); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithoutPoolShouldApplyAdditionalProperties() { |
||||
this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false", "spring.r2dbc.url:r2dbc:simple://foo", |
||||
"spring.r2dbc.properties.test=value", "spring.r2dbc.properties.another=2").run((context) -> { |
||||
SimpleTestConnectionFactory connectionFactory = context.getBean(SimpleTestConnectionFactory.class); |
||||
assertThat((Object) connectionFactory.options.getRequiredValue(Option.valueOf("test"))) |
||||
.isEqualTo("value"); |
||||
assertThat((Object) connectionFactory.options.getRequiredValue(Option.valueOf("another"))) |
||||
.isEqualTo("2"); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithPoolShouldApplyAdditionalProperties() { |
||||
this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:simple://foo", |
||||
"spring.r2dbc.properties.test=value", "spring.r2dbc.properties.another=2").run((context) -> { |
||||
assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class); |
||||
SimpleTestConnectionFactory connectionFactory = (SimpleTestConnectionFactory) context |
||||
.getBean(ConnectionPool.class).unwrap(); |
||||
assertThat((Object) connectionFactory.options.getRequiredValue(Option.valueOf("test"))) |
||||
.isEqualTo("value"); |
||||
assertThat((Object) connectionFactory.options.getRequiredValue(Option.valueOf("another"))) |
||||
.isEqualTo("2"); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithoutUrlShouldCreateEmbeddedConnectionPoolByDefault() { |
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) |
||||
.hasSingleBean(ConnectionPool.class)); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithoutUrlAndPollPoolDisabledCreateGenericConnectionFactory() { |
||||
this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false").run((context) -> { |
||||
assertThat(context).hasSingleBean(ConnectionFactory.class).doesNotHaveBean(ConnectionPool.class); |
||||
assertThat(context.getBean(ConnectionFactory.class)).isExactlyInstanceOf(H2ConnectionFactory.class); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithoutUrlAndSprigJdbcCreateEmbeddedConnectionFactory() { |
||||
this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.jdbc")) |
||||
.run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) |
||||
.hasSingleBean(ConnectionPool.class)); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithoutUrlAndEmbeddedCandidateFails() { |
||||
this.contextRunner.withClassLoader(new DisableEmbeddedDatabaseClassLoader()).run((context) -> { |
||||
assertThat(context).hasFailed(); |
||||
assertThat(context).getFailure().isInstanceOf(BeanCreationException.class) |
||||
.hasMessageContaining("Failed to determine a suitable R2DBC Connection URL"); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void configureWithDataSourceAutoConfigurationDoesNotCreateDataSource() { |
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) |
||||
.run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) |
||||
.doesNotHaveBean(DataSource.class)); |
||||
} |
||||
|
||||
private String randomDatabaseName() { |
||||
return "testdb-" + UUID.randomUUID(); |
||||
} |
||||
|
||||
private Function<ApplicationContextRunner, ApplicationContextRunner> hideConnectionPool() { |
||||
return (runner) -> runner.withClassLoader(new FilteredClassLoader("io.r2dbc.pool")); |
||||
} |
||||
|
||||
private static class DisableEmbeddedDatabaseClassLoader extends URLClassLoader { |
||||
|
||||
DisableEmbeddedDatabaseClassLoader() { |
||||
super(new URL[0], DisableEmbeddedDatabaseClassLoader.class.getClassLoader()); |
||||
} |
||||
|
||||
@Override |
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
||||
for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) { |
||||
if (name.equals(candidate.getDriverClassName())) { |
||||
throw new ClassNotFoundException(); |
||||
} |
||||
} |
||||
return super.loadClass(name, resolve); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
private static class CustomizerConfiguration { |
||||
|
||||
@Bean |
||||
ConnectionFactoryOptionsBuilderCustomizer customizer() { |
||||
return (builder) -> builder.option(Option.valueOf("customized"), true); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
org.springframework.boot.autoconfigure.r2dbc.SimpleConnectionFactoryProvider |
||||
Loading…
Reference in new issue