Create spring-boot-data-redis module

This commit is contained in:
Stéphane Nicoll
2025-03-28 14:11:55 +01:00
committed by Phillip Webb
parent 10db864d35
commit fa0bdc895f
73 changed files with 499 additions and 462 deletions

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.data.redis.autoconfigure.RedisConnectionDetails.Node;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundle;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link PropertiesRedisConnectionDetails}.
*
* @author Scott Frederick
* @author Moritz Halbritter
*/
class PropertiesRedisConnectionDetailsTests {
private RedisProperties properties;
private PropertiesRedisConnectionDetails connectionDetails;
private DefaultSslBundleRegistry sslBundleRegistry;
@BeforeEach
void setUp() {
this.properties = new RedisProperties();
this.sslBundleRegistry = new DefaultSslBundleRegistry();
this.connectionDetails = new PropertiesRedisConnectionDetails(this.properties, this.sslBundleRegistry);
}
@Test
void connectionIsConfiguredWithDefaults() {
RedisConnectionDetails.Standalone standalone = this.connectionDetails.getStandalone();
assertThat(standalone.getHost()).isEqualTo("localhost");
assertThat(standalone.getPort()).isEqualTo(6379);
assertThat(standalone.getDatabase()).isEqualTo(0);
assertThat(this.connectionDetails.getSentinel()).isNull();
assertThat(this.connectionDetails.getCluster()).isNull();
assertThat(this.connectionDetails.getUsername()).isNull();
assertThat(this.connectionDetails.getPassword()).isNull();
}
@Test
void credentialsAreConfiguredFromUrlWithUsernameAndPassword() {
this.properties.setUrl("redis://user:secret@example.com");
assertThat(this.connectionDetails.getUsername()).isEqualTo("user");
assertThat(this.connectionDetails.getPassword()).isEqualTo("secret");
}
@Test
void credentialsAreConfiguredFromUrlWithUsernameAndColon() {
this.properties.setUrl("redis://user:@example.com");
this.properties.setUsername("notused");
this.properties.setPassword("notused");
assertThat(this.connectionDetails.getUsername()).isEqualTo("user");
assertThat(this.connectionDetails.getPassword()).isEmpty();
}
@Test
void credentialsAreConfiguredFromUrlWithColonAndPassword() {
this.properties.setUrl("redis://:secret@example.com");
this.properties.setUsername("notused");
this.properties.setPassword("notused");
assertThat(this.connectionDetails.getUsername()).isEmpty();
assertThat(this.connectionDetails.getPassword()).isEqualTo("secret");
}
@Test
void credentialsAreConfiguredFromUrlWithPasswordOnly() {
this.properties.setUrl("redis://secret@example.com");
this.properties.setUsername("notused");
this.properties.setPassword("notused");
assertThat(this.connectionDetails.getUsername()).isNull();
assertThat(this.connectionDetails.getPassword()).isEqualTo("secret");
}
@Test
void credentialsAreConfiguredFromProperties() {
this.properties.setUsername("user");
this.properties.setPassword("secret");
assertThat(this.connectionDetails.getUsername()).isEqualTo("user");
assertThat(this.connectionDetails.getPassword()).isEqualTo("secret");
}
@Test
void standaloneIsConfiguredFromUrl() {
this.properties.setUrl("redis://example.com:1234/9999");
this.properties.setHost("notused");
this.properties.setPort(9999);
this.properties.setDatabase(5);
RedisConnectionDetails.Standalone standalone = this.connectionDetails.getStandalone();
assertThat(standalone.getHost()).isEqualTo("example.com");
assertThat(standalone.getPort()).isEqualTo(1234);
assertThat(standalone.getDatabase()).isEqualTo(9999);
}
@Test
void standaloneIsConfiguredFromUrlWithoutDatabase() {
this.properties.setUrl("redis://example.com:1234");
this.properties.setDatabase(5);
PropertiesRedisConnectionDetails connectionDetails = new PropertiesRedisConnectionDetails(this.properties,
null);
RedisConnectionDetails.Standalone standalone = connectionDetails.getStandalone();
assertThat(standalone.getHost()).isEqualTo("example.com");
assertThat(standalone.getPort()).isEqualTo(1234);
assertThat(standalone.getDatabase()).isEqualTo(0);
}
@Test
void standaloneIsConfiguredFromProperties() {
this.properties.setHost("example.com");
this.properties.setPort(1234);
this.properties.setDatabase(5);
RedisConnectionDetails.Standalone standalone = this.connectionDetails.getStandalone();
assertThat(standalone.getHost()).isEqualTo("example.com");
assertThat(standalone.getPort()).isEqualTo(1234);
assertThat(standalone.getDatabase()).isEqualTo(5);
}
@Test
void clusterIsConfigured() {
RedisProperties.Cluster cluster = new RedisProperties.Cluster();
cluster.setNodes(List.of("localhost:1111", "127.0.0.1:2222", "[::1]:3333"));
this.properties.setCluster(cluster);
assertThat(this.connectionDetails.getCluster().getNodes()).containsExactly(new Node("localhost", 1111),
new Node("127.0.0.1", 2222), new Node("[::1]", 3333));
}
@Test
void sentinelIsConfigured() {
RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel();
sentinel.setNodes(List.of("localhost:1111", "127.0.0.1:2222", "[::1]:3333"));
this.properties.setSentinel(sentinel);
this.properties.setDatabase(5);
PropertiesRedisConnectionDetails connectionDetails = new PropertiesRedisConnectionDetails(this.properties,
null);
assertThat(connectionDetails.getSentinel().getNodes()).containsExactly(new Node("localhost", 1111),
new Node("127.0.0.1", 2222), new Node("[::1]", 3333));
assertThat(connectionDetails.getSentinel().getDatabase()).isEqualTo(5);
}
@Test
void sentinelDatabaseIsConfiguredFromUrl() {
RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel();
sentinel.setNodes(List.of("localhost:1111", "127.0.0.1:2222", "[::1]:3333"));
this.properties.setSentinel(sentinel);
this.properties.setUrl("redis://example.com:1234/9999");
this.properties.setDatabase(5);
PropertiesRedisConnectionDetails connectionDetails = new PropertiesRedisConnectionDetails(this.properties,
null);
assertThat(connectionDetails.getSentinel().getDatabase()).isEqualTo(9999);
}
@Test
void shouldReturnSslBundle() {
SslBundle bundle1 = mock(SslBundle.class);
this.sslBundleRegistry.registerBundle("bundle-1", bundle1);
this.properties.getSsl().setBundle("bundle-1");
SslBundle sslBundle = this.connectionDetails.getStandalone().getSslBundle();
assertThat(sslBundle).isSameAs(bundle1);
}
@Test
void shouldReturnSystemBundleIfSslIsEnabledButBundleNotSet() {
this.properties.getSsl().setEnabled(true);
SslBundle sslBundle = this.connectionDetails.getStandalone().getSslBundle();
assertThat(sslBundle).isNotNull();
}
@Test
void shouldReturnNullIfSslIsNotEnabled() {
this.properties.getSsl().setEnabled(false);
SslBundle sslBundle = this.connectionDetails.getStandalone().getSslBundle();
assertThat(sslBundle).isNull();
}
}

View File

@@ -0,0 +1,389 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.assertj.SimpleAsyncTaskExecutorAssert;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RedisAutoConfiguration} when Lettuce is not on the classpath.
*
* @author Mark Paluch
* @author Stephane Nicoll
* @author Weix Sun
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
*/
@ClassPathExclusions("lettuce-core-*.jar")
class RedisAutoConfigurationJedisTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, SslAutoConfiguration.class));
@Test
void connectionFactoryDefaultsToJedis() {
this.contextRunner.run((context) -> assertThat(context.getBean("redisConnectionFactory"))
.isInstanceOf(JedisConnectionFactory.class));
}
@Test
void connectionFactoryIsNotCreatedWhenLettuceIsSelected() {
this.contextRunner.withPropertyValues("spring.data.redis.client-type=lettuce")
.run((context) -> assertThat(context).doesNotHaveBean(RedisConnectionFactory.class));
}
@Test
void testOverrideRedisConfiguration() {
this.contextRunner.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.database:1")
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getDatabase()).isOne();
assertThat(getUserName(cf)).isNull();
assertThat(cf.getPassword()).isNull();
assertThat(cf.isUseSsl()).isFalse();
});
}
@Test
void testCustomizeRedisConfiguration() {
this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.isUseSsl()).isTrue();
});
}
@Test
void usesConnectionDetailsIfAvailable() {
this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.isUseSsl()).isFalse();
});
}
@Test
void testRedisUrlConfiguration() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.url:redis://user:password@example:33")
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("example");
assertThat(cf.getPort()).isEqualTo(33);
assertThat(getUserName(cf)).isEqualTo("user");
assertThat(cf.getPassword()).isEqualTo("password");
assertThat(cf.isUseSsl()).isFalse();
});
}
@Test
void testOverrideUrlRedisConfiguration() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.password:xyz",
"spring.data.redis.port:1000", "spring.data.redis.ssl.enabled:false",
"spring.data.redis.url:rediss://user:password@example:33")
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("example");
assertThat(cf.getPort()).isEqualTo(33);
assertThat(getUserName(cf)).isEqualTo("user");
assertThat(cf.getPassword()).isEqualTo("password");
assertThat(cf.isUseSsl()).isTrue();
});
}
@Test
void testPasswordInUrlWithColon() {
this.contextRunner.withPropertyValues("spring.data.redis.url:redis://:pass:word@example:33").run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("example");
assertThat(cf.getPort()).isEqualTo(33);
assertThat(getUserName(cf)).isEmpty();
assertThat(cf.getPassword()).isEqualTo("pass:word");
});
}
@Test
void testPasswordInUrlStartsWithColon() {
this.contextRunner.withPropertyValues("spring.data.redis.url:redis://user::pass:word@example:33")
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("example");
assertThat(cf.getPort()).isEqualTo(33);
assertThat(getUserName(cf)).isEqualTo("user");
assertThat(cf.getPassword()).isEqualTo(":pass:word");
});
}
@Test
void testRedisConfigurationWithPool() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.jedis.pool.min-idle:1",
"spring.data.redis.jedis.pool.max-idle:4", "spring.data.redis.jedis.pool.max-active:16",
"spring.data.redis.jedis.pool.max-wait:2000",
"spring.data.redis.jedis.pool.time-between-eviction-runs:30000")
.withUserConfiguration(JedisDisableStartupConfiguration.class)
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getPoolConfig()).satisfies((poolConfig) -> {
assertThat(poolConfig.getMinIdle()).isOne();
assertThat(poolConfig.getMaxIdle()).isEqualTo(4);
assertThat(poolConfig.getMaxTotal()).isEqualTo(16);
assertThat(poolConfig.getMaxWaitDuration()).isEqualTo(Duration.ofSeconds(2));
assertThat(poolConfig.getDurationBetweenEvictionRuns()).isEqualTo(Duration.ofSeconds(30));
});
});
}
@Test
void testRedisConfigurationDisabledPool() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.jedis.pool.enabled:false")
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getClientConfiguration().isUsePooling()).isFalse();
});
}
@Test
void testRedisConfigurationWithTimeoutAndConnectTimeout() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.timeout:250",
"spring.data.redis.connect-timeout:1000")
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getTimeout()).isEqualTo(250);
assertThat(cf.getClientConfiguration().getConnectTimeout().toMillis()).isEqualTo(1000);
});
}
@Test
void testRedisConfigurationWithDefaultTimeouts() {
this.contextRunner.withPropertyValues("spring.data.redis.host:foo").run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getTimeout()).isEqualTo(2000);
assertThat(cf.getClientConfiguration().getConnectTimeout().toMillis()).isEqualTo(2000);
});
}
@Test
void testRedisConfigurationWithClientName() {
this.contextRunner.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.client-name:spring-boot")
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getClientName()).isEqualTo("spring-boot");
});
}
@Test
void testRedisConfigurationWithSentinel() {
this.contextRunner
.withPropertyValues("spring.data.redis.sentinel.master:mymaster",
"spring.data.redis.sentinel.nodes:127.0.0.1:26379,127.0.0.1:26380")
.withUserConfiguration(JedisConnectionFactoryCaptorConfiguration.class)
.run((context) -> assertThat(JedisConnectionFactoryCaptor.connectionFactory.isRedisSentinelAware())
.isTrue());
}
@Test
void testRedisConfigurationWithSentinelAndAuthentication() {
this.contextRunner
.withPropertyValues("spring.data.redis.username=user", "spring.data.redis.password=password",
"spring.data.redis.sentinel.master:mymaster",
"spring.data.redis.sentinel.nodes:127.0.0.1:26379,127.0.0.1:26380")
.withUserConfiguration(JedisConnectionFactoryCaptorConfiguration.class)
.run((context) -> {
assertThat(JedisConnectionFactoryCaptor.connectionFactory.isRedisSentinelAware()).isTrue();
assertThat(getUserName(JedisConnectionFactoryCaptor.connectionFactory)).isEqualTo("user");
assertThat(JedisConnectionFactoryCaptor.connectionFactory.getPassword()).isEqualTo("password");
});
}
@Test
void testRedisConfigurationWithCluster() {
this.contextRunner.withPropertyValues("spring.data.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380")
.withUserConfiguration(JedisConnectionFactoryCaptorConfiguration.class)
.run((context) -> assertThat(JedisConnectionFactoryCaptor.connectionFactory.isRedisClusterAware())
.isTrue());
}
@Test
void testRedisConfigurationWithSslEnabled() {
this.contextRunner.withPropertyValues("spring.data.redis.ssl.enabled:true").run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.isUseSsl()).isTrue();
});
}
@Test
@WithPackageResources("test.jks")
void testRedisConfigurationWithSslBundle() {
this.contextRunner
.withPropertyValues("spring.data.redis.ssl.bundle:test-bundle",
"spring.ssl.bundle.jks.test-bundle.keystore.location:classpath:test.jks",
"spring.ssl.bundle.jks.test-bundle.keystore.password:secret",
"spring.ssl.bundle.jks.test-bundle.key.password:password")
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.isUseSsl()).isTrue();
});
}
@Test
void testRedisConfigurationWithSslDisabledAndBundle() {
this.contextRunner
.withPropertyValues("spring.data.redis.ssl.enabled:false", "spring.data.redis.ssl.bundle:test-bundle")
.run((context) -> {
JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class);
assertThat(cf.isUseSsl()).isFalse();
});
}
@Test
void shouldUsePlatformThreadsByDefault() {
this.contextRunner.run((context) -> {
JedisConnectionFactory factory = context.getBean(JedisConnectionFactory.class);
assertThat(factory).extracting("executor").isNull();
});
}
@Test
@EnabledForJreRange(min = JRE.JAVA_21)
void shouldUseVirtualThreadsIfEnabled() {
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> {
JedisConnectionFactory factory = context.getBean(JedisConnectionFactory.class);
assertThat(factory).extracting("executor")
.satisfies((executor) -> SimpleAsyncTaskExecutorAssert.assertThat((SimpleAsyncTaskExecutor) executor)
.usesVirtualThreads());
});
}
private String getUserName(JedisConnectionFactory factory) {
return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername");
}
@Configuration(proxyBeanMethods = false)
static class CustomConfiguration {
@Bean
JedisClientConfigurationBuilderCustomizer customizer() {
return JedisClientConfigurationBuilder::useSsl;
}
}
@Configuration(proxyBeanMethods = false)
static class ConnectionDetailsConfiguration {
@Bean
RedisConnectionDetails redisConnectionDetails() {
return new RedisConnectionDetails() {
@Override
public Standalone getStandalone() {
return new Standalone() {
@Override
public String getHost() {
return "localhost";
}
@Override
public int getPort() {
return 6379;
}
};
}
};
}
}
@Configuration(proxyBeanMethods = false)
static class JedisConnectionFactoryCaptorConfiguration {
@Bean
static JedisConnectionFactoryCaptor jedisConnectionFactoryCaptor() {
return new JedisConnectionFactoryCaptor();
}
}
static class JedisConnectionFactoryCaptor implements BeanPostProcessor {
static JedisConnectionFactory connectionFactory;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof JedisConnectionFactory jedisConnectionFactory) {
connectionFactory = jedisConnectionFactory;
}
return bean;
}
}
@Configuration(proxyBeanMethods = false)
static class JedisDisableStartupConfiguration {
@Bean
static BeanPostProcessor jedisDisableStartup() {
return new BeanPostProcessor() {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof JedisConnectionFactory jedisConnectionFactory) {
jedisConnectionFactory.setEarlyStartup(false);
jedisConnectionFactory.setAutoStartup(false);
}
return bean;
}
};
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RedisAutoConfiguration} when commons-pool2 is not on the classpath.
*
* @author Stephane Nicoll
*/
@ClassPathExclusions("commons-pool2-*.jar")
class RedisAutoConfigurationLettuceWithoutCommonsPool2Tests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class));
@Test
void poolWithoutCommonsPool2IsDisabledByDefault() {
this.contextRunner.withPropertyValues("spring.data.redis.host:foo").run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getClientConfiguration()).isNotInstanceOf(LettucePoolingClientConfiguration.class);
});
}
}

View File

@@ -0,0 +1,886 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import java.time.Duration;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.ReadFrom.Nodes;
import io.lettuce.core.RedisURI;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.RefreshTrigger;
import io.lettuce.core.cluster.models.partitions.RedisClusterNode;
import io.lettuce.core.models.role.RedisNodeDescription;
import io.lettuce.core.resource.DefaultClientResources;
import io.lettuce.core.tracing.Tracing;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Pool;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.testsupport.assertj.SimpleAsyncTaskExecutorAssert;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
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.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RedisAutoConfiguration}.
*
* @author Dave Syer
* @author Christian Dupuis
* @author Christoph Strobl
* @author Eddú Meléndez
* @author Marco Aust
* @author Mark Paluch
* @author Stephane Nicoll
* @author Alen Turkovic
* @author Scott Frederick
* @author Weix Sun
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
class RedisAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, SslAutoConfiguration.class));
@Test
void testDefaultRedisConfiguration() {
this.contextRunner.run((context) -> {
assertThat(context.getBean("redisTemplate")).isInstanceOf(RedisOperations.class);
assertThat(context).hasSingleBean(StringRedisTemplate.class);
assertThat(context).hasSingleBean(RedisConnectionFactory.class);
assertThat(context.getBean(RedisConnectionFactory.class)).isInstanceOf(LettuceConnectionFactory.class);
});
}
@Test
void testOverrideRedisConfiguration() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.database:1",
"spring.data.redis.lettuce.shutdown-timeout:500")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getDatabase()).isOne();
assertThat(getUserName(cf)).isNull();
assertThat(cf.getPassword()).isNull();
assertThat(cf.isUseSsl()).isFalse();
assertThat(cf.getShutdownTimeout()).isEqualTo(500);
});
}
@ParameterizedTest(name = "{0}")
@MethodSource
void shouldConfigureLettuceReadFromProperty(String type, ReadFrom readFrom) {
this.contextRunner.withPropertyValues("spring.data.redis.lettuce.read-from:" + type).run((context) -> {
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
LettuceClientConfiguration configuration = factory.getClientConfiguration();
assertThat(configuration.getReadFrom()).hasValue(readFrom);
});
}
static Stream<Arguments> shouldConfigureLettuceReadFromProperty() {
return Stream.of(Arguments.of("any", ReadFrom.ANY), Arguments.of("any-replica", ReadFrom.ANY_REPLICA),
Arguments.of("lowest-latency", ReadFrom.LOWEST_LATENCY), Arguments.of("replica", ReadFrom.REPLICA),
Arguments.of("replica-preferred", ReadFrom.REPLICA_PREFERRED),
Arguments.of("upstream", ReadFrom.UPSTREAM),
Arguments.of("upstream-preferred", ReadFrom.UPSTREAM_PREFERRED));
}
@Test
void shouldConfigureLettuceRegexReadFromProperty() {
RedisClusterNode node1 = createRedisNode("redis-node-1.region-1.example.com");
RedisClusterNode node2 = createRedisNode("redis-node-2.region-1.example.com");
RedisClusterNode node3 = createRedisNode("redis-node-1.region-2.example.com");
RedisClusterNode node4 = createRedisNode("redis-node-2.region-2.example.com");
this.contextRunner.withPropertyValues("spring.data.redis.lettuce.read-from:regex:.*region-1.*")
.run((context) -> {
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
LettuceClientConfiguration configuration = factory.getClientConfiguration();
assertThat(configuration.getReadFrom()).hasValueSatisfying((readFrom) -> {
List<RedisNodeDescription> result = readFrom.select(new RedisNodes(node1, node2, node3, node4));
assertThat(result).hasSize(2).containsExactly(node1, node2);
});
});
}
@Test
void shouldConfigureLettuceSubnetReadFromProperty() {
RedisClusterNode nodeInSubnetIpv4 = createRedisNode("192.0.2.1");
RedisClusterNode nodeNotInSubnetIpv4 = createRedisNode("198.51.100.1");
RedisClusterNode nodeInSubnetIpv6 = createRedisNode("2001:db8:abcd:0000::1");
RedisClusterNode nodeNotInSubnetIpv6 = createRedisNode("2001:db8:abcd:1000::");
this.contextRunner
.withPropertyValues("spring.data.redis.lettuce.read-from:subnet:192.0.2.0/24,2001:db8:abcd:0000::/52")
.run((context) -> {
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
LettuceClientConfiguration configuration = factory.getClientConfiguration();
assertThat(configuration.getReadFrom()).hasValueSatisfying((readFrom) -> {
List<RedisNodeDescription> result = readFrom.select(new RedisNodes(nodeInSubnetIpv4,
nodeNotInSubnetIpv4, nodeInSubnetIpv6, nodeNotInSubnetIpv6));
assertThat(result).hasSize(2).containsExactly(nodeInSubnetIpv4, nodeInSubnetIpv6);
});
});
}
@Test
void testCustomizeClientResources() {
Tracing tracing = mock(Tracing.class);
this.contextRunner.withBean(ClientResourcesBuilderCustomizer.class, () -> (builder) -> builder.tracing(tracing))
.run((context) -> {
DefaultClientResources clientResources = context.getBean(DefaultClientResources.class);
assertThat(clientResources.tracing()).isEqualTo(tracing);
});
}
@Test
void testCustomizeRedisConfiguration() {
this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.isUseSsl()).isTrue();
assertThat(cf.getClientConfiguration().getClientOptions())
.hasValueSatisfying((options) -> assertThat(options.isAutoReconnect()).isFalse());
});
}
@Test
void testRedisUrlConfiguration() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.url:redis://user:password@example:33")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("example");
assertThat(cf.getPort()).isEqualTo(33);
assertThat(getUserName(cf)).isEqualTo("user");
assertThat(cf.getPassword()).isEqualTo("password");
assertThat(cf.isUseSsl()).isFalse();
});
}
@Test
void testOverrideUrlRedisConfiguration() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.redis.data.user:alice",
"spring.data.redis.password:xyz", "spring.data.redis.port:1000",
"spring.data.redis.ssl.enabled:false", "spring.data.redis.url:rediss://user:password@example:33")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("example");
assertThat(cf.getPort()).isEqualTo(33);
assertThat(getUserName(cf)).isEqualTo("user");
assertThat(cf.getPassword()).isEqualTo("password");
assertThat(cf.isUseSsl()).isTrue();
});
}
@Test
void testPasswordInUrlWithColon() {
this.contextRunner.withPropertyValues("spring.data.redis.url:redis://:pass:word@example:33").run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("example");
assertThat(cf.getPort()).isEqualTo(33);
assertThat(getUserName(cf)).isEmpty();
assertThat(cf.getPassword()).isEqualTo("pass:word");
});
}
@Test
void testPasswordInUrlStartsWithColon() {
this.contextRunner.withPropertyValues("spring.data.redis.url:redis://user::pass:word@example:33")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("example");
assertThat(cf.getPort()).isEqualTo(33);
assertThat(getUserName(cf)).isEqualTo("user");
assertThat(cf.getPassword()).isEqualTo(":pass:word");
});
}
@Test
void testRedisConfigurationUsePoolByDefault() {
Pool defaultPool = new RedisProperties().getLettuce().getPool();
this.contextRunner.withPropertyValues("spring.data.redis.host:foo").run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
GenericObjectPoolConfig<?> poolConfig = getPoolingClientConfiguration(cf).getPoolConfig();
assertThat(poolConfig.getMinIdle()).isEqualTo(defaultPool.getMinIdle());
assertThat(poolConfig.getMaxIdle()).isEqualTo(defaultPool.getMaxIdle());
assertThat(poolConfig.getMaxTotal()).isEqualTo(defaultPool.getMaxActive());
assertThat(poolConfig.getMaxWaitDuration()).isEqualTo(defaultPool.getMaxWait());
});
}
@Test
void testRedisConfigurationWithCustomPoolSettings() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.lettuce.pool.min-idle:1",
"spring.data.redis.lettuce.pool.max-idle:4", "spring.data.redis.lettuce.pool.max-active:16",
"spring.data.redis.lettuce.pool.max-wait:2000",
"spring.data.redis.lettuce.pool.time-between-eviction-runs:30000",
"spring.data.redis.lettuce.shutdown-timeout:1000")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
GenericObjectPoolConfig<?> poolConfig = getPoolingClientConfiguration(cf).getPoolConfig();
assertThat(poolConfig.getMinIdle()).isOne();
assertThat(poolConfig.getMaxIdle()).isEqualTo(4);
assertThat(poolConfig.getMaxTotal()).isEqualTo(16);
assertThat(poolConfig.getMaxWaitDuration()).isEqualTo(Duration.ofSeconds(2));
assertThat(poolConfig.getDurationBetweenEvictionRuns()).isEqualTo(Duration.ofSeconds(30));
assertThat(cf.getShutdownTimeout()).isEqualTo(1000);
});
}
@Test
void testRedisConfigurationDisabledPool() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.lettuce.pool.enabled:false")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getClientConfiguration()).isNotInstanceOf(LettucePoolingClientConfiguration.class);
});
}
@Test
void testRedisConfigurationWithTimeoutAndConnectTimeout() {
this.contextRunner
.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.timeout:250",
"spring.data.redis.connect-timeout:1000")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getTimeout()).isEqualTo(250);
assertThat(cf.getClientConfiguration()
.getClientOptions()
.get()
.getSocketOptions()
.getConnectTimeout()
.toMillis()).isEqualTo(1000);
});
}
@Test
void testRedisConfigurationWithDefaultTimeouts() {
this.contextRunner.withPropertyValues("spring.data.redis.host:foo").run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getTimeout()).isEqualTo(60000);
assertThat(cf.getClientConfiguration()
.getClientOptions()
.get()
.getSocketOptions()
.getConnectTimeout()
.toMillis()).isEqualTo(10000);
});
}
@Test
void testRedisConfigurationWithCustomBean() {
this.contextRunner.withUserConfiguration(RedisStandaloneConfig.class).run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
});
}
@Test
void testRedisConfigurationWithClientName() {
this.contextRunner.withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.client-name:spring-boot")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.getHostName()).isEqualTo("foo");
assertThat(cf.getClientName()).isEqualTo("spring-boot");
});
}
@Test
void connectionFactoryWithJedisClientType() {
this.contextRunner.withPropertyValues("spring.data.redis.client-type:jedis").run((context) -> {
assertThat(context).hasSingleBean(RedisConnectionFactory.class);
assertThat(context.getBean(RedisConnectionFactory.class)).isInstanceOf(JedisConnectionFactory.class);
});
}
@Test
void connectionFactoryWithLettuceClientType() {
this.contextRunner.withPropertyValues("spring.data.redis.client-type:lettuce").run((context) -> {
assertThat(context).hasSingleBean(RedisConnectionFactory.class);
assertThat(context.getBean(RedisConnectionFactory.class)).isInstanceOf(LettuceConnectionFactory.class);
});
}
@Test
void testRedisConfigurationWithSentinel() {
List<String> sentinels = Arrays.asList("127.0.0.1:26379", "127.0.0.1:26380");
this.contextRunner
.withPropertyValues("spring.data.redis.sentinel.master:mymaster",
"spring.data.redis.sentinel.nodes:" + StringUtils.collectionToCommaDelimitedString(sentinels))
.run((context) -> assertThat(context.getBean(LettuceConnectionFactory.class).isRedisSentinelAware())
.isTrue());
}
@Test
void testRedisConfigurationWithIpv6Sentinel() {
List<String> sentinels = Arrays.asList("[0:0:0:0:0:0:0:1]:26379", "[0:0:0:0:0:0:0:1]:26380");
this.contextRunner
.withPropertyValues("spring.data.redis.sentinel.master:mymaster",
"spring.data.redis.sentinel.nodes:" + StringUtils.collectionToCommaDelimitedString(sentinels))
.run((context) -> {
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
assertThat(connectionFactory.isRedisSentinelAware()).isTrue();
assertThat(connectionFactory.getSentinelConfiguration().getSentinels()).isNotNull()
.containsExactlyInAnyOrder(new RedisNode("[0:0:0:0:0:0:0:1]", 26379),
new RedisNode("[0:0:0:0:0:0:0:1]", 26380));
});
}
@Test
void testRedisConfigurationWithSentinelAndDatabase() {
this.contextRunner
.withPropertyValues("spring.data.redis.database:1", "spring.data.redis.sentinel.master:mymaster",
"spring.data.redis.sentinel.nodes:127.0.0.1:26379, 127.0.0.1:26380")
.run((context) -> {
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
assertThat(connectionFactory.getDatabase()).isOne();
assertThat(connectionFactory.isRedisSentinelAware()).isTrue();
});
}
@Test
void testRedisConfigurationWithSentinelAndAuthentication() {
this.contextRunner
.withPropertyValues("spring.data.redis.username=user", "spring.data.redis.password=password",
"spring.data.redis.sentinel.master:mymaster",
"spring.data.redis.sentinel.nodes:127.0.0.1:26379, 127.0.0.1:26380")
.run(assertSentinelConfiguration("user", "password", (sentinelConfiguration) -> {
assertThat(sentinelConfiguration.getSentinelPassword().isPresent()).isFalse();
Set<RedisNode> sentinels = sentinelConfiguration.getSentinels();
assertThat(sentinels.stream().map(Object::toString).collect(Collectors.toSet()))
.contains("127.0.0.1:26379", "127.0.0.1:26380");
}));
}
@Test
void testRedisConfigurationWithSentinelPasswordAndDataNodePassword() {
this.contextRunner
.withPropertyValues("spring.data.redis.password=password", "spring.data.redis.sentinel.password=secret",
"spring.data.redis.sentinel.master:mymaster",
"spring.data.redis.sentinel.nodes:127.0.0.1:26379, 127.0.0.1:26380")
.run(assertSentinelConfiguration(null, "password", (sentinelConfiguration) -> {
assertThat(sentinelConfiguration.getSentinelUsername()).isNull();
assertThat(new String(sentinelConfiguration.getSentinelPassword().get())).isEqualTo("secret");
Set<RedisNode> sentinels = sentinelConfiguration.getSentinels();
assertThat(sentinels.stream().map(Object::toString).collect(Collectors.toSet()))
.contains("127.0.0.1:26379", "127.0.0.1:26380");
}));
}
@Test
void testRedisConfigurationWithSentinelAuthenticationAndDataNodeAuthentication() {
this.contextRunner
.withPropertyValues("spring.data.redis.username=username", "spring.data.redis.password=password",
"spring.data.redis.sentinel.username=sentinel", "spring.data.redis.sentinel.password=secret",
"spring.data.redis.sentinel.master:mymaster",
"spring.data.redis.sentinel.nodes:127.0.0.1:26379, 127.0.0.1:26380")
.run(assertSentinelConfiguration("username", "password", (sentinelConfiguration) -> {
assertThat(sentinelConfiguration.getSentinelUsername()).isEqualTo("sentinel");
assertThat(new String(sentinelConfiguration.getSentinelPassword().get())).isEqualTo("secret");
Set<RedisNode> sentinels = sentinelConfiguration.getSentinels();
assertThat(sentinels.stream().map(Object::toString).collect(Collectors.toSet()))
.contains("127.0.0.1:26379", "127.0.0.1:26380");
}));
}
private ContextConsumer<AssertableApplicationContext> assertSentinelConfiguration(String userName, String password,
Consumer<RedisSentinelConfiguration> sentinelConfiguration) {
return (context) -> {
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
assertThat(getUserName(connectionFactory)).isEqualTo(userName);
assertThat(connectionFactory.getPassword()).isEqualTo(password);
assertThat(connectionFactory.getSentinelConfiguration()).satisfies(sentinelConfiguration);
};
}
@Test
void testRedisSentinelUrlConfiguration() {
this.contextRunner
.withPropertyValues(
"spring.data.redis.url=redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster")
.run((context) -> assertThatIllegalStateException()
.isThrownBy(() -> context.getBean(LettuceConnectionFactory.class))
.withRootCauseInstanceOf(RedisUrlSyntaxException.class)
.havingRootCause()
.withMessageContaining(
"Invalid Redis URL 'redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster'"));
}
@Test
void testRedisConfigurationWithCluster() {
List<String> clusterNodes = Arrays.asList("127.0.0.1:27379", "127.0.0.1:27380", "[::1]:27381");
this.contextRunner
.withPropertyValues("spring.data.redis.cluster.nodes[0]:" + clusterNodes.get(0),
"spring.data.redis.cluster.nodes[1]:" + clusterNodes.get(1),
"spring.data.redis.cluster.nodes[2]:" + clusterNodes.get(2))
.run((context) -> {
RedisClusterConfiguration clusterConfiguration = context.getBean(LettuceConnectionFactory.class)
.getClusterConfiguration();
assertThat(clusterConfiguration.getClusterNodes()).hasSize(3);
assertThat(clusterConfiguration.getClusterNodes()).containsExactlyInAnyOrder(
new RedisNode("127.0.0.1", 27379), new RedisNode("127.0.0.1", 27380),
new RedisNode("[::1]", 27381));
});
}
@Test
void testRedisConfigurationWithClusterAndAuthentication() {
List<String> clusterNodes = Arrays.asList("127.0.0.1:27379", "127.0.0.1:27380");
this.contextRunner
.withPropertyValues("spring.data.redis.username=user", "spring.data.redis.password=password",
"spring.data.redis.cluster.nodes[0]:" + clusterNodes.get(0),
"spring.data.redis.cluster.nodes[1]:" + clusterNodes.get(1))
.run((context) -> {
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
assertThat(getUserName(connectionFactory)).isEqualTo("user");
assertThat(connectionFactory.getPassword()).isEqualTo("password");
}
);
}
@Test
void testRedisConfigurationCreateClientOptionsByDefault() {
this.contextRunner.run(assertClientOptions(ClientOptions.class, (options) -> {
assertThat(options.getTimeoutOptions().isApplyConnectionTimeout()).isTrue();
assertThat(options.getTimeoutOptions().isTimeoutCommands()).isTrue();
}));
}
@Test
void testRedisConfigurationWithClusterCreateClusterClientOptions() {
this.contextRunner.withPropertyValues("spring.data.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380")
.run(assertClientOptions(ClusterClientOptions.class, (options) -> {
assertThat(options.getTimeoutOptions().isApplyConnectionTimeout()).isTrue();
assertThat(options.getTimeoutOptions().isTimeoutCommands()).isTrue();
}));
}
@Test
void testRedisConfigurationWithClusterRefreshPeriod() {
this.contextRunner
.withPropertyValues("spring.data.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380",
"spring.data.redis.lettuce.cluster.refresh.period=30s")
.run(assertClientOptions(ClusterClientOptions.class,
(options) -> assertThat(options.getTopologyRefreshOptions().getRefreshPeriod()).hasSeconds(30)));
}
@Test
void testRedisConfigurationWithClusterAdaptiveRefresh() {
this.contextRunner
.withPropertyValues("spring.data.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380",
"spring.data.redis.lettuce.cluster.refresh.adaptive=true")
.run(assertClientOptions(ClusterClientOptions.class,
(options) -> assertThat(options.getTopologyRefreshOptions().getAdaptiveRefreshTriggers())
.isEqualTo(EnumSet.allOf(RefreshTrigger.class))));
}
@Test
void testRedisConfigurationWithClusterRefreshPeriodHasNoEffectWithNonClusteredConfiguration() {
this.contextRunner.withPropertyValues("spring.data.redis.cluster.refresh.period=30s")
.run(assertClientOptions(ClientOptions.class,
(options) -> assertThat(options.getClass()).isEqualTo(ClientOptions.class)));
}
@Test
void testRedisConfigurationWithClusterDynamicRefreshSourcesEnabled() {
this.contextRunner
.withPropertyValues("spring.data.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380",
"spring.data.redis.lettuce.cluster.refresh.dynamic-refresh-sources=true")
.run(assertClientOptions(ClusterClientOptions.class,
(options) -> assertThat(options.getTopologyRefreshOptions().useDynamicRefreshSources()).isTrue()));
}
@Test
void testRedisConfigurationWithClusterDynamicRefreshSourcesDisabled() {
this.contextRunner
.withPropertyValues("spring.data.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380",
"spring.data.redis.lettuce.cluster.refresh.dynamic-refresh-sources=false")
.run(assertClientOptions(ClusterClientOptions.class,
(options) -> assertThat(options.getTopologyRefreshOptions().useDynamicRefreshSources()).isFalse()));
}
@Test
void testRedisConfigurationWithClusterDynamicSourcesUnspecifiedUsesDefault() {
this.contextRunner
.withPropertyValues("spring.data.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380",
"spring.data.redis.lettuce.cluster.refresh.dynamic-sources=")
.run(assertClientOptions(ClusterClientOptions.class,
(options) -> assertThat(options.getTopologyRefreshOptions().useDynamicRefreshSources()).isTrue()));
}
@Test
void definesPropertiesBasedConnectionDetailsByDefault() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PropertiesRedisConnectionDetails.class));
}
@Test
void usesStandaloneFromCustomConnectionDetails() {
this.contextRunner.withUserConfiguration(ConnectionDetailsStandaloneConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(RedisConnectionDetails.class)
.doesNotHaveBean(PropertiesRedisConnectionDetails.class);
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.isUseSsl()).isFalse();
RedisStandaloneConfiguration configuration = cf.getStandaloneConfiguration();
assertThat(configuration.getHostName()).isEqualTo("redis.example.com");
assertThat(configuration.getPort()).isEqualTo(16379);
assertThat(configuration.getDatabase()).isOne();
assertThat(configuration.getUsername()).isEqualTo("user-1");
assertThat(configuration.getPassword()).isEqualTo(RedisPassword.of("password-1"));
});
}
@Test
void usesSentinelFromCustomConnectionDetails() {
this.contextRunner.withUserConfiguration(ConnectionDetailsSentinelConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(RedisConnectionDetails.class)
.doesNotHaveBean(PropertiesRedisConnectionDetails.class);
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.isUseSsl()).isFalse();
RedisSentinelConfiguration configuration = cf.getSentinelConfiguration();
assertThat(configuration).isNotNull();
assertThat(configuration.getSentinelUsername()).isEqualTo("sentinel-1");
assertThat(configuration.getSentinelPassword().get()).isEqualTo("secret-1".toCharArray());
assertThat(configuration.getSentinels()).containsExactly(new RedisNode("node-1", 12345));
assertThat(configuration.getUsername()).isEqualTo("user-1");
assertThat(configuration.getPassword()).isEqualTo(RedisPassword.of("password-1"));
assertThat(configuration.getDatabase()).isOne();
assertThat(configuration.getMaster().getName()).isEqualTo("master.redis.example.com");
});
}
@Test
void usesClusterFromCustomConnectionDetails() {
this.contextRunner.withUserConfiguration(ConnectionDetailsClusterConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(RedisConnectionDetails.class)
.doesNotHaveBean(PropertiesRedisConnectionDetails.class);
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.isUseSsl()).isFalse();
RedisClusterConfiguration configuration = cf.getClusterConfiguration();
assertThat(configuration).isNotNull();
assertThat(configuration.getUsername()).isEqualTo("user-1");
assertThat(configuration.getPassword().get()).isEqualTo("password-1".toCharArray());
assertThat(configuration.getClusterNodes()).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) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.isUseSsl()).isTrue();
});
}
@Test
@WithPackageResources("test.jks")
void testRedisConfigurationWithSslBundle() {
this.contextRunner
.withPropertyValues("spring.data.redis.ssl.bundle:test-bundle",
"spring.ssl.bundle.jks.test-bundle.keystore.location:classpath:test.jks",
"spring.ssl.bundle.jks.test-bundle.keystore.password:secret",
"spring.ssl.bundle.jks.test-bundle.key.password:password")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.isUseSsl()).isTrue();
});
}
@Test
void testRedisConfigurationWithSslDisabledBundle() {
this.contextRunner
.withPropertyValues("spring.data.redis.ssl.enabled:false", "spring.data.redis.ssl.bundle:test-bundle")
.run((context) -> {
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.isUseSsl()).isFalse();
});
}
@Test
void shouldUsePlatformThreadsByDefault() {
this.contextRunner.run((context) -> {
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
assertThat(factory).extracting("executor").isNull();
});
}
@Test
@EnabledForJreRange(min = JRE.JAVA_21)
void shouldUseVirtualThreadsIfEnabled() {
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> {
LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class);
assertThat(factory).extracting("executor")
.satisfies((executor) -> SimpleAsyncTaskExecutorAssert.assertThat((SimpleAsyncTaskExecutor) executor)
.usesVirtualThreads());
});
}
private <T extends ClientOptions> ContextConsumer<AssertableApplicationContext> assertClientOptions(
Class<T> expectedType, Consumer<T> options) {
return (context) -> {
LettuceClientConfiguration clientConfiguration = context.getBean(LettuceConnectionFactory.class)
.getClientConfiguration();
assertThat(clientConfiguration.getClientOptions()).isPresent();
ClientOptions clientOptions = clientConfiguration.getClientOptions().get();
assertThat(clientOptions.getClass()).isEqualTo(expectedType);
options.accept(expectedType.cast(clientOptions));
};
}
private LettucePoolingClientConfiguration getPoolingClientConfiguration(LettuceConnectionFactory factory) {
return (LettucePoolingClientConfiguration) factory.getClientConfiguration();
}
private String getUserName(LettuceConnectionFactory factory) {
return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername");
}
private RedisClusterNode createRedisNode(String host) {
RedisClusterNode node = new RedisClusterNode();
node.setUri(RedisURI.Builder.redis(host).build());
return node;
}
private static final class RedisNodes implements Nodes {
private final List<RedisNodeDescription> descriptions;
RedisNodes(RedisNodeDescription... descriptions) {
this.descriptions = List.of(descriptions);
}
@Override
public List<RedisNodeDescription> getNodes() {
return this.descriptions;
}
@Override
public Iterator<RedisNodeDescription> iterator() {
return this.descriptions.iterator();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomConfiguration {
@Bean
LettuceClientConfigurationBuilderCustomizer customizer() {
return LettuceClientConfigurationBuilder::useSsl;
}
@Bean
LettuceClientOptionsBuilderCustomizer clientOptionsBuilderCustomizer() {
return (builder) -> builder.autoReconnect(false);
}
}
@Configuration(proxyBeanMethods = false)
static class RedisStandaloneConfig {
@Bean
RedisStandaloneConfiguration standaloneConfiguration() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName("foo");
return config;
}
}
@Configuration(proxyBeanMethods = false)
static class ConnectionDetailsStandaloneConfiguration {
@Bean
RedisConnectionDetails redisConnectionDetails() {
return new RedisConnectionDetails() {
@Override
public String getUsername() {
return "user-1";
}
@Override
public String getPassword() {
return "password-1";
}
@Override
public Standalone getStandalone() {
return new Standalone() {
@Override
public int getDatabase() {
return 1;
}
@Override
public String getHost() {
return "redis.example.com";
}
@Override
public int getPort() {
return 16379;
}
};
}
};
}
}
@Configuration(proxyBeanMethods = false)
static class ConnectionDetailsSentinelConfiguration {
@Bean
RedisConnectionDetails redisConnectionDetails() {
return new RedisConnectionDetails() {
@Override
public String getUsername() {
return "user-1";
}
@Override
public String getPassword() {
return "password-1";
}
@Override
public Sentinel getSentinel() {
return new Sentinel() {
@Override
public int getDatabase() {
return 1;
}
@Override
public String getMaster() {
return "master.redis.example.com";
}
@Override
public List<Node> getNodes() {
return List.of(new Node("node-1", 12345));
}
@Override
public String getUsername() {
return "sentinel-1";
}
@Override
public String getPassword() {
return "secret-1";
}
};
}
};
}
}
@Configuration(proxyBeanMethods = false)
static class ConnectionDetailsClusterConfiguration {
@Bean
RedisConnectionDetails redisConnectionDetails() {
return new RedisConnectionDetails() {
@Override
public String getUsername() {
return "user-1";
}
@Override
public String getPassword() {
return "password-1";
}
@Override
public Cluster getCluster() {
return new Cluster() {
@Override
public List<Node> getNodes() {
return List.of(new Node("node-1", 12345), new Node("node-2", 23456));
}
};
}
};
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Lettuce;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RedisProperties}.
*
* @author Stephane Nicoll
*/
class RedisPropertiesTests {
@Test
void lettuceDefaultsAreConsistent() {
Lettuce lettuce = new RedisProperties().getLettuce();
ClusterTopologyRefreshOptions defaultClusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
.build();
assertThat(lettuce.getCluster().getRefresh().isDynamicRefreshSources())
.isEqualTo(defaultClusterTopologyRefreshOptions.useDynamicRefreshSources());
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RedisReactiveAutoConfiguration}.
*
* @author Stephane Nicoll
*/
class RedisReactiveAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class));
@Test
void testDefaultRedisConfiguration() {
this.contextRunner.run((context) -> {
Map<String, ?> beans = context.getBeansOfType(ReactiveRedisTemplate.class);
assertThat(beans).containsOnlyKeys("reactiveRedisTemplate", "reactiveStringRedisTemplate");
});
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.diagnostics.FailureAnalysis;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RedisUrlSyntaxFailureAnalyzer}.
*
* @author Scott Frederick
*/
class RedisUrlSyntaxFailureAnalyzerTests {
@Test
void analyzeInvalidUrlSyntax() {
RedisUrlSyntaxException exception = new RedisUrlSyntaxException("redis://invalid");
FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception);
assertThat(analysis.getDescription()).contains("The URL 'redis://invalid' is not valid");
assertThat(analysis.getAction()).contains("Review the value of the property 'spring.data.redis.url'");
}
@Test
void analyzeRedisHttpUrl() {
RedisUrlSyntaxException exception = new RedisUrlSyntaxException("http://127.0.0.1:26379/mymaster");
FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception);
assertThat(analysis.getDescription()).contains("The URL 'http://127.0.0.1:26379/mymaster' is not valid")
.contains("The scheme 'http' is not supported");
assertThat(analysis.getAction()).contains("Use the scheme 'redis://' for insecure or 'rediss://' for secure");
}
@Test
void analyzeRedisSentinelUrl() {
RedisUrlSyntaxException exception = new RedisUrlSyntaxException(
"redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster");
FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception);
assertThat(analysis.getDescription()).contains(
"The URL 'redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster' is not valid")
.contains("The scheme 'redis-sentinel' is not supported");
assertThat(analysis.getAction()).contains("Use spring.data.redis.sentinel properties");
}
@Test
void analyzeRedisSocketUrl() {
RedisUrlSyntaxException exception = new RedisUrlSyntaxException("redis-socket:///redis/redis.sock");
FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception);
assertThat(analysis.getDescription()).contains("The URL 'redis-socket:///redis/redis.sock' is not valid")
.contains("The scheme 'redis-socket' is not supported");
assertThat(analysis.getAction()).contains("Configure the appropriate Spring Data Redis connection beans");
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2025 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.data.redis.domain.city;
import java.io.Serializable;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@RedisHash("cities")
public class City implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Long id;
private String name;
private String state;
private String country;
private String map;
protected City() {
}
public City(String name, String country) {
this.name = name;
this.country = country;
}
public String getName() {
return this.name;
}
public String getState() {
return this.state;
}
public String getCountry() {
return this.country;
}
public String getMap() {
return this.map;
}
@Override
public String toString() {
return getName() + "," + getState() + "," + getCountry();
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2025 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.data.redis.domain.city;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.Repository;
public interface CityRepository extends Repository<City, Long> {
Page<City> findAll(Pageable pageable);
Page<City> findByNameLikeAndCountryLikeAllIgnoringCase(String name, String country, Pageable pageable);
City findByNameAndCountryAllIgnoringCase(String name, String country);
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2012-2025 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.data.redis.domain.empty;
public class EmptyPackage {
}