diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java index bf10929eea..2c21d10d2b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.cassandra; +import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.util.LinkedHashMap; @@ -52,6 +53,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Scope; +import org.springframework.core.io.Resource; /** * {@link EnableAutoConfiguration Auto-configuration} for Cassandra. @@ -116,6 +118,28 @@ public class CassandraAutoConfiguration { } private Config cassandraConfiguration(CassandraProperties properties) { + Config config = mapConfig(properties); + Resource configFile = properties.getConfig(); + return (configFile != null) ? applyDefaultFallback(config.withFallback(loadConfig(configFile))) + : applyDefaultFallback(config); + } + + private Config applyDefaultFallback(Config config) { + ConfigFactory.invalidateCaches(); + return ConfigFactory.defaultOverrides().withFallback(config).withFallback(ConfigFactory.defaultReference()) + .resolve(); + } + + private Config loadConfig(Resource config) { + try { + return ConfigFactory.parseURL(config.getURL()); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to load cassandra configuration from " + config, ex); + } + } + + private Config mapConfig(CassandraProperties properties) { CassandraDriverOptions options = new CassandraDriverOptions(); PropertyMapper map = PropertyMapper.get(); map.from(properties.getSessionName()).whenHasText() @@ -133,9 +157,7 @@ public class CassandraAutoConfiguration { .to((contactPoints) -> options.add(DefaultDriverOption.CONTACT_POINTS, contactPoints)); map.from(properties.getLocalDatacenter()).to( (localDatacenter) -> options.add(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, localDatacenter)); - ConfigFactory.invalidateCaches(); - return ConfigFactory.defaultOverrides().withFallback(options.build()) - .withFallback(ConfigFactory.defaultReference()).resolve(); + return options.build(); } private void mapConnectionOptions(CassandraProperties properties, CassandraDriverOptions options) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java index fdf84bd180..8c72e048a2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java @@ -24,6 +24,7 @@ import java.util.List; import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; /** * Configuration properties for Cassandra. @@ -37,6 +38,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.data.cassandra") public class CassandraProperties { + /** + * Location of the configuration file to use. + */ + private Resource config; + /** * Keyspace name to use. */ @@ -109,6 +115,14 @@ public class CassandraProperties { */ private final Controlconnection controlconnection = new Controlconnection(); + public Resource getConfig() { + return this.config; + } + + public void setConfig(Resource config) { + this.config = config; + } + public String getKeyspaceName() { return this.keyspaceName; } @@ -206,13 +220,13 @@ public class CassandraProperties { /** * Timeout to use when establishing driver connections. */ - private Duration connectTimeout = Duration.ofSeconds(5); + private Duration connectTimeout; /** * Timeout to use for internal queries that run as part of the initialization * process, just after a connection is opened. */ - private Duration initQueryTimeout = Duration.ofSeconds(5); + private Duration initQueryTimeout; public Duration getConnectTimeout() { return this.connectTimeout; @@ -237,7 +251,7 @@ public class CassandraProperties { /** * How long the driver waits for a request to complete. */ - private Duration timeout = Duration.ofSeconds(2); + private Duration timeout; /** * Queries consistency level. @@ -252,7 +266,7 @@ public class CassandraProperties { /** * How many rows will be retrieved simultaneously in a single network roundtrip. */ - private int pageSize = 5000; + private int pageSize; private final Throttler throttler = new Throttler(); @@ -302,13 +316,13 @@ public class CassandraProperties { /** * Idle timeout before an idle connection is removed. */ - private Duration idleTimeout = Duration.ofSeconds(5); + private Duration idleTimeout; /** * Heartbeat interval after which a message is sent on an idle connection to make * sure it's still alive. */ - private Duration heartbeatInterval = Duration.ofSeconds(30); + private Duration heartbeatInterval; public Duration getIdleTimeout() { return this.idleTimeout; @@ -350,7 +364,7 @@ public class CassandraProperties { /** * Request throttling type. */ - private ThrottlerType type = ThrottlerType.NONE; + private ThrottlerType type; /** * Maximum number of requests that can be enqueued when the throttling threshold 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 cb6fa6771e..761e4c0efd 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 @@ -511,6 +511,14 @@ "name": "spring.data.cassandra.compression", "defaultValue": "none" }, + { + "name": "spring.data.cassandra.connection.connection-timeout", + "defaultValue": "5s" + }, + { + "name": "spring.data.cassandra.connection.init-query-timeout", + "defaultValue": "5s" + }, { "name": "spring.data.cassandra.contact-points", "defaultValue": [ @@ -565,6 +573,14 @@ "description": "Type of Cassandra repositories to enable.", "defaultValue": "auto" }, + { + "name": "spring.data.cassandra.request.page-size", + "defaultValue": 5000 + }, + { + "name": "spring.data.cassandra.request.timeout", + "defaultValue": "2s" + }, { "name": "spring.data.cassandra.request.throttler.type", "defaultValue": "none" @@ -577,6 +593,14 @@ "level": "error" } }, + { + "name": "spring.data.cassandra.pool.heartbeat-interval", + "defaultValue": "30s" + }, + { + "name": "spring.data.cassandra.pool.idle-timeout", + "defaultValue": "5s" + }, { "name": "spring.data.couchbase.consistency", "type": "org.springframework.data.couchbase.core.query.Consistency", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java index 034bc27838..ed05b182aa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java @@ -16,10 +16,13 @@ package org.springframework.boot.autoconfigure.cassandra; +import java.time.Duration; + import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.CqlSessionBuilder; import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; import com.datastax.oss.driver.api.core.config.DriverConfigLoader; import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; import com.datastax.oss.driver.internal.core.session.throttling.ConcurrencyLimitingRequestThrottler; @@ -228,6 +231,31 @@ class CassandraAutoConfigurationTests { }); } + @Test + void driverConfigLoaderWithConfigComplementSettings() { + String configLocation = "org/springframework/boot/autoconfigure/cassandra/simple.conf"; + this.contextRunner.withPropertyValues("spring.data.cassandra.session-name=testcluster", + "spring.data.cassandra.config=" + configLocation).run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + assertThat(context.getBean(DriverConfigLoader.class).getInitialConfig().getDefaultProfile() + .getString(DefaultDriverOption.SESSION_NAME)).isEqualTo("testcluster"); + assertThat(context.getBean(DriverConfigLoader.class).getInitialConfig().getDefaultProfile() + .getDuration(DefaultDriverOption.REQUEST_TIMEOUT)).isEqualTo(Duration.ofMillis(500)); + }); + } + + @Test + void driverConfigLoaderWithConfigCreateProfiles() { + String configLocation = "org/springframework/boot/autoconfigure/cassandra/profiles.conf"; + this.contextRunner.withPropertyValues("spring.data.cassandra.config=" + configLocation).run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + DriverConfig driverConfig = context.getBean(DriverConfigLoader.class).getInitialConfig(); + assertThat(driverConfig.getProfiles()).containsOnlyKeys("default", "first", "second"); + assertThat(driverConfig.getProfile("first").getDuration(DefaultDriverOption.REQUEST_TIMEOUT)) + .isEqualTo(Duration.ofMillis(100)); + }); + } + @Configuration(proxyBeanMethods = false) static class SimpleDriverConfigLoaderBuilderCustomizerConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraPropertiesTests.java index a759a55641..e4eef02c11 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraPropertiesTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.cassandra; +import java.time.Duration; + import com.datastax.oss.driver.api.core.config.OptionsMap; import com.datastax.oss.driver.api.core.config.TypedDriverOption; import org.junit.jupiter.api.Test; @@ -26,27 +28,34 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link CassandraProperties}. * * @author Chris Bono + * @author Stephane Nicoll */ class CassandraPropertiesTests { + /** + * To let a configuration file override values, {@link CassandraProperties} can't have + * any default hardcoded. This test makes sure that the default that we moved to + * manual meta-data are accurate. + */ @Test - void defaultValuesAreConsistent() { - CassandraProperties properties = new CassandraProperties(); + void defaultValuesInManualMetadataAreConsistent() { OptionsMap driverDefaults = OptionsMap.driverDefaults(); - assertThat(properties.getConnection().getConnectTimeout()) - .isEqualTo(driverDefaults.get(TypedDriverOption.CONNECTION_CONNECT_TIMEOUT)); - assertThat(properties.getConnection().getInitQueryTimeout()) - .isEqualTo(driverDefaults.get(TypedDriverOption.CONNECTION_INIT_QUERY_TIMEOUT)); - assertThat(properties.getRequest().getTimeout()) - .isEqualTo(driverDefaults.get(TypedDriverOption.REQUEST_TIMEOUT)); - assertThat(properties.getRequest().getPageSize()) - .isEqualTo(driverDefaults.get(TypedDriverOption.REQUEST_PAGE_SIZE)); - assertThat(properties.getRequest().getThrottler().getType().type()) - .isEqualTo(driverDefaults.get(TypedDriverOption.REQUEST_THROTTLER_CLASS)); - assertThat(properties.getPool().getHeartbeatInterval()) - .isEqualTo(driverDefaults.get(TypedDriverOption.HEARTBEAT_INTERVAL)); - assertThat(properties.getPool().getIdleTimeout()) - .isEqualTo(driverDefaults.get(TypedDriverOption.HEARTBEAT_TIMEOUT)); + // spring.data.cassandra.connection.connection-timeout + assertThat(driverDefaults.get(TypedDriverOption.CONNECTION_CONNECT_TIMEOUT)).isEqualTo(Duration.ofSeconds(5)); + // spring.data.cassandra.connection.init-query-timeout + assertThat(driverDefaults.get(TypedDriverOption.CONNECTION_INIT_QUERY_TIMEOUT)) + .isEqualTo(Duration.ofSeconds(5)); + // spring.data.cassandra.request.timeout + assertThat(driverDefaults.get(TypedDriverOption.REQUEST_TIMEOUT)).isEqualTo(Duration.ofSeconds(2)); + // spring.data.cassandra.request.page-size + assertThat(driverDefaults.get(TypedDriverOption.REQUEST_PAGE_SIZE)).isEqualTo(5000); + // spring.data.cassandra.request.throttler.type + assertThat(driverDefaults.get(TypedDriverOption.REQUEST_THROTTLER_CLASS)) + .isEqualTo("PassThroughRequestThrottler"); // "none" + // spring.data.cassandra.pool.heartbeat-interval + assertThat(driverDefaults.get(TypedDriverOption.HEARTBEAT_INTERVAL)).isEqualTo(Duration.ofSeconds(30)); + // spring.data.cassandra.pool.idle-timeout + assertThat(driverDefaults.get(TypedDriverOption.HEARTBEAT_TIMEOUT)).isEqualTo(Duration.ofSeconds(5)); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cassandra/profiles.conf b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cassandra/profiles.conf new file mode 100644 index 0000000000..0527280e8f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cassandra/profiles.conf @@ -0,0 +1,12 @@ +datastax-java-driver { + profiles { + first { + basic.request.timeout = 100 milliseconds + basic.request.consistency = ONE + } + second { + basic.request.timeout = 5 seconds + basic.request.consistency = QUORUM + } + } +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cassandra/simple.conf b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cassandra/simple.conf new file mode 100644 index 0000000000..494ff4d86d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cassandra/simple.conf @@ -0,0 +1,6 @@ +datastax-java-driver { + basic { + session-name = Test session + request.timeout = 500 milliseconds + } +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index e774350394..068f2ed6e1 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -4554,7 +4554,9 @@ If you need to configure the port, use `spring.data.cassandra.port`. ==== The Cassandra driver has its own configuration infrastructure that loads an `application.conf` at the root of the classpath. -Spring Boot does not look for such a file and rather provides a number of configuration properties via the `spring.data.cassandra.*` namespace. +Spring Boot does not look for such a file by default but can load one using `spring.data.cassandra.config`. +If a property is both present in `+spring.data.cassandra.*+` and the configuration file, the value in `+spring.data.cassandra.*+` takes precedence. + For more advanced driver customizations, you can register an arbitrary number of beans that implement `DriverConfigLoaderBuilderCustomizer`. The `CqlSession` can be customized with a bean of type `CqlSessionBuilderCustomizer`. ====