diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index bb38138e2c..9ca0035694 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -78,7 +78,7 @@ public class DocumentConfigurationProperties extends AbstractTask { .addSection("data-migration").withKeyPrefixes("spring.flyway", "spring.liquibase").addSection("data") .withKeyPrefixes("spring.couchbase", "spring.elasticsearch", "spring.h2", "spring.influx", "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.tomcat", "Tomcat datasource specific settings") .addOverride("spring.datasource.hikari", "Hikari specific settings").addSection("transaction") diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 0e6b6033bf..7d4b6a15b9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -33,6 +33,8 @@ dependencies { optional("de.flapdoodle.embed:de.flapdoodle.embed.mongo") optional("io.lettuce:lettuce-core") 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-transport-netty") optional("io.undertow:undertow-servlet") { @@ -156,6 +158,7 @@ dependencies { testImplementation("com.jayway.jsonpath:json-path") testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("com.sun.xml.messaging.saaj:saaj-impl") + testImplementation("io.r2dbc:r2dbc-h2") testImplementation("jakarta.json:jakarta.json-api") testImplementation("jakarta.xml.ws:jakarta.xml.ws-api") testImplementation("mysql:mysql-connector-java") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index e4f249a769..770789e65e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -51,6 +51,7 @@ import org.springframework.util.StringUtils; */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") @EnableConfigurationProperties(DataSourceProperties.class) @Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class }) public class DataSourceAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java new file mode 100644 index 0000000000..3ece313e4f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java @@ -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 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(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java new file mode 100644 index 0000000000..5c37a2e3db --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java @@ -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) { + 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 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) { + 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 void configureIf(Builder optionsBuilder, + ConnectionFactoryOptions originalOptions, Option option, Supplier valueSupplier, + Predicate setIf) { + if (originalOptions.hasOption(option)) { + return; + } + T value = valueSupplier.get(); + if (setIf.test(value)) { + optionsBuilder.option(option, value); + } + } + + private ConnectionFactoryBeanCreationException connectionFactoryBeanCreationException(String message, + R2dbcProperties properties, EmbeddedDatabaseConnection embeddedDatabaseConnection) { + return new ConnectionFactoryBeanCreationException(message, properties, embeddedDatabaseConnection); + } + + 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; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java new file mode 100644 index 0000000000..d918470ba6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java @@ -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 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 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 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"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsBuilderCustomizer.java new file mode 100644 index 0000000000..20fd830ed6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsBuilderCustomizer.java @@ -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); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java new file mode 100644 index 0000000000..1b4fc1e362 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java @@ -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; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java new file mode 100644 index 0000000000..31dd8e381f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java @@ -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 { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcProperties.java new file mode 100644 index 0000000000..7792e67392 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcProperties.java @@ -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 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 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; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/package-info.java new file mode 100644 index 0000000000..e36e5c3a3a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index af60810bbc..f196ba0749 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -813,6 +813,11 @@ "name": "spring.quartz.scheduler-name", "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", "defaultValue": "channel" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 31e6e47b76..feb87f0f9f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -97,6 +97,7 @@ org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\ org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\ org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\ +org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\ @@ -147,6 +148,7 @@ org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinition org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\ +org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\ org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer # Template availability providers diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java index 92af2e060d..efd85f9fed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java @@ -33,6 +33,7 @@ import java.util.logging.Logger; import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; +import io.r2dbc.spi.ConnectionFactory; import org.apache.commons.dbcp2.BasicDataSource; import org.junit.jupiter.api.Test; @@ -94,6 +95,12 @@ class DataSourceAutoConfigurationTests { .hasMessageContaining("org.none.jdbcDriver")); } + @Test + void datasourceWhenConnectionFactoryPresentIsNotAutoConfigured() { + this.contextRunner.withBean(ConnectionFactory.class, () -> mock(ConnectionFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(DataSource.class)); + } + @Test void hikariValidatesConnectionByDefault() { assertDataSource(HikariDataSource.class, Collections.singletonList("org.apache.tomcat"), (dataSource) -> diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzerTests.java new file mode 100644 index 0000000000..2faacabe37 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzerTests.java @@ -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 { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java new file mode 100644 index 0000000000..713e2afc86 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java @@ -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.valueOf("simpleOne"))).isEqualTo("one"); + assertThat(options.getRequiredValue(Option.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(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java new file mode 100644 index 0000000000..3e3830e59c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java @@ -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.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.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 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); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/SimpleConnectionFactoryProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/SimpleConnectionFactoryProvider.java new file mode 100644 index 0000000000..2a589fbf79 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/SimpleConnectionFactoryProvider.java @@ -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 create() { + return Mono.error(new UnsupportedOperationException()); + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + return SimpleConnectionFactoryProvider.class::getName; + } + + public ConnectionFactoryOptions getOptions() { + return this.options; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider new file mode 100644 index 0000000000..b8002e9ea3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.r2dbc.SimpleConnectionFactoryProvider diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9a4068c7d8..a7a8972d58 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1360,6 +1360,13 @@ bom { ] } } + library("R2DBC Bom", "Arabba-SR2") { + group("io.r2dbc") { + imports = [ + "r2dbc-bom" + ] + } + } library("Rabbit AMQP Client", "5.8.0") { group("com.rabbitmq") { modules = [