Create spring-boot-data-redis module
This commit is contained in:
committed by
Phillip Webb
parent
10db864d35
commit
fa0bdc895f
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 com.redis.testcontainers.RedisContainer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.data.redis.domain.city.City;
|
||||
import org.springframework.boot.data.redis.domain.city.CityRepository;
|
||||
import org.springframework.boot.data.redis.domain.empty.EmptyPackage;
|
||||
import org.springframework.boot.test.util.TestPropertyValues;
|
||||
import org.springframework.boot.testsupport.container.TestImage;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link RedisRepositoriesAutoConfiguration}.
|
||||
*
|
||||
* @author Eddú Meléndez
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class RedisRepositoriesAutoConfigurationTests {
|
||||
|
||||
@Container
|
||||
public static RedisContainer redis = TestImage.container(RedisContainer.class);
|
||||
|
||||
private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
TestPropertyValues
|
||||
.of("spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort())
|
||||
.applyTo(this.context.getEnvironment());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void close() {
|
||||
this.context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultRepositoryConfiguration() {
|
||||
this.context.register(TestConfiguration.class, RedisAutoConfiguration.class,
|
||||
RedisRepositoriesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
assertThat(this.context.getBean(CityRepository.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoRepositoryConfiguration() {
|
||||
this.context.register(EmptyConfiguration.class, RedisAutoConfiguration.class,
|
||||
RedisRepositoriesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
assertThat(this.context.getBean("redisTemplate")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotTriggerDefaultRepositoryDetectionIfCustomized() {
|
||||
this.context.register(CustomizedConfiguration.class, RedisAutoConfiguration.class,
|
||||
RedisRepositoriesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class);
|
||||
this.context.refresh();
|
||||
assertThat(this.context.getBean(CityRepository.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@TestAutoConfigurationPackage(City.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@TestAutoConfigurationPackage(EmptyPackage.class)
|
||||
static class EmptyConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@TestAutoConfigurationPackage(RedisRepositoriesAutoConfigurationTests.class)
|
||||
@EnableRedisRepositories(basePackageClasses = CityRepository.class)
|
||||
static class CustomizedConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.resource.ClientResources;
|
||||
import io.lettuce.core.resource.ClientResources.Builder;
|
||||
|
||||
/**
|
||||
* Callback interface that can be implemented by beans wishing to customize the
|
||||
* {@link ClientResources} through a {@link Builder} whilst retaining default
|
||||
* auto-configuration.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public interface ClientResourcesBuilderCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the {@link Builder}.
|
||||
* @param clientResourcesBuilder the builder to customize
|
||||
*/
|
||||
void customize(Builder clientResourcesBuilder);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.springframework.data.redis.connection.jedis.JedisClientConfiguration;
|
||||
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder;
|
||||
|
||||
/**
|
||||
* Callback interface that can be implemented by beans wishing to customize the
|
||||
* {@link JedisClientConfiguration} through a {@link JedisClientConfigurationBuilder
|
||||
* JedisClientConfiguration.JedisClientConfigurationBuilder} whilst retaining default
|
||||
* auto-configuration.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface JedisClientConfigurationBuilderCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the {@link JedisClientConfigurationBuilder}.
|
||||
* @param clientConfigurationBuilder the builder to customize
|
||||
*/
|
||||
void customize(JedisClientConfigurationBuilder clientConfigurationBuilder);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 javax.net.ssl.SSLParameters;
|
||||
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
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.autoconfigure.thread.Threading;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
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.RedisSentinelConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
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;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnection;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Redis connection configuration using Jedis.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Stephane Nicoll
|
||||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })
|
||||
@ConditionalOnMissingBean(RedisConnectionFactory.class)
|
||||
@ConditionalOnProperty(name = "spring.data.redis.client-type", havingValue = "jedis", matchIfMissing = true)
|
||||
class JedisConnectionConfiguration extends RedisConnectionConfiguration {
|
||||
|
||||
JedisConnectionConfiguration(RedisProperties properties,
|
||||
ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
|
||||
ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
|
||||
ObjectProvider<RedisClusterConfiguration> clusterConfiguration, RedisConnectionDetails connectionDetails) {
|
||||
super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfiguration,
|
||||
clusterConfiguration);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnThreading(Threading.PLATFORM)
|
||||
JedisConnectionFactory redisConnectionFactory(
|
||||
ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
|
||||
return createJedisConnectionFactory(builderCustomizers);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnThreading(Threading.VIRTUAL)
|
||||
JedisConnectionFactory redisConnectionFactoryVirtualThreads(
|
||||
ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
|
||||
JedisConnectionFactory factory = createJedisConnectionFactory(builderCustomizers);
|
||||
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("redis-");
|
||||
executor.setVirtualThreads(true);
|
||||
factory.setExecutor(executor);
|
||||
return factory;
|
||||
}
|
||||
|
||||
private JedisConnectionFactory createJedisConnectionFactory(
|
||||
ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
|
||||
JedisClientConfiguration clientConfiguration = getJedisClientConfiguration(builderCustomizers);
|
||||
return switch (this.mode) {
|
||||
case STANDALONE -> new JedisConnectionFactory(getStandaloneConfig(), clientConfiguration);
|
||||
case CLUSTER -> new JedisConnectionFactory(getClusterConfiguration(), clientConfiguration);
|
||||
case SENTINEL -> new JedisConnectionFactory(getSentinelConfig(), clientConfiguration);
|
||||
};
|
||||
}
|
||||
|
||||
private JedisClientConfiguration getJedisClientConfiguration(
|
||||
ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
|
||||
JedisClientConfigurationBuilder builder = applyProperties(JedisClientConfiguration.builder());
|
||||
applySslIfNeeded(builder);
|
||||
RedisProperties.Pool pool = getProperties().getJedis().getPool();
|
||||
if (isPoolEnabled(pool)) {
|
||||
applyPooling(pool, builder);
|
||||
}
|
||||
if (StringUtils.hasText(getProperties().getUrl())) {
|
||||
customizeConfigurationFromUrl(builder);
|
||||
}
|
||||
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private JedisClientConfigurationBuilder applyProperties(JedisClientConfigurationBuilder builder) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(getProperties().getTimeout()).to(builder::readTimeout);
|
||||
map.from(getProperties().getConnectTimeout()).to(builder::connectTimeout);
|
||||
map.from(getProperties().getClientName()).whenHasText().to(builder::clientName);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void applySslIfNeeded(JedisClientConfigurationBuilder builder) {
|
||||
SslBundle sslBundle = getSslBundle();
|
||||
if (sslBundle == null) {
|
||||
return;
|
||||
}
|
||||
JedisSslClientConfigurationBuilder sslBuilder = builder.useSsl();
|
||||
sslBuilder.sslSocketFactory(sslBundle.createSslContext().getSocketFactory());
|
||||
SslOptions sslOptions = sslBundle.getOptions();
|
||||
SSLParameters sslParameters = new SSLParameters();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(sslOptions.getCiphers()).to(sslParameters::setCipherSuites);
|
||||
map.from(sslOptions.getEnabledProtocols()).to(sslParameters::setProtocols);
|
||||
sslBuilder.sslParameters(sslParameters);
|
||||
}
|
||||
|
||||
private void applyPooling(RedisProperties.Pool pool,
|
||||
JedisClientConfiguration.JedisClientConfigurationBuilder builder) {
|
||||
builder.usePooling().poolConfig(jedisPoolConfig(pool));
|
||||
}
|
||||
|
||||
private JedisPoolConfig jedisPoolConfig(RedisProperties.Pool pool) {
|
||||
JedisPoolConfig config = new JedisPoolConfig();
|
||||
config.setMaxTotal(pool.getMaxActive());
|
||||
config.setMaxIdle(pool.getMaxIdle());
|
||||
config.setMinIdle(pool.getMinIdle());
|
||||
if (pool.getTimeBetweenEvictionRuns() != null) {
|
||||
config.setTimeBetweenEvictionRuns(pool.getTimeBetweenEvictionRuns());
|
||||
}
|
||||
if (pool.getMaxWait() != null) {
|
||||
config.setMaxWait(pool.getMaxWait());
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private void customizeConfigurationFromUrl(JedisClientConfiguration.JedisClientConfigurationBuilder builder) {
|
||||
if (urlUsesSsl()) {
|
||||
builder.useSsl();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder;
|
||||
|
||||
/**
|
||||
* Callback interface that can be implemented by beans wishing to customize the
|
||||
* {@link LettuceClientConfiguration} through a {@link LettuceClientConfigurationBuilder
|
||||
* LettuceClientConfiguration.LettuceClientConfigurationBuilder} whilst retaining default
|
||||
* auto-configuration. To customize only the
|
||||
* {@link LettuceClientConfiguration#getClientOptions() client options} of the
|
||||
* configuration, use {@link LettuceClientOptionsBuilderCustomizer} instead.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LettuceClientConfigurationBuilderCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the {@link LettuceClientConfigurationBuilder}.
|
||||
* @param clientConfigurationBuilder the builder to customize
|
||||
*/
|
||||
void customize(LettuceClientConfigurationBuilder clientConfigurationBuilder);
|
||||
|
||||
}
|
||||
@@ -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.ClientOptions;
|
||||
import io.lettuce.core.ClientOptions.Builder;
|
||||
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
|
||||
|
||||
/**
|
||||
* Callback interface that can be implemented by beans wishing to customize the
|
||||
* {@link ClientOptions} of the {@link LettuceClientConfiguration} through a
|
||||
* {@link Builder} whilst retaining default auto-configuration. To customize the entire
|
||||
* configuration, use {@link LettuceClientConfigurationBuilderCustomizer} instead.
|
||||
*
|
||||
* @author Soohyun Lim
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LettuceClientOptionsBuilderCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the {@link Builder}.
|
||||
* @param clientOptionsBuilder the builder to customize
|
||||
*/
|
||||
void customize(Builder clientOptionsBuilder);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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 io.lettuce.core.ClientOptions;
|
||||
import io.lettuce.core.ReadFrom;
|
||||
import io.lettuce.core.RedisClient;
|
||||
import io.lettuce.core.SocketOptions;
|
||||
import io.lettuce.core.TimeoutOptions;
|
||||
import io.lettuce.core.api.StatefulConnection;
|
||||
import io.lettuce.core.cluster.ClusterClientOptions;
|
||||
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
|
||||
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.Builder;
|
||||
import io.lettuce.core.resource.ClientResources;
|
||||
import io.lettuce.core.resource.DefaultClientResources;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
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.autoconfigure.thread.Threading;
|
||||
import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Lettuce.Cluster.Refresh;
|
||||
import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Pool;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
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.RedisSentinelConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
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.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Redis connection configuration using Lettuce.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Andy Wilkinson
|
||||
* @author Moritz Halbritter
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(RedisClient.class)
|
||||
@ConditionalOnProperty(name = "spring.data.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
|
||||
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
|
||||
|
||||
LettuceConnectionConfiguration(RedisProperties properties,
|
||||
ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
|
||||
ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
|
||||
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider,
|
||||
RedisConnectionDetails connectionDetails) {
|
||||
super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfigurationProvider,
|
||||
clusterConfigurationProvider);
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "shutdown")
|
||||
@ConditionalOnMissingBean(ClientResources.class)
|
||||
DefaultClientResources lettuceClientResources(ObjectProvider<ClientResourcesBuilderCustomizer> customizers) {
|
||||
DefaultClientResources.Builder builder = DefaultClientResources.builder();
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(RedisConnectionFactory.class)
|
||||
@ConditionalOnThreading(Threading.PLATFORM)
|
||||
LettuceConnectionFactory redisConnectionFactory(
|
||||
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> clientConfigurationBuilderCustomizers,
|
||||
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientOptionsBuilderCustomizers,
|
||||
ClientResources clientResources) {
|
||||
return createConnectionFactory(clientConfigurationBuilderCustomizers, clientOptionsBuilderCustomizers,
|
||||
clientResources);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(RedisConnectionFactory.class)
|
||||
@ConditionalOnThreading(Threading.VIRTUAL)
|
||||
LettuceConnectionFactory redisConnectionFactoryVirtualThreads(
|
||||
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> clientConfigurationBuilderCustomizers,
|
||||
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientOptionsBuilderCustomizers,
|
||||
ClientResources clientResources) {
|
||||
LettuceConnectionFactory factory = createConnectionFactory(clientConfigurationBuilderCustomizers,
|
||||
clientOptionsBuilderCustomizers, clientResources);
|
||||
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("redis-");
|
||||
executor.setVirtualThreads(true);
|
||||
factory.setExecutor(executor);
|
||||
return factory;
|
||||
}
|
||||
|
||||
private LettuceConnectionFactory createConnectionFactory(
|
||||
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> clientConfigurationBuilderCustomizers,
|
||||
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientOptionsBuilderCustomizers,
|
||||
ClientResources clientResources) {
|
||||
LettuceClientConfiguration clientConfiguration = getLettuceClientConfiguration(
|
||||
clientConfigurationBuilderCustomizers, clientOptionsBuilderCustomizers, clientResources,
|
||||
getProperties().getLettuce().getPool());
|
||||
return switch (this.mode) {
|
||||
case STANDALONE -> new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
|
||||
case CLUSTER -> new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
|
||||
case SENTINEL -> new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
|
||||
};
|
||||
}
|
||||
|
||||
private LettuceClientConfiguration getLettuceClientConfiguration(
|
||||
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> clientConfigurationBuilderCustomizers,
|
||||
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientOptionsBuilderCustomizers,
|
||||
ClientResources clientResources, Pool pool) {
|
||||
LettuceClientConfigurationBuilder builder = createBuilder(pool);
|
||||
SslBundle sslBundle = getSslBundle();
|
||||
applyProperties(builder, sslBundle);
|
||||
if (StringUtils.hasText(getProperties().getUrl())) {
|
||||
customizeConfigurationFromUrl(builder);
|
||||
}
|
||||
builder.clientOptions(createClientOptions(clientOptionsBuilderCustomizers, sslBundle));
|
||||
builder.clientResources(clientResources);
|
||||
clientConfigurationBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private LettuceClientConfigurationBuilder createBuilder(Pool pool) {
|
||||
if (isPoolEnabled(pool)) {
|
||||
return new PoolBuilderFactory().createBuilder(pool);
|
||||
}
|
||||
return LettuceClientConfiguration.builder();
|
||||
}
|
||||
|
||||
private void applyProperties(LettuceClientConfigurationBuilder builder, SslBundle sslBundle) {
|
||||
if (sslBundle != null) {
|
||||
builder.useSsl();
|
||||
}
|
||||
if (getProperties().getTimeout() != null) {
|
||||
builder.commandTimeout(getProperties().getTimeout());
|
||||
}
|
||||
if (getProperties().getLettuce() != null) {
|
||||
RedisProperties.Lettuce lettuce = getProperties().getLettuce();
|
||||
if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {
|
||||
builder.shutdownTimeout(getProperties().getLettuce().getShutdownTimeout());
|
||||
}
|
||||
String readFrom = lettuce.getReadFrom();
|
||||
if (readFrom != null) {
|
||||
builder.readFrom(getReadFrom(readFrom));
|
||||
}
|
||||
}
|
||||
if (StringUtils.hasText(getProperties().getClientName())) {
|
||||
builder.clientName(getProperties().getClientName());
|
||||
}
|
||||
}
|
||||
|
||||
private ReadFrom getReadFrom(String readFrom) {
|
||||
int index = readFrom.indexOf(':');
|
||||
if (index == -1) {
|
||||
return ReadFrom.valueOf(getCanonicalReadFromName(readFrom));
|
||||
}
|
||||
String name = getCanonicalReadFromName(readFrom.substring(0, index));
|
||||
String value = readFrom.substring(index + 1);
|
||||
return ReadFrom.valueOf(name + ":" + value);
|
||||
}
|
||||
|
||||
private String getCanonicalReadFromName(String name) {
|
||||
StringBuilder canonicalName = new StringBuilder(name.length());
|
||||
name.chars()
|
||||
.filter(Character::isLetterOrDigit)
|
||||
.map(Character::toLowerCase)
|
||||
.forEach((c) -> canonicalName.append((char) c));
|
||||
return canonicalName.toString();
|
||||
}
|
||||
|
||||
private ClientOptions createClientOptions(
|
||||
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientConfigurationBuilderCustomizers,
|
||||
SslBundle sslBundle) {
|
||||
ClientOptions.Builder builder = initializeClientOptionsBuilder();
|
||||
Duration connectTimeout = getProperties().getConnectTimeout();
|
||||
if (connectTimeout != null) {
|
||||
builder.socketOptions(SocketOptions.builder().connectTimeout(connectTimeout).build());
|
||||
}
|
||||
if (sslBundle != null) {
|
||||
io.lettuce.core.SslOptions.Builder sslOptionsBuilder = io.lettuce.core.SslOptions.builder();
|
||||
sslOptionsBuilder.keyManager(sslBundle.getManagers().getKeyManagerFactory());
|
||||
sslOptionsBuilder.trustManager(sslBundle.getManagers().getTrustManagerFactory());
|
||||
SslOptions sslOptions = sslBundle.getOptions();
|
||||
if (sslOptions.getCiphers() != null) {
|
||||
sslOptionsBuilder.cipherSuites(sslOptions.getCiphers());
|
||||
}
|
||||
if (sslOptions.getEnabledProtocols() != null) {
|
||||
sslOptionsBuilder.protocols(sslOptions.getEnabledProtocols());
|
||||
}
|
||||
builder.sslOptions(sslOptionsBuilder.build());
|
||||
}
|
||||
builder.timeoutOptions(TimeoutOptions.enabled());
|
||||
clientConfigurationBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private ClientOptions.Builder initializeClientOptionsBuilder() {
|
||||
if (getProperties().getCluster() != null) {
|
||||
ClusterClientOptions.Builder builder = ClusterClientOptions.builder();
|
||||
Refresh refreshProperties = getProperties().getLettuce().getCluster().getRefresh();
|
||||
Builder refreshBuilder = ClusterTopologyRefreshOptions.builder()
|
||||
.dynamicRefreshSources(refreshProperties.isDynamicRefreshSources());
|
||||
if (refreshProperties.getPeriod() != null) {
|
||||
refreshBuilder.enablePeriodicRefresh(refreshProperties.getPeriod());
|
||||
}
|
||||
if (refreshProperties.isAdaptive()) {
|
||||
refreshBuilder.enableAllAdaptiveRefreshTriggers();
|
||||
}
|
||||
return builder.topologyRefreshOptions(refreshBuilder.build());
|
||||
}
|
||||
return ClientOptions.builder();
|
||||
}
|
||||
|
||||
private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) {
|
||||
if (urlUsesSsl()) {
|
||||
builder.useSsl();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class to allow optional commons-pool2 dependency.
|
||||
*/
|
||||
private static final class PoolBuilderFactory {
|
||||
|
||||
LettuceClientConfigurationBuilder createBuilder(Pool properties) {
|
||||
return LettucePoolingClientConfiguration.builder().poolConfig(getPoolConfig(properties));
|
||||
}
|
||||
|
||||
private GenericObjectPoolConfig<StatefulConnection<?, ?>> getPoolConfig(Pool properties) {
|
||||
GenericObjectPoolConfig<StatefulConnection<?, ?>> config = new GenericObjectPoolConfig<>();
|
||||
config.setMaxTotal(properties.getMaxActive());
|
||||
config.setMaxIdle(properties.getMaxIdle());
|
||||
config.setMinIdle(properties.getMinIdle());
|
||||
if (properties.getTimeBetweenEvictionRuns() != null) {
|
||||
config.setTimeBetweenEvictionRuns(properties.getTimeBetweenEvictionRuns());
|
||||
}
|
||||
if (properties.getMaxWait() != null) {
|
||||
config.setMaxWait(properties.getMaxWait());
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Adapts {@link RedisProperties} to {@link RedisConnectionDetails}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Yanming Zhou
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PropertiesRedisConnectionDetails implements RedisConnectionDetails {
|
||||
|
||||
private final RedisProperties properties;
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
PropertiesRedisConnectionDetails(RedisProperties properties, SslBundles sslBundles) {
|
||||
this.properties = properties;
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
RedisUrl redisUrl = getRedisUrl();
|
||||
return (redisUrl != null) ? redisUrl.credentials().username() : this.properties.getUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
RedisUrl redisUrl = getRedisUrl();
|
||||
return (redisUrl != null) ? redisUrl.credentials().password() : this.properties.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Standalone getStandalone() {
|
||||
RedisUrl redisUrl = getRedisUrl();
|
||||
return (redisUrl != null)
|
||||
? Standalone.of(redisUrl.uri().getHost(), redisUrl.uri().getPort(), redisUrl.database(), getSslBundle())
|
||||
: Standalone.of(this.properties.getHost(), this.properties.getPort(), this.properties.getDatabase(),
|
||||
getSslBundle());
|
||||
}
|
||||
|
||||
private SslBundle getSslBundle() {
|
||||
if (!this.properties.getSsl().isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
String bundleName = this.properties.getSsl().getBundle();
|
||||
if (StringUtils.hasLength(bundleName)) {
|
||||
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
|
||||
return this.sslBundles.getBundle(bundleName);
|
||||
}
|
||||
return SslBundle.systemDefault();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sentinel getSentinel() {
|
||||
RedisProperties.Sentinel sentinel = this.properties.getSentinel();
|
||||
return (sentinel != null) ? new PropertiesSentinel(getStandalone().getDatabase(), sentinel) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cluster getCluster() {
|
||||
RedisProperties.Cluster cluster = this.properties.getCluster();
|
||||
return (cluster != null) ? new PropertiesCluster(cluster) : null;
|
||||
}
|
||||
|
||||
private RedisUrl getRedisUrl() {
|
||||
return RedisUrl.of(this.properties.getUrl());
|
||||
}
|
||||
|
||||
private List<Node> asNodes(List<String> nodes) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Cluster} implementation backed by properties.
|
||||
*/
|
||||
private class PropertiesCluster implements Cluster {
|
||||
|
||||
private final List<Node> nodes;
|
||||
|
||||
PropertiesCluster(RedisProperties.Cluster properties) {
|
||||
this.nodes = asNodes(properties.getNodes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Node> getNodes() {
|
||||
return this.nodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslBundle getSslBundle() {
|
||||
return PropertiesRedisConnectionDetails.this.getSslBundle();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Sentinel} implementation backed by properties.
|
||||
*/
|
||||
private class PropertiesSentinel implements Sentinel {
|
||||
|
||||
private final int database;
|
||||
|
||||
private final RedisProperties.Sentinel properties;
|
||||
|
||||
PropertiesSentinel(int database, RedisProperties.Sentinel properties) {
|
||||
this.database = database;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDatabase() {
|
||||
return this.database;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMaster() {
|
||||
return this.properties.getMaster();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Node> getNodes() {
|
||||
return asNodes(this.properties.getNodes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return this.properties.getUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return this.properties.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslBundle getSslBundle() {
|
||||
return PropertiesRedisConnectionDetails.this.getSslBundle();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Redis support.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
* @author Christian Dupuis
|
||||
* @author Christoph Strobl
|
||||
* @author Phillip Webb
|
||||
* @author Eddú Meléndez
|
||||
* @author Stephane Nicoll
|
||||
* @author Marco Aust
|
||||
* @author Mark Paluch
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(RedisOperations.class)
|
||||
@EnableConfigurationProperties(RedisProperties.class)
|
||||
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
|
||||
public class RedisAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(RedisConnectionDetails.class)
|
||||
PropertiesRedisConnectionDetails redisConnectionDetails(RedisProperties properties,
|
||||
ObjectProvider<SslBundles> sslBundles) {
|
||||
return new PropertiesRedisConnectionDetails(properties, sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "redisTemplate")
|
||||
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
|
||||
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<Object, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(redisConnectionFactory);
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
|
||||
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
return new StringRedisTemplate(redisConnectionFactory);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* 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.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.data.redis.autoconfigure.RedisConnectionDetails.Cluster;
|
||||
import org.springframework.boot.data.redis.autoconfigure.RedisConnectionDetails.Node;
|
||||
import org.springframework.boot.data.redis.autoconfigure.RedisConnectionDetails.Sentinel;
|
||||
import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Pool;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.data.redis.connection.RedisClusterConfiguration;
|
||||
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.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Base Redis connection configuration.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Stephane Nicoll
|
||||
* @author Alen Turkovic
|
||||
* @author Scott Frederick
|
||||
* @author Eddú Meléndez
|
||||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @author Yanming Zhou
|
||||
*/
|
||||
abstract class RedisConnectionConfiguration {
|
||||
|
||||
private static final boolean COMMONS_POOL2_AVAILABLE = ClassUtils.isPresent("org.apache.commons.pool2.ObjectPool",
|
||||
RedisConnectionConfiguration.class.getClassLoader());
|
||||
|
||||
private final RedisProperties properties;
|
||||
|
||||
private final RedisStandaloneConfiguration standaloneConfiguration;
|
||||
|
||||
private final RedisSentinelConfiguration sentinelConfiguration;
|
||||
|
||||
private final RedisClusterConfiguration clusterConfiguration;
|
||||
|
||||
private final RedisConnectionDetails connectionDetails;
|
||||
|
||||
protected final Mode mode;
|
||||
|
||||
protected RedisConnectionConfiguration(RedisProperties properties, RedisConnectionDetails connectionDetails,
|
||||
ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
|
||||
ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
|
||||
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
|
||||
this.properties = properties;
|
||||
this.standaloneConfiguration = standaloneConfigurationProvider.getIfAvailable();
|
||||
this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable();
|
||||
this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable();
|
||||
this.connectionDetails = connectionDetails;
|
||||
this.mode = determineMode();
|
||||
}
|
||||
|
||||
protected final RedisStandaloneConfiguration getStandaloneConfig() {
|
||||
if (this.standaloneConfiguration != null) {
|
||||
return this.standaloneConfiguration;
|
||||
}
|
||||
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
|
||||
config.setHostName(this.connectionDetails.getStandalone().getHost());
|
||||
config.setPort(this.connectionDetails.getStandalone().getPort());
|
||||
config.setUsername(this.connectionDetails.getUsername());
|
||||
config.setPassword(RedisPassword.of(this.connectionDetails.getPassword()));
|
||||
config.setDatabase(this.connectionDetails.getStandalone().getDatabase());
|
||||
return config;
|
||||
}
|
||||
|
||||
protected final RedisSentinelConfiguration getSentinelConfig() {
|
||||
if (this.sentinelConfiguration != null) {
|
||||
return this.sentinelConfiguration;
|
||||
}
|
||||
if (this.connectionDetails.getSentinel() != null) {
|
||||
RedisSentinelConfiguration config = new RedisSentinelConfiguration();
|
||||
config.master(this.connectionDetails.getSentinel().getMaster());
|
||||
config.setSentinels(createSentinels(this.connectionDetails.getSentinel()));
|
||||
config.setUsername(this.connectionDetails.getUsername());
|
||||
String password = this.connectionDetails.getPassword();
|
||||
if (password != null) {
|
||||
config.setPassword(RedisPassword.of(password));
|
||||
}
|
||||
config.setSentinelUsername(this.connectionDetails.getSentinel().getUsername());
|
||||
String sentinelPassword = this.connectionDetails.getSentinel().getPassword();
|
||||
if (sentinelPassword != null) {
|
||||
config.setSentinelPassword(RedisPassword.of(sentinelPassword));
|
||||
}
|
||||
config.setDatabase(this.connectionDetails.getSentinel().getDatabase());
|
||||
return config;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RedisClusterConfiguration} if necessary.
|
||||
* @return {@literal null} if no cluster settings are set.
|
||||
*/
|
||||
protected final RedisClusterConfiguration getClusterConfiguration() {
|
||||
if (this.clusterConfiguration != null) {
|
||||
return this.clusterConfiguration;
|
||||
}
|
||||
RedisProperties.Cluster clusterProperties = this.properties.getCluster();
|
||||
if (this.connectionDetails.getCluster() != null) {
|
||||
RedisClusterConfiguration config = new RedisClusterConfiguration();
|
||||
config.setClusterNodes(getNodes(this.connectionDetails.getCluster()));
|
||||
if (clusterProperties != null && clusterProperties.getMaxRedirects() != null) {
|
||||
config.setMaxRedirects(clusterProperties.getMaxRedirects());
|
||||
}
|
||||
config.setUsername(this.connectionDetails.getUsername());
|
||||
String password = this.connectionDetails.getPassword();
|
||||
if (password != null) {
|
||||
config.setPassword(RedisPassword.of(password));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<RedisNode> getNodes(Cluster cluster) {
|
||||
return cluster.getNodes().stream().map(this::asRedisNode).toList();
|
||||
}
|
||||
|
||||
private RedisNode asRedisNode(Node node) {
|
||||
return new RedisNode(node.host(), node.port());
|
||||
}
|
||||
|
||||
protected final RedisProperties getProperties() {
|
||||
return this.properties;
|
||||
}
|
||||
|
||||
protected SslBundle getSslBundle() {
|
||||
return switch (this.mode) {
|
||||
case STANDALONE -> (this.connectionDetails.getStandalone() != null)
|
||||
? this.connectionDetails.getStandalone().getSslBundle() : null;
|
||||
case CLUSTER -> (this.connectionDetails.getCluster() != null)
|
||||
? this.connectionDetails.getCluster().getSslBundle() : null;
|
||||
case SENTINEL -> (this.connectionDetails.getSentinel() != null)
|
||||
? this.connectionDetails.getSentinel().getSslBundle() : null;
|
||||
};
|
||||
}
|
||||
|
||||
protected final boolean isSslEnabled() {
|
||||
return getProperties().getSsl().isEnabled();
|
||||
}
|
||||
|
||||
protected final boolean urlUsesSsl() {
|
||||
return RedisUrl.of(this.properties.getUrl()).useSsl();
|
||||
}
|
||||
|
||||
protected boolean isPoolEnabled(Pool pool) {
|
||||
Boolean enabled = pool.getEnabled();
|
||||
return (enabled != null) ? enabled : COMMONS_POOL2_AVAILABLE;
|
||||
}
|
||||
|
||||
private List<RedisNode> createSentinels(Sentinel sentinel) {
|
||||
List<RedisNode> nodes = new ArrayList<>();
|
||||
for (Node node : sentinel.getNodes()) {
|
||||
nodes.add(asRedisNode(node));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
protected final RedisConnectionDetails getConnectionDetails() {
|
||||
return this.connectionDetails;
|
||||
}
|
||||
|
||||
private Mode determineMode() {
|
||||
if (getSentinelConfig() != null) {
|
||||
return Mode.SENTINEL;
|
||||
}
|
||||
if (getClusterConfiguration() != null) {
|
||||
return Mode.CLUSTER;
|
||||
}
|
||||
return Mode.STANDALONE;
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
|
||||
STANDALONE, CLUSTER, SENTINEL
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* 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.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Details required to establish a connection to a Redis service.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public interface RedisConnectionDetails extends ConnectionDetails {
|
||||
|
||||
/**
|
||||
* Login username of the redis server.
|
||||
* @return the login username of the redis server
|
||||
*/
|
||||
default String getUsername() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login password of the redis server.
|
||||
* @return the login password of the redis server
|
||||
*/
|
||||
default String getPassword() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis standalone configuration. Mutually exclusive with {@link #getSentinel()} and
|
||||
* {@link #getCluster()}.
|
||||
* @return the Redis standalone configuration
|
||||
*/
|
||||
default Standalone getStandalone() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis sentinel configuration. Mutually exclusive with {@link #getStandalone()} and
|
||||
* {@link #getCluster()}.
|
||||
* @return the Redis sentinel configuration
|
||||
*/
|
||||
default Sentinel getSentinel() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis cluster configuration. Mutually exclusive with {@link #getStandalone()} and
|
||||
* {@link #getSentinel()}.
|
||||
* @return the Redis cluster configuration
|
||||
*/
|
||||
default Cluster getCluster() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis standalone configuration.
|
||||
*/
|
||||
interface Standalone {
|
||||
|
||||
/**
|
||||
* Redis server host.
|
||||
* @return the redis server host
|
||||
*/
|
||||
String getHost();
|
||||
|
||||
/**
|
||||
* Redis server port.
|
||||
* @return the redis server port
|
||||
*/
|
||||
int getPort();
|
||||
|
||||
/**
|
||||
* Database index used by the connection factory.
|
||||
* @return the database index used by the connection factory
|
||||
*/
|
||||
default int getDatabase() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSL bundle to use.
|
||||
* @return the SSL bundle to use
|
||||
* @since 3.5.0
|
||||
*/
|
||||
default SslBundle getSslBundle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance with the given host and port.
|
||||
* @param host the host
|
||||
* @param port the port
|
||||
* @return the new instance
|
||||
*/
|
||||
static Standalone of(String host, int port) {
|
||||
return of(host, port, 0, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance with the given host, port and SSL bundle.
|
||||
* @param host the host
|
||||
* @param port the port
|
||||
* @param sslBundle the SSL bundle
|
||||
* @return the new instance
|
||||
* @since 3.5.0
|
||||
*/
|
||||
static Standalone of(String host, int port, SslBundle sslBundle) {
|
||||
return of(host, port, 0, sslBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance with the given host, port and database.
|
||||
* @param host the host
|
||||
* @param port the port
|
||||
* @param database the database
|
||||
* @return the new instance
|
||||
*/
|
||||
static Standalone of(String host, int port, int database) {
|
||||
return of(host, port, database, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance with the given host, port, database and SSL bundle.
|
||||
* @param host the host
|
||||
* @param port the port
|
||||
* @param database the database
|
||||
* @param sslBundle the SSL bundle
|
||||
* @return the new instance
|
||||
* @since 3.5.0
|
||||
*/
|
||||
static Standalone of(String host, int port, int database, SslBundle sslBundle) {
|
||||
Assert.hasLength(host, "'host' must not be empty");
|
||||
return new Standalone() {
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslBundle getSslBundle() {
|
||||
return sslBundle;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis sentinel configuration.
|
||||
*/
|
||||
interface Sentinel {
|
||||
|
||||
/**
|
||||
* Database index used by the connection factory.
|
||||
* @return the database index used by the connection factory
|
||||
*/
|
||||
int getDatabase();
|
||||
|
||||
/**
|
||||
* Name of the Redis server.
|
||||
* @return the name of the Redis server
|
||||
*/
|
||||
String getMaster();
|
||||
|
||||
/**
|
||||
* List of nodes.
|
||||
* @return the list of nodes
|
||||
*/
|
||||
List<Node> getNodes();
|
||||
|
||||
/**
|
||||
* Login username for authenticating with sentinel(s).
|
||||
* @return the login username for authenticating with sentinel(s) or {@code null}
|
||||
*/
|
||||
String getUsername();
|
||||
|
||||
/**
|
||||
* Password for authenticating with sentinel(s).
|
||||
* @return the password for authenticating with sentinel(s) or {@code null}
|
||||
*/
|
||||
String getPassword();
|
||||
|
||||
/**
|
||||
* SSL bundle to use.
|
||||
* @return the SSL bundle to use
|
||||
* @since 3.5.0
|
||||
*/
|
||||
default SslBundle getSslBundle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis cluster configuration.
|
||||
*/
|
||||
interface Cluster {
|
||||
|
||||
/**
|
||||
* Nodes to bootstrap from. This represents an "initial" list of cluster nodes and
|
||||
* is required to have at least one entry.
|
||||
* @return nodes to bootstrap from
|
||||
*/
|
||||
List<Node> getNodes();
|
||||
|
||||
/**
|
||||
* SSL bundle to use.
|
||||
* @return the SSL bundle to use
|
||||
* @since 3.5.0
|
||||
*/
|
||||
default SslBundle getSslBundle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A node in a sentinel or cluster configuration.
|
||||
*
|
||||
* @param host the hostname of the node
|
||||
* @param port the port of the node
|
||||
*/
|
||||
record Node(String host, int port) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* 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.List;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Configuration properties for Redis.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Christoph Strobl
|
||||
* @author Eddú Meléndez
|
||||
* @author Marco Aust
|
||||
* @author Mark Paluch
|
||||
* @author Stephane Nicoll
|
||||
* @author Scott Frederick
|
||||
* @author Yanming Zhou
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.data.redis")
|
||||
public class RedisProperties {
|
||||
|
||||
/**
|
||||
* Database index used by the connection factory.
|
||||
*/
|
||||
private int database = 0;
|
||||
|
||||
/**
|
||||
* Connection URL. Overrides host, port, username, password, and database. Example:
|
||||
* redis://user:password@example.com:6379/8
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* Redis server host.
|
||||
*/
|
||||
private String host = "localhost";
|
||||
|
||||
/**
|
||||
* Login username of the redis server.
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* Login password of the redis server.
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* Redis server port.
|
||||
*/
|
||||
private int port = 6379;
|
||||
|
||||
/**
|
||||
* Read timeout.
|
||||
*/
|
||||
private Duration timeout;
|
||||
|
||||
/**
|
||||
* Connection timeout.
|
||||
*/
|
||||
private Duration connectTimeout;
|
||||
|
||||
/**
|
||||
* Client name to be set on connections with CLIENT SETNAME.
|
||||
*/
|
||||
private String clientName;
|
||||
|
||||
/**
|
||||
* Type of client to use. By default, auto-detected according to the classpath.
|
||||
*/
|
||||
private ClientType clientType;
|
||||
|
||||
private Sentinel sentinel;
|
||||
|
||||
private Cluster cluster;
|
||||
|
||||
private final Ssl ssl = new Ssl();
|
||||
|
||||
private final Jedis jedis = new Jedis();
|
||||
|
||||
private final Lettuce lettuce = new Lettuce();
|
||||
|
||||
public int getDatabase() {
|
||||
return this.database;
|
||||
}
|
||||
|
||||
public void setDatabase(int database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return this.host;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public Ssl getSsl() {
|
||||
return this.ssl;
|
||||
}
|
||||
|
||||
public void setTimeout(Duration timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public Duration getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
public Duration getConnectTimeout() {
|
||||
return this.connectTimeout;
|
||||
}
|
||||
|
||||
public void setConnectTimeout(Duration connectTimeout) {
|
||||
this.connectTimeout = connectTimeout;
|
||||
}
|
||||
|
||||
public String getClientName() {
|
||||
return this.clientName;
|
||||
}
|
||||
|
||||
public void setClientName(String clientName) {
|
||||
this.clientName = clientName;
|
||||
}
|
||||
|
||||
public ClientType getClientType() {
|
||||
return this.clientType;
|
||||
}
|
||||
|
||||
public void setClientType(ClientType clientType) {
|
||||
this.clientType = clientType;
|
||||
}
|
||||
|
||||
public Sentinel getSentinel() {
|
||||
return this.sentinel;
|
||||
}
|
||||
|
||||
public void setSentinel(Sentinel sentinel) {
|
||||
this.sentinel = sentinel;
|
||||
}
|
||||
|
||||
public Cluster getCluster() {
|
||||
return this.cluster;
|
||||
}
|
||||
|
||||
public void setCluster(Cluster cluster) {
|
||||
this.cluster = cluster;
|
||||
}
|
||||
|
||||
public Jedis getJedis() {
|
||||
return this.jedis;
|
||||
}
|
||||
|
||||
public Lettuce getLettuce() {
|
||||
return this.lettuce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of Redis client to use.
|
||||
*/
|
||||
public enum ClientType {
|
||||
|
||||
/**
|
||||
* Use the Lettuce redis client.
|
||||
*/
|
||||
LETTUCE,
|
||||
|
||||
/**
|
||||
* Use the Jedis redis client.
|
||||
*/
|
||||
JEDIS
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Pool properties.
|
||||
*/
|
||||
public static class Pool {
|
||||
|
||||
/**
|
||||
* Whether to enable the pool. Enabled automatically if "commons-pool2" is
|
||||
* available. With Jedis, pooling is implicitly enabled in sentinel mode and this
|
||||
* setting only applies to single node setup.
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* Maximum number of "idle" connections in the pool. Use a negative value to
|
||||
* indicate an unlimited number of idle connections.
|
||||
*/
|
||||
private int maxIdle = 8;
|
||||
|
||||
/**
|
||||
* Target for the minimum number of idle connections to maintain in the pool. This
|
||||
* setting only has an effect if both it and time between eviction runs are
|
||||
* positive.
|
||||
*/
|
||||
private int minIdle = 0;
|
||||
|
||||
/**
|
||||
* Maximum number of connections that can be allocated by the pool at a given
|
||||
* time. Use a negative value for no limit.
|
||||
*/
|
||||
private int maxActive = 8;
|
||||
|
||||
/**
|
||||
* Maximum amount of time a connection allocation should block before throwing an
|
||||
* exception when the pool is exhausted. Use a negative value to block
|
||||
* indefinitely.
|
||||
*/
|
||||
private Duration maxWait = Duration.ofMillis(-1);
|
||||
|
||||
/**
|
||||
* Time between runs of the idle object evictor thread. When positive, the idle
|
||||
* object evictor thread starts, otherwise no idle object eviction is performed.
|
||||
*/
|
||||
private Duration timeBetweenEvictionRuns;
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public int getMaxIdle() {
|
||||
return this.maxIdle;
|
||||
}
|
||||
|
||||
public void setMaxIdle(int maxIdle) {
|
||||
this.maxIdle = maxIdle;
|
||||
}
|
||||
|
||||
public int getMinIdle() {
|
||||
return this.minIdle;
|
||||
}
|
||||
|
||||
public void setMinIdle(int minIdle) {
|
||||
this.minIdle = minIdle;
|
||||
}
|
||||
|
||||
public int getMaxActive() {
|
||||
return this.maxActive;
|
||||
}
|
||||
|
||||
public void setMaxActive(int maxActive) {
|
||||
this.maxActive = maxActive;
|
||||
}
|
||||
|
||||
public Duration getMaxWait() {
|
||||
return this.maxWait;
|
||||
}
|
||||
|
||||
public void setMaxWait(Duration maxWait) {
|
||||
this.maxWait = maxWait;
|
||||
}
|
||||
|
||||
public Duration getTimeBetweenEvictionRuns() {
|
||||
return this.timeBetweenEvictionRuns;
|
||||
}
|
||||
|
||||
public void setTimeBetweenEvictionRuns(Duration timeBetweenEvictionRuns) {
|
||||
this.timeBetweenEvictionRuns = timeBetweenEvictionRuns;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Cluster properties.
|
||||
*/
|
||||
public static class Cluster {
|
||||
|
||||
/**
|
||||
* List of "host:port" pairs to bootstrap from. This represents an "initial" list
|
||||
* of cluster nodes and is required to have at least one entry.
|
||||
*/
|
||||
private List<String> nodes;
|
||||
|
||||
/**
|
||||
* Maximum number of redirects to follow when executing commands across the
|
||||
* cluster.
|
||||
*/
|
||||
private Integer maxRedirects;
|
||||
|
||||
public List<String> getNodes() {
|
||||
return this.nodes;
|
||||
}
|
||||
|
||||
public void setNodes(List<String> nodes) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
public Integer getMaxRedirects() {
|
||||
return this.maxRedirects;
|
||||
}
|
||||
|
||||
public void setMaxRedirects(Integer maxRedirects) {
|
||||
this.maxRedirects = maxRedirects;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis sentinel properties.
|
||||
*/
|
||||
public static class Sentinel {
|
||||
|
||||
/**
|
||||
* Name of the Redis server.
|
||||
*/
|
||||
private String master;
|
||||
|
||||
/**
|
||||
* List of "host:port" pairs.
|
||||
*/
|
||||
private List<String> nodes;
|
||||
|
||||
/**
|
||||
* Login username for authenticating with sentinel(s).
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* Password for authenticating with sentinel(s).
|
||||
*/
|
||||
private String password;
|
||||
|
||||
public String getMaster() {
|
||||
return this.master;
|
||||
}
|
||||
|
||||
public void setMaster(String master) {
|
||||
this.master = master;
|
||||
}
|
||||
|
||||
public List<String> getNodes() {
|
||||
return this.nodes;
|
||||
}
|
||||
|
||||
public void setNodes(List<String> nodes) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Ssl {
|
||||
|
||||
/**
|
||||
* Whether to enable SSL support. Enabled automatically if "bundle" is provided
|
||||
* unless specified otherwise.
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* SSL bundle name.
|
||||
*/
|
||||
private String bundle;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return (this.enabled != null) ? this.enabled : this.bundle != null;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getBundle() {
|
||||
return this.bundle;
|
||||
}
|
||||
|
||||
public void setBundle(String bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Jedis client properties.
|
||||
*/
|
||||
public static class Jedis {
|
||||
|
||||
/**
|
||||
* Jedis pool configuration.
|
||||
*/
|
||||
private final Pool pool = new Pool();
|
||||
|
||||
public Pool getPool() {
|
||||
return this.pool;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Lettuce client properties.
|
||||
*/
|
||||
public static class Lettuce {
|
||||
|
||||
/**
|
||||
* Shutdown timeout.
|
||||
*/
|
||||
private Duration shutdownTimeout = Duration.ofMillis(100);
|
||||
|
||||
/**
|
||||
* Defines from which Redis nodes data is read.
|
||||
*/
|
||||
private String readFrom;
|
||||
|
||||
/**
|
||||
* Lettuce pool configuration.
|
||||
*/
|
||||
private final Pool pool = new Pool();
|
||||
|
||||
private final Cluster cluster = new Cluster();
|
||||
|
||||
public Duration getShutdownTimeout() {
|
||||
return this.shutdownTimeout;
|
||||
}
|
||||
|
||||
public void setShutdownTimeout(Duration shutdownTimeout) {
|
||||
this.shutdownTimeout = shutdownTimeout;
|
||||
}
|
||||
|
||||
public void setReadFrom(String readFrom) {
|
||||
this.readFrom = readFrom;
|
||||
}
|
||||
|
||||
public String getReadFrom() {
|
||||
return this.readFrom;
|
||||
}
|
||||
|
||||
public Pool getPool() {
|
||||
return this.pool;
|
||||
}
|
||||
|
||||
public Cluster getCluster() {
|
||||
return this.cluster;
|
||||
}
|
||||
|
||||
public static class Cluster {
|
||||
|
||||
private final Refresh refresh = new Refresh();
|
||||
|
||||
public Refresh getRefresh() {
|
||||
return this.refresh;
|
||||
}
|
||||
|
||||
public static class Refresh {
|
||||
|
||||
/**
|
||||
* Whether to discover and query all cluster nodes for obtaining the
|
||||
* cluster topology. When set to false, only the initial seed nodes are
|
||||
* used as sources for topology discovery.
|
||||
*/
|
||||
private boolean dynamicRefreshSources = true;
|
||||
|
||||
/**
|
||||
* Cluster topology refresh period.
|
||||
*/
|
||||
private Duration period;
|
||||
|
||||
/**
|
||||
* Whether adaptive topology refreshing using all available refresh
|
||||
* triggers should be used.
|
||||
*/
|
||||
private boolean adaptive;
|
||||
|
||||
public boolean isDynamicRefreshSources() {
|
||||
return this.dynamicRefreshSources;
|
||||
}
|
||||
|
||||
public void setDynamicRefreshSources(boolean dynamicRefreshSources) {
|
||||
this.dynamicRefreshSources = dynamicRefreshSources;
|
||||
}
|
||||
|
||||
public Duration getPeriod() {
|
||||
return this.period;
|
||||
}
|
||||
|
||||
public void setPeriod(Duration period) {
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public boolean isAdaptive() {
|
||||
return this.adaptive;
|
||||
}
|
||||
|
||||
public void setAdaptive(boolean adaptive) {
|
||||
this.adaptive = adaptive;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Spring Data's reactive Redis
|
||||
* support.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Stephane Nicoll
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = RedisAutoConfiguration.class)
|
||||
@ConditionalOnClass({ ReactiveRedisConnectionFactory.class, ReactiveRedisTemplate.class, Flux.class })
|
||||
public class RedisReactiveAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "reactiveRedisTemplate")
|
||||
@ConditionalOnBean(ReactiveRedisConnectionFactory.class)
|
||||
public ReactiveRedisTemplate<Object, Object> reactiveRedisTemplate(
|
||||
ReactiveRedisConnectionFactory reactiveRedisConnectionFactory, ResourceLoader resourceLoader) {
|
||||
RedisSerializer<Object> javaSerializer = RedisSerializer.java(resourceLoader.getClassLoader());
|
||||
RedisSerializationContext<Object, Object> serializationContext = RedisSerializationContext
|
||||
.newSerializationContext()
|
||||
.key(javaSerializer)
|
||||
.value(javaSerializer)
|
||||
.hashKey(javaSerializer)
|
||||
.hashValue(javaSerializer)
|
||||
.build();
|
||||
return new ReactiveRedisTemplate<>(reactiveRedisConnectionFactory, serializationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "reactiveStringRedisTemplate")
|
||||
@ConditionalOnBean(ReactiveRedisConnectionFactory.class)
|
||||
public ReactiveStringRedisTemplate reactiveStringRedisTemplate(
|
||||
ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) {
|
||||
return new ReactiveStringRedisTemplate(reactiveRedisConnectionFactory);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
|
||||
import org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Redis
|
||||
* Repositories.
|
||||
*
|
||||
* @author Eddú Meléndez
|
||||
* @author Stephane Nicoll
|
||||
* @since 4.0.0
|
||||
* @see EnableRedisRepositories
|
||||
*/
|
||||
@AutoConfiguration(after = RedisAutoConfiguration.class)
|
||||
@ConditionalOnClass(EnableRedisRepositories.class)
|
||||
@ConditionalOnBean(RedisConnectionFactory.class)
|
||||
@ConditionalOnBooleanProperty(name = "spring.data.redis.repositories.enabled", matchIfMissing = true)
|
||||
@ConditionalOnMissingBean(RedisRepositoryFactoryBean.class)
|
||||
@Import(RedisRepositoriesRegistrar.class)
|
||||
public class RedisRepositoriesAutoConfiguration {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.lang.annotation.Annotation;
|
||||
|
||||
import org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
|
||||
import org.springframework.data.redis.repository.configuration.RedisRepositoryConfigurationExtension;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
|
||||
|
||||
/**
|
||||
* {@link ImportBeanDefinitionRegistrar} used to auto-configure Spring Data Redis
|
||||
* Repositories.
|
||||
*
|
||||
* @author Eddú Meléndez
|
||||
*/
|
||||
class RedisRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport {
|
||||
|
||||
@Override
|
||||
protected Class<? extends Annotation> getAnnotation() {
|
||||
return EnableRedisRepositories.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> getConfiguration() {
|
||||
return EnableRedisRepositoriesConfiguration.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() {
|
||||
return new RedisRepositoryConfigurationExtension();
|
||||
}
|
||||
|
||||
@EnableRedisRepositories
|
||||
private static final class EnableRedisRepositoriesConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A parsed URL used to connect to Redis.
|
||||
*
|
||||
* @param uri the source URI
|
||||
* @param useSsl if SSL is used to connect
|
||||
* @param credentials the connection credentials
|
||||
* @param database the database index
|
||||
* @author Mark Paluch
|
||||
* @author Stephane Nicoll
|
||||
* @author Alen Turkovic
|
||||
* @author Scott Frederick
|
||||
* @author Eddú Meléndez
|
||||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @author Yanming Zhou
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
record RedisUrl(URI uri, boolean useSsl, Credentials credentials, int database) {
|
||||
|
||||
static RedisUrl of(String url) {
|
||||
return (url != null) ? of(toUri(url)) : null;
|
||||
}
|
||||
|
||||
private static RedisUrl of(URI uri) {
|
||||
boolean useSsl = ("rediss".equals(uri.getScheme()));
|
||||
Credentials credentials = Credentials.fromUserInfo(uri.getUserInfo());
|
||||
int database = getDatabase(uri);
|
||||
return new RedisUrl(uri, useSsl, credentials, database);
|
||||
}
|
||||
|
||||
private static int getDatabase(URI uri) {
|
||||
String path = uri.getPath();
|
||||
String[] split = (!StringUtils.hasText(path)) ? new String[0] : path.split("/", 2);
|
||||
return (split.length > 1 && !split[1].isEmpty()) ? Integer.parseInt(split[1]) : 0;
|
||||
}
|
||||
|
||||
private static URI toUri(String url) {
|
||||
try {
|
||||
URI uri = new URI(url);
|
||||
String scheme = uri.getScheme();
|
||||
if (!"redis".equals(scheme) && !"rediss".equals(scheme)) {
|
||||
throw new RedisUrlSyntaxException(url);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throw new RedisUrlSyntaxException(url, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis connection credentials.
|
||||
*
|
||||
* @param username the username or {@code null}
|
||||
* @param password the password
|
||||
*/
|
||||
record Credentials(String username, String password) {
|
||||
|
||||
private static final Credentials NONE = new Credentials(null, null);
|
||||
|
||||
private static Credentials fromUserInfo(String userInfo) {
|
||||
if (userInfo == null) {
|
||||
return NONE;
|
||||
}
|
||||
int index = userInfo.indexOf(':');
|
||||
if (index != -1) {
|
||||
return new Credentials(userInfo.substring(0, index), userInfo.substring(index + 1));
|
||||
}
|
||||
return new Credentials(null, userInfo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Exception thrown when a Redis URL is malformed or invalid.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class RedisUrlSyntaxException extends RuntimeException {
|
||||
|
||||
private final String url;
|
||||
|
||||
RedisUrlSyntaxException(String url, Exception cause) {
|
||||
super(buildMessage(url), cause);
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
RedisUrlSyntaxException(String url) {
|
||||
super(buildMessage(url));
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
private static String buildMessage(String url) {
|
||||
return "Invalid Redis URL '" + url + "'";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
|
||||
import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||
|
||||
/**
|
||||
* A {@code FailureAnalyzer} that performs analysis of failures caused by a
|
||||
* {@link RedisUrlSyntaxException}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class RedisUrlSyntaxFailureAnalyzer extends AbstractFailureAnalyzer<RedisUrlSyntaxException> {
|
||||
|
||||
@Override
|
||||
protected FailureAnalysis analyze(Throwable rootFailure, RedisUrlSyntaxException cause) {
|
||||
try {
|
||||
URI uri = new URI(cause.getUrl());
|
||||
if ("redis-sentinel".equals(uri.getScheme())) {
|
||||
return new FailureAnalysis(getUnsupportedSchemeDescription(cause.getUrl(), uri.getScheme()),
|
||||
"Use spring.data.redis.sentinel properties instead of spring.data.redis.url to configure Redis sentinel addresses.",
|
||||
cause);
|
||||
}
|
||||
if ("redis-socket".equals(uri.getScheme())) {
|
||||
return new FailureAnalysis(getUnsupportedSchemeDescription(cause.getUrl(), uri.getScheme()),
|
||||
"Configure the appropriate Spring Data Redis connection beans directly instead of setting the property 'spring.data.redis.url'.",
|
||||
cause);
|
||||
}
|
||||
if (!"redis".equals(uri.getScheme()) && !"rediss".equals(uri.getScheme())) {
|
||||
return new FailureAnalysis(getUnsupportedSchemeDescription(cause.getUrl(), uri.getScheme()),
|
||||
"Use the scheme 'redis://' for insecure or 'rediss://' for secure Redis standalone configuration.",
|
||||
cause);
|
||||
}
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
// fall through to default description and action
|
||||
}
|
||||
return new FailureAnalysis(getDefaultDescription(cause.getUrl()),
|
||||
"Review the value of the property 'spring.data.redis.url'.", cause);
|
||||
}
|
||||
|
||||
private String getDefaultDescription(String url) {
|
||||
return "The URL '" + url + "' is not valid for configuring Spring Data Redis. ";
|
||||
}
|
||||
|
||||
private String getUnsupportedSchemeDescription(String url, String scheme) {
|
||||
return getDefaultDescription(url) + "The scheme '" + scheme + "' is not supported.";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for Spring Data Redis.
|
||||
*/
|
||||
package org.springframework.boot.data.redis.autoconfigure;
|
||||
@@ -0,0 +1,319 @@
|
||||
{
|
||||
"groups": [],
|
||||
"properties": [
|
||||
{
|
||||
"name": "spring.data.redis.repositories.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to enable Redis repositories.",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"name": "spring.data.redis.ssl",
|
||||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.ssl.enabled",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.client-name",
|
||||
"type": "java.lang.String",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.client-name",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.client-type",
|
||||
"type": "org.springframework.boot.data.redis.autoconfigure.RedisProperties$ClientType",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.client-type",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.cluster.max-redirects",
|
||||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.cluster.max-redirects",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.cluster.nodes",
|
||||
"type": "java.util.List<java.lang.String>",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.cluster.nodes",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.connect-timeout",
|
||||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.connect-timeout",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.database",
|
||||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.database",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.host",
|
||||
"type": "java.lang.String",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.host",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.jedis.pool.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.jedis.pool.max-active",
|
||||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.jedis.pool.max-idle",
|
||||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.jedis.pool.max-wait",
|
||||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.jedis.pool.min-idle",
|
||||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.jedis.pool.time-between-eviction-runs",
|
||||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.cluster.refresh.adaptive",
|
||||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.lettuce.cluster.refresh.adaptive",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.cluster.refresh.dynamic-refresh-sources",
|
||||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.lettuce.cluster.refresh.dynamic-refresh-sources",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.cluster.refresh.period",
|
||||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.lettuce.cluster.refresh.period",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.pool.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.pool.max-active",
|
||||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.pool.max-idle",
|
||||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.pool.max-wait",
|
||||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.pool.min-idle",
|
||||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.pool.time-between-eviction-runs",
|
||||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.lettuce.shutdown-timeout",
|
||||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.lettuce.shutdown-timeout",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.password",
|
||||
"type": "java.lang.String",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.password",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.port",
|
||||
"type": "java.lang.Integer",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.port",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.sentinel.master",
|
||||
"type": "java.lang.String",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.sentinel.master",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.sentinel.nodes",
|
||||
"type": "java.util.List<java.lang.String>",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.sentinel.nodes",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.sentinel.password",
|
||||
"type": "java.lang.String",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.sentinel.password",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.sentinel.username",
|
||||
"type": "java.lang.String",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.sentinel.username",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.ssl",
|
||||
"type": "java.lang.Boolean",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.ssl",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.timeout",
|
||||
"type": "java.time.Duration",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.timeout",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.url",
|
||||
"type": "java.lang.String",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.url",
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.username",
|
||||
"type": "java.lang.String",
|
||||
"deprecation": {
|
||||
"replacement": "spring.data.redis.username",
|
||||
"level": "error"
|
||||
}
|
||||
}
|
||||
],
|
||||
"hints": [
|
||||
{
|
||||
"name": "spring.data.redis.lettuce.read-from",
|
||||
"values": [
|
||||
{
|
||||
"value": "any",
|
||||
"description": "Read from any node."
|
||||
},
|
||||
{
|
||||
"value": "any-replica",
|
||||
"description": "Read from any replica node."
|
||||
},
|
||||
{
|
||||
"value": "lowest-latency",
|
||||
"description": "Read from the node with the lowest latency during topology discovery."
|
||||
},
|
||||
{
|
||||
"value": "regex:",
|
||||
"description": "Read from any node that has RedisURI matching with the given pattern."
|
||||
},
|
||||
{
|
||||
"value": "replica",
|
||||
"description": "Read from the replica only."
|
||||
},
|
||||
{
|
||||
"value": "replica-preferred",
|
||||
"description": "Read preferred from replica and fall back to upstream if no replica is available."
|
||||
},
|
||||
{
|
||||
"value": "subnet:",
|
||||
"description": "Read from any node in the subnets."
|
||||
},
|
||||
{
|
||||
"value": "upstream",
|
||||
"description": "Read from the upstream only."
|
||||
},
|
||||
{
|
||||
"value": "upstream-preferred",
|
||||
"description": "Read preferred from the upstream and fall back to a replica if the upstream is not available."
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"name": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Failure Analyzers
|
||||
org.springframework.boot.diagnostics.FailureAnalyzer=\
|
||||
org.springframework.boot.data.redis.autoconfigure.RedisUrlSyntaxFailureAnalyzer
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
org.springframework.boot.data.redis.autoconfigure.RedisAutoConfiguration
|
||||
org.springframework.boot.data.redis.autoconfigure.RedisReactiveAutoConfiguration
|
||||
org.springframework.boot.data.redis.autoconfigure.RedisRepositoriesAutoConfiguration
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user