From fbcc1fdec6cc3aa78e5ffc7d398993f9c2aac767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 1 Oct 2025 11:18:55 +0200 Subject: [PATCH] Polish "Add support for static master-replica with Lettuce" See gh-46957 --- .../modules/reference/pages/data/nosql.adoc | 5 +- .../DataRedisConnectionConfiguration.java | 34 +++++-- .../DataRedisConnectionDetails.java | 35 +++++-- .../autoconfigure/DataRedisProperties.java | 45 ++++++--- .../JedisConnectionConfiguration.java | 6 +- .../LettuceConnectionConfiguration.java | 57 ++---------- .../PropertiesDataRedisConnectionDetails.java | 24 +++++ .../DataRedisAutoConfigurationJedisTests.java | 9 ++ .../DataRedisAutoConfigurationTests.java | 91 ++++++++++++++----- ...PropertiesRedisConnectionDetailsTests.java | 10 ++ 10 files changed, 214 insertions(+), 102 deletions(-) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc index 811b1fe242b..8fe7c3864b7 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/data/nosql.adoc @@ -75,7 +75,10 @@ spring: TIP: You can also register an arbitrary number of beans that implement javadoc:org.springframework.boot.data.redis.autoconfigure.LettuceClientConfigurationBuilderCustomizer[] for more advanced customizations. javadoc:io.lettuce.core.resource.ClientResources[] can also be customized using javadoc:org.springframework.boot.data.redis.autoconfigure.ClientResourcesBuilderCustomizer[]. If you use Jedis, javadoc:org.springframework.boot.data.redis.autoconfigure.JedisClientConfigurationBuilderCustomizer[] is also available. -Alternatively, you can register a bean of type javadoc:org.springframework.data.redis.connection.RedisStandaloneConfiguration[], javadoc:org.springframework.data.redis.connection.RedisSentinelConfiguration[], or javadoc:org.springframework.data.redis.connection.RedisClusterConfiguration[] to take full control over the configuration. + +Alternatively, you can register a bean of type javadoc:org.springframework.data.redis.connection.RedisStandaloneConfiguration[], javadoc:org.springframework.data.redis.connection.RedisSentinelConfiguration[], javadoc:org.springframework.data.redis.connection.RedisClusterConfiguration[], or javadoc:org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration[]] to take full control over the configuration. + +NOTE: master/replica is not supported by Jedis. If you add your own javadoc:org.springframework.context.annotation.Bean[format=annotation] of any of the auto-configured types, it replaces the default (except in the case of javadoc:org.springframework.data.redis.core.RedisTemplate[], when the exclusion is based on the bean name, `redisTemplate`, not its type). diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionConfiguration.java index c6513fe6b5f..3f475f502bf 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionConfiguration.java @@ -33,9 +33,9 @@ import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.CollectionUtils; /** * Base Redis connection configuration. @@ -49,7 +49,6 @@ import org.springframework.util.CollectionUtils; * @author Andy Wilkinson * @author Phillip Webb * @author Yanming Zhou - * @author Yong-Hyun Kim */ abstract class DataRedisConnectionConfiguration { @@ -64,6 +63,8 @@ abstract class DataRedisConnectionConfiguration { private final @Nullable RedisClusterConfiguration clusterConfiguration; + private final @Nullable RedisStaticMasterReplicaConfiguration masterReplicaConfiguration; + private final DataRedisConnectionDetails connectionDetails; protected final Mode mode; @@ -72,11 +73,13 @@ abstract class DataRedisConnectionConfiguration { DataRedisConnectionDetails connectionDetails, ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, - ObjectProvider clusterConfigurationProvider) { + ObjectProvider clusterConfigurationProvider, + ObjectProvider masterReplicaConfiguration) { this.properties = properties; this.standaloneConfiguration = standaloneConfigurationProvider.getIfAvailable(); this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable(); this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable(); + this.masterReplicaConfiguration = masterReplicaConfiguration.getIfAvailable(); this.connectionDetails = connectionDetails; this.mode = determineMode(); } @@ -145,6 +148,25 @@ abstract class DataRedisConnectionConfiguration { return null; } + protected final @Nullable RedisStaticMasterReplicaConfiguration getMasterReplicaConfiguration() { + if (this.masterReplicaConfiguration != null) { + return this.masterReplicaConfiguration; + } + if (this.connectionDetails.getMasterReplica() != null) { + List nodes = this.connectionDetails.getMasterReplica().getNodes(); + RedisStaticMasterReplicaConfiguration config = new RedisStaticMasterReplicaConfiguration( + nodes.get(0).host(), nodes.get(0).port()); + nodes.stream().skip(1).forEach((node) -> config.addNode(node.host(), node.port())); + config.setUsername(this.connectionDetails.getUsername()); + String password = this.connectionDetails.getPassword(); + if (password != null) { + config.setPassword(RedisPassword.of(password)); + } + return config; + } + return null; + } + private List getNodes(Cluster cluster) { return cluster.getNodes().stream().map(this::asRedisNode).toList(); } @@ -193,15 +215,15 @@ abstract class DataRedisConnectionConfiguration { if (getClusterConfiguration() != null) { return Mode.CLUSTER; } - if (!CollectionUtils.isEmpty(this.properties.getLettuce().getNodes())) { - return Mode.STATIC_MASTER_REPLICA; + if (getMasterReplicaConfiguration() != null) { + return Mode.MASTER_REPLICA; } return Mode.STANDALONE; } enum Mode { - STANDALONE, CLUSTER, SENTINEL, STATIC_MASTER_REPLICA + STANDALONE, CLUSTER, MASTER_REPLICA, SENTINEL } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionDetails.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionDetails.java index ec249e1993d..5fe5cd3d2c9 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionDetails.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionDetails.java @@ -58,8 +58,8 @@ public interface DataRedisConnectionDetails extends ConnectionDetails { } /** - * Redis standalone configuration. Mutually exclusive with {@link #getSentinel()} and - * {@link #getCluster()}. + * Redis standalone configuration. Mutually exclusive with {@link #getSentinel()}, + * {@link #getCluster()} and {@link #getMasterReplica()}. * @return the Redis standalone configuration */ default @Nullable Standalone getStandalone() { @@ -67,8 +67,8 @@ public interface DataRedisConnectionDetails extends ConnectionDetails { } /** - * Redis sentinel configuration. Mutually exclusive with {@link #getStandalone()} and - * {@link #getCluster()}. + * Redis sentinel configuration. Mutually exclusive with {@link #getStandalone()}, + * {@link #getCluster()} and {@link #getMasterReplica()}. * @return the Redis sentinel configuration */ default @Nullable Sentinel getSentinel() { @@ -76,14 +76,23 @@ public interface DataRedisConnectionDetails extends ConnectionDetails { } /** - * Redis cluster configuration. Mutually exclusive with {@link #getStandalone()} and - * {@link #getSentinel()}. + * Redis cluster configuration. Mutually exclusive with {@link #getStandalone()}, + * {@link #getSentinel()} and {@link #getMasterReplica()}. * @return the Redis cluster configuration */ default @Nullable Cluster getCluster() { return null; } + /** + * Redis master replica configuration. Mutually exclusive with + * {@link #getStandalone()}, {@link #getSentinel()} and {@link #getCluster()}. + * @return the Redis master replica configuration + */ + default @Nullable MasterReplica getMasterReplica() { + return null; + } + /** * Redis standalone configuration. */ @@ -201,6 +210,20 @@ public interface DataRedisConnectionDetails extends ConnectionDetails { } + /** + * Redis master replica configuration. + */ + interface MasterReplica { + + /** + * Static nodes to use. This represents the full list of cluster nodes and is + * required to have at least one entry. + * @return the nodes to use + */ + List getNodes(); + + } + /** * A node in a sentinel or cluster configuration. * diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java index 76ef7aa51ae..66152a41c22 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java @@ -34,7 +34,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author Stephane Nicoll * @author Scott Frederick * @author Yanming Zhou - * @author Yong-Hyun Kim * @since 4.0.0 */ @ConfigurationProperties("spring.data.redis") @@ -95,6 +94,8 @@ public class DataRedisProperties { private @Nullable Cluster cluster; + private @Nullable Masterreplica masterreplica; + private final Ssl ssl = new Ssl(); private final Jedis jedis = new Jedis(); @@ -201,6 +202,14 @@ public class DataRedisProperties { this.cluster = cluster; } + public @Nullable Masterreplica getMasterreplica() { + return this.masterreplica; + } + + public void setMasterreplica(@Nullable Masterreplica masterreplica) { + this.masterreplica = masterreplica; + } + public Jedis getJedis() { return this.jedis; } @@ -355,6 +364,26 @@ public class DataRedisProperties { } + /** + * Master Replica properties. + */ + public static class Masterreplica { + + /** + * Static list of "host:port" pairs to use, at least one entry is required. + */ + private @Nullable List nodes; + + public @Nullable List getNodes() { + return this.nodes; + } + + public void setNodes(@Nullable List nodes) { + this.nodes = nodes; + } + + } + /** * Redis sentinel properties. */ @@ -483,20 +512,6 @@ public class DataRedisProperties { private final Cluster cluster = new Cluster(); - /** - * List of static master-replica "host:port" pairs regardless of role - * as the actual roles are determined by querying each node's ROLE command. - */ - private @Nullable List nodes; - - public @Nullable List getNodes() { - return this.nodes; - } - - public void setNodes(@Nullable List nodes) { - this.nodes = nodes; - } - public Duration getShutdownTimeout() { return this.shutdownTimeout; } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java index 22d948154d3..80d0aba0c69 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java @@ -38,6 +38,7 @@ import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisSslClientConfigurationBuilder; @@ -66,9 +67,10 @@ class JedisConnectionConfiguration extends DataRedisConnectionConfiguration { ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfiguration, ObjectProvider clusterConfiguration, + ObjectProvider masterReplicaConfiguration, DataRedisConnectionDetails connectionDetails) { super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfiguration, - clusterConfiguration); + clusterConfiguration, masterReplicaConfiguration); } @Bean @@ -104,7 +106,7 @@ class JedisConnectionConfiguration extends DataRedisConnectionConfiguration { Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); yield new JedisConnectionFactory(sentinelConfig, clientConfiguration); } - case STATIC_MASTER_REPLICA -> throw new IllegalStateException("Static master replica is not supported for Jedis"); + case MASTER_REPLICA -> throw new IllegalStateException("'masterReplicaConfig' is not supported by Jedis"); }; } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java index 88dbfcbf8c1..177040323c1 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java @@ -17,8 +17,6 @@ package org.springframework.boot.data.redis.autoconfigure; import java.time.Duration; -import java.util.Collections; -import java.util.List; import io.lettuce.core.ClientOptions; import io.lettuce.core.ReadFrom; @@ -39,8 +37,6 @@ 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.ConditionalOnThreading; -import org.springframework.boot.data.redis.autoconfigure.RedisConnectionDetails.Node; -import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Lettuce; import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Lettuce.Cluster.Refresh; import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Pool; import org.springframework.boot.ssl.SslBundle; @@ -59,7 +55,6 @@ import org.springframework.data.redis.connection.lettuce.LettuceClientConfigurat import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -70,7 +65,6 @@ import org.springframework.util.StringUtils; * @author Moritz Halbritter * @author Phillip Webb * @author Scott Frederick - * @author Yong-Hyun Kim */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisClient.class) @@ -81,9 +75,10 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration { ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, ObjectProvider clusterConfigurationProvider, + ObjectProvider masterReplicaConfiguration, DataRedisConnectionDetails connectionDetails) { super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfigurationProvider, - clusterConfigurationProvider); + clusterConfigurationProvider, masterReplicaConfiguration); } @Bean(destroyMethod = "shutdown") @@ -127,12 +122,6 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration { LettuceClientConfiguration clientConfiguration = getLettuceClientConfiguration( clientConfigurationBuilderCustomizers, clientOptionsBuilderCustomizers, clientResources, getProperties().getLettuce().getPool()); - - RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = getStaticMasterReplicaConfiguration(); - if (staticMasterReplicaConfiguration != null) { - return new LettuceConnectionFactory(staticMasterReplicaConfiguration, clientConfiguration); - } - return switch (this.mode) { case STANDALONE -> new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration); case CLUSTER -> { @@ -145,34 +134,14 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration { Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); yield new LettuceConnectionFactory(sentinelConfig, clientConfiguration); } - case STATIC_MASTER_REPLICA -> { - RedisStaticMasterReplicaConfiguration configuration = getStaticMasterReplicaConfiguration(); - Assert.state(configuration != null, "'staticMasterReplicaConfiguration' must not be null"); - yield new LettuceConnectionFactory(configuration, clientConfiguration); + case MASTER_REPLICA -> { + RedisStaticMasterReplicaConfiguration masterReplicaConfiguration = getMasterReplicaConfiguration(); + Assert.state(masterReplicaConfiguration != null, "'masterReplicaConfig' must not be null"); + yield new LettuceConnectionFactory(masterReplicaConfiguration, clientConfiguration); } }; } - private @Nullable RedisStaticMasterReplicaConfiguration getStaticMasterReplicaConfiguration() { - RedisProperties.Lettuce lettuce = getProperties().getLettuce(); - - if (!CollectionUtils.isEmpty(lettuce.getNodes())) { - List nodes = asNodes(lettuce.getNodes()); - RedisStaticMasterReplicaConfiguration configuration = new RedisStaticMasterReplicaConfiguration( - nodes.get(0).host(), nodes.get(0).port()); - configuration.setUsername(getProperties().getUsername()); - if (StringUtils.hasText(getProperties().getPassword())) { - configuration.setPassword(getProperties().getPassword()); - } - configuration.setDatabase(getProperties().getDatabase()); - nodes.stream().skip(1).forEach((node) -> configuration.addNode(node.host(), node.port())); - - return configuration; - } - - return null; - } - private LettuceClientConfiguration getLettuceClientConfiguration( ObjectProvider clientConfigurationBuilderCustomizers, ObjectProvider clientOptionsBuilderCustomizers, @@ -288,20 +257,6 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration { } } - private List asNodes(@Nullable List nodes) { - if (nodes == null) { - return Collections.emptyList(); - } - return nodes.stream().map(this::asNode).toList(); - } - - private Node asNode(String node) { - int portSeparatorIndex = node.lastIndexOf(':'); - String host = node.substring(0, portSeparatorIndex); - int port = Integer.parseInt(node.substring(portSeparatorIndex + 1)); - return new Node(host, port); - } - /** * Inner class to allow optional commons-pool2 dependency. */ diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesDataRedisConnectionDetails.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesDataRedisConnectionDetails.java index 97e786186ee..372244ca24f 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesDataRedisConnectionDetails.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesDataRedisConnectionDetails.java @@ -92,6 +92,12 @@ class PropertiesDataRedisConnectionDetails implements DataRedisConnectionDetails return (cluster != null) ? new PropertiesCluster(cluster) : null; } + @Override + public @Nullable MasterReplica getMasterReplica() { + DataRedisProperties.Masterreplica masterreplica = this.properties.getMasterreplica(); + return (masterreplica != null) ? new PropertiesMasterReplica(masterreplica) : null; + } + private @Nullable DataRedisUrl getRedisUrl() { return DataRedisUrl.of(this.properties.getUrl()); } @@ -128,6 +134,24 @@ class PropertiesDataRedisConnectionDetails implements DataRedisConnectionDetails } + /** + * {@link MasterReplica} implementation backed by properties. + */ + private class PropertiesMasterReplica implements MasterReplica { + + private final List nodes; + + PropertiesMasterReplica(DataRedisProperties.Masterreplica properties) { + this.nodes = asNodes(properties.getNodes()); + } + + @Override + public List getNodes() { + return this.nodes; + } + + } + /** * {@link Sentinel} implementation backed by properties. */ diff --git a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationJedisTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationJedisTests.java index bfe5279440c..717aba2cf43 100644 --- a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationJedisTests.java +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationJedisTests.java @@ -247,6 +247,15 @@ class DataRedisAutoConfigurationJedisTests { .isTrue()); } + @Test + void testRedisConfigurationWitMasterReplica() { + this.contextRunner.withPropertyValues("spring.data.redis.masterreplica.nodes=127.0.0.1:27379,127.0.0.1:27380") + .run((context) -> assertThat(context).hasFailed() + .getFailure() + .rootCause() + .hasMessageContaining("'masterReplicaConfig' is not supported by Jedis")); + } + @Test void testRedisConfigurationWithSslEnabled() { this.contextRunner.withPropertyValues("spring.data.redis.ssl.enabled:true").run((context) -> { diff --git a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationTests.java index 49641174cff..d698784c7b3 100644 --- a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationTests.java +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationTests.java @@ -61,6 +61,7 @@ import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder; @@ -91,7 +92,6 @@ import static org.mockito.Mockito.mock; * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb - * @author Yong-Hyun Kim */ class DataRedisAutoConfigurationTests { @@ -497,40 +497,38 @@ class DataRedisAutoConfigurationTests { LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); assertThat(getUserName(connectionFactory)).isEqualTo("user"); assertThat(connectionFactory.getPassword()).isEqualTo("password"); - } - - ); + }); } @Test - void testRedisConfigurationWithStaticMasterReplica() { - List staticMasterReplicaNodes = Arrays.asList("127.0.0.1:28319", "127.0.0.1:28320", "[::1]:28321"); + void testRedisConfigurationWithMasterReplica() { this.contextRunner - .withPropertyValues( - "spring.data.redis.lettuce.static-master-replica.nodes[0]:" + staticMasterReplicaNodes.get(0), - "spring.data.redis.lettuce.static-master-replica.nodes[1]:" + staticMasterReplicaNodes.get(1), - "spring.data.redis.lettuce.static-master-replica.nodes[2]:" + staticMasterReplicaNodes.get(2)) + .withPropertyValues("spring.data.redis.masterreplica.nodes=127.0.0.1:28319,127.0.0.1:28320,[::1]:28321") .run((context) -> { LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); assertThat(connectionFactory.getSentinelConfiguration()).isNull(); assertThat(connectionFactory.getClusterConfiguration()).isNull(); - assertThat(isStaticMasterReplicaAware(connectionFactory)).isTrue(); + assertThat(connectionFactory).extracting("configuration") + .isInstanceOfSatisfying(RedisStaticMasterReplicaConfiguration.class, + (masterReplicaConfiguration) -> assertThat(masterReplicaConfiguration.getNodes() + .stream() + .map((config) -> new RedisNode(config.getHostName(), config.getPort()))) + .containsExactly(new RedisNode("127.0.0.1", 28319), new RedisNode("127.0.0.1", 28320), + new RedisNode("[::1]", 28321))); }); } @Test - void testRedisConfigurationWithStaticMasterReplicaAndAuthenticationAndDatabase() { - List staticMasterReplicaNodes = Arrays.asList("127.0.0.1:28319", "127.0.0.1:28320"); + void testRedisConfigurationWithMasterAndAuthentication() { this.contextRunner .withPropertyValues("spring.data.redis.username=user", "spring.data.redis.password=password", - "spring.data.redis.database=1", - "spring.data.redis.lettuce.static-master-replica.nodes[0]:" + staticMasterReplicaNodes.get(0), - "spring.data.redis.lettuce.static-master-replica.nodes[1]:" + staticMasterReplicaNodes.get(1)) + "spring.data.redis.masterreplica.nodes=127.0.0.1:28319,127.0.0.1:28320") .run((context) -> { LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); assertThat(getUserName(connectionFactory)).isEqualTo("user"); assertThat(connectionFactory.getPassword()).isEqualTo("password"); - assertThat(connectionFactory.getDatabase()).isOne(); + assertThat(connectionFactory).extracting("configuration") + .isInstanceOf(RedisStaticMasterReplicaConfiguration.class); }); } @@ -661,6 +659,27 @@ class DataRedisAutoConfigurationTests { }); } + @Test + void usesMasterReplicaFromCustomConnectionDetails() { + this.contextRunner.withUserConfiguration(ConnectionDetailsMasterReplicaConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(DataRedisConnectionDetails.class) + .doesNotHaveBean(PropertiesDataRedisConnectionDetails.class); + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.isUseSsl()).isFalse(); + assertThat(cf).extracting("configuration") + .isInstanceOfSatisfying(RedisStaticMasterReplicaConfiguration.class, + (masterReplicationConfiguration) -> { + assertThat(masterReplicationConfiguration.getUsername()).isEqualTo("user-1"); + assertThat(masterReplicationConfiguration.getPassword().get()) + .isEqualTo("password-1".toCharArray()); + assertThat(masterReplicationConfiguration.getNodes()) + .map((nodeConfiguration) -> new RedisNode(nodeConfiguration.getHostName(), + nodeConfiguration.getPort())) + .containsExactly(new RedisNode("node-1", 12345), new RedisNode("node-2", 23456)); + }); + }); + } + @Test void testRedisConfigurationWithSslEnabled() { this.contextRunner.withPropertyValues("spring.data.redis.ssl.enabled:true").run((context) -> { @@ -738,10 +757,6 @@ class DataRedisAutoConfigurationTests { return node; } - private boolean isStaticMasterReplicaAware(LettuceConnectionFactory factory) { - return ReflectionTestUtils.invokeMethod(factory, "isStaticMasterReplicaAware"); - } - private static final class RedisNodes implements Nodes { private final List descriptions; @@ -921,4 +936,38 @@ class DataRedisAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsMasterReplicaConfiguration { + + @Bean + DataRedisConnectionDetails redisConnectionDetails() { + return new DataRedisConnectionDetails() { + + @Override + public String getUsername() { + return "user-1"; + } + + @Override + public String getPassword() { + return "password-1"; + } + + @Override + public MasterReplica getMasterReplica() { + return new MasterReplica() { + + @Override + public List getNodes() { + return List.of(new Node("node-1", 12345), new Node("node-2", 23456)); + } + + }; + } + + }; + } + + } + } diff --git a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java index 51610c47c94..697367efc72 100644 --- a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java @@ -57,6 +57,7 @@ class PropertiesRedisConnectionDetailsTests { assertThat(standalone.getDatabase()).isEqualTo(0); assertThat(this.connectionDetails.getSentinel()).isNull(); assertThat(this.connectionDetails.getCluster()).isNull(); + assertThat(this.connectionDetails.getMasterReplica()).isNull(); assertThat(this.connectionDetails.getUsername()).isNull(); assertThat(this.connectionDetails.getPassword()).isNull(); } @@ -147,6 +148,15 @@ class PropertiesRedisConnectionDetailsTests { new Node("127.0.0.1", 2222), new Node("[::1]", 3333)); } + @Test + void masterReplicaIsConfigured() { + DataRedisProperties.Masterreplica masterReplica = new DataRedisProperties.Masterreplica(); + masterReplica.setNodes(List.of("localhost:1111", "127.0.0.1:2222", "[::1]:3333")); + this.properties.setMasterreplica(masterReplica); + assertThat(this.connectionDetails.getMasterReplica().getNodes()).containsExactly(new Node("localhost", 1111), + new Node("127.0.0.1", 2222), new Node("[::1]", 3333)); + } + @Test void sentinelIsConfigured() { DataRedisProperties.Sentinel sentinel = new DataRedisProperties.Sentinel();