I suggest setting RedisConnectionConfiguration to protected, so that it can be reused in situations with multiple data sources. The current design with default access level restricts the ability of users to extend it.

Comment From: wilkinsona

Thanks for the suggestion but RedisConnectionConfiguration and Boot's sub-classes of it are intentionally package-private as we do not want the restrictions that would come with them being public.

Can you describe in more detail what you're trying to do and show the code that you currently have to write to do it?

Comment From: quaff

Here is my workaround to configure multiple redis services:

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.concurrent.Executor;

import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.data.redis.ClientResourcesBuilderCustomizer;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.ssl.SslBundles;
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.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.ClassUtils;

public class RedisConfigurationSupport {

    private final Object configuration;

    RedisConfigurationSupport(RedisProperties properties,
            ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
            ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
            ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider,
            RedisConnectionDetails connectionDetails, ObjectProvider<SslBundles> sslBundles) {
        try {
            Class<?> clazz = RedisAutoConfiguration.class;
            Class<?> configurationClass = ClassUtils.forName(clazz.getPackageName() + ".LettuceConnectionConfiguration",
                    clazz.getClassLoader());
            Constructor<?> ctor = configurationClass.getDeclaredConstructor(
                    RedisConfigurationSupport.class.getDeclaredConstructors()[0].getParameterTypes());
            ctor.setAccessible(true);
            this.configuration = ctor.newInstance(properties, standaloneConfigurationProvider,
                    sentinelConfigurationProvider, clusterConfigurationProvider, connectionDetails, sslBundles);
        }
        catch (Exception ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }

    protected DefaultClientResources lettuceClientResources(
            ObjectProvider<ClientResourcesBuilderCustomizer> customizers) {
        try {
            Method m = this.configuration.getClass().getDeclaredMethod("lettuceClientResources", ObjectProvider.class);
            m.setAccessible(true);
            return (DefaultClientResources) m.invoke(this.configuration, customizers);
        }
        catch (Exception ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }

    protected LettuceConnectionFactory redisConnectionFactory(
            ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
            ClientResources clientResources) {
        try {
            Method m = this.configuration.getClass()
                .getDeclaredMethod("redisConnectionFactory", ObjectProvider.class, ClientResources.class);
            m.setAccessible(true);
            return (LettuceConnectionFactory) m.invoke(this.configuration, builderCustomizers, clientResources);
        }
        catch (Exception ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }

    protected RedisTemplate<String, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        return template;
    }

    protected StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }

    protected RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,
            Optional<Executor> taskExecutor) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        taskExecutor.ifPresent(container::setTaskExecutor);
        return container;
    }

    protected static RedisConnectionDetails redisConnectionDetails(RedisProperties properties) {
        try {
            Constructor<?> ctor = ClassUtils
                .forName(RedisProperties.class.getPackageName() + ".PropertiesRedisConnectionDetails",
                        RedisProperties.class.getClassLoader())
                .getDeclaredConstructor(RedisProperties.class);
            ctor.setAccessible(true);
            return (RedisConnectionDetails) ctor.newInstance(properties);
        }
        catch (Exception ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }

}

import java.util.Optional;
import java.util.concurrent.Executor;

import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.data.redis.ClientResourcesBuilderCustomizer;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(DefaultRedisProperties.class)
public class DefaultRedisConfiguration extends RedisConfigurationSupport {

    @Bean
    public static RedisConnectionDetails redisConnectionDetails(DefaultRedisProperties properties) {
        return RedisConfigurationSupport.redisConnectionDetails(properties);
    }

    DefaultRedisConfiguration(DefaultRedisProperties properties,
            ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
            ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
            ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider,
            RedisConnectionDetails redisConnectionDetails, ObjectProvider<SslBundles> sslBundles) {
        super(properties, standaloneConfigurationProvider, sentinelConfigurationProvider, clusterConfigurationProvider,
                redisConnectionDetails, sslBundles);
    }

    @Primary
    @Bean(destroyMethod = "shutdown")
    @Override
    public DefaultClientResources lettuceClientResources(ObjectProvider<ClientResourcesBuilderCustomizer> customizers) {
        return super.lettuceClientResources(customizers);
    }

    @Override
    @Bean
    public LettuceConnectionFactory redisConnectionFactory(
            ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
            ClientResources lettuceClientResources) {
        return super.redisConnectionFactory(builderCustomizers, lettuceClientResources);
    }

    @Bean
    @Primary
    @Override
    public RedisTemplate<String, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return super.redisTemplate(redisConnectionFactory);
    }

    @Bean
    @Override
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return super.stringRedisTemplate(redisConnectionFactory);
    }

    @Bean
    @Primary
    @Override
    public RedisMessageListenerContainer redisMessageListenerContainer(
            @Qualifier("redisConnectionFactory") RedisConnectionFactory redisConnectionFactory,
            Optional<Executor> taskExecutor) {
        return super.redisMessageListenerContainer(redisConnectionFactory, taskExecutor);
    }

    @ConfigurationProperties(prefix = DefaultRedisProperties.PREFIX)
    public static class DefaultRedisProperties extends RedisProperties {

        public static final String PREFIX = "spring.data.redis";

    }

}

import java.util.Optional;
import java.util.concurrent.Executor;

import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.ClientResourcesBuilderCustomizer;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GlobalRedisProperties.class)
@ConditionalOnProperty(prefix = GlobalRedisProperties.PREFIX, name = "enabled", havingValue = "true")
public class GlobalRedisConfiguration extends RedisConfigurationSupport {

    @Bean
    public static RedisConnectionDetails globalRedisConnectionDetails(GlobalRedisProperties properties) {
        return RedisConfigurationSupport.redisConnectionDetails(properties);
    }

    GlobalRedisConfiguration(GlobalRedisProperties properties,
            ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
            ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
            ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider,
            RedisConnectionDetails globalRedisConnectionDetails, ObjectProvider<SslBundles> sslBundles) {
        super(properties, standaloneConfigurationProvider, sentinelConfigurationProvider, clusterConfigurationProvider,
                globalRedisConnectionDetails, sslBundles);
    }

    @Bean(destroyMethod = "shutdown")
    public DefaultClientResources globalLettuceClientResources(
            ObjectProvider<ClientResourcesBuilderCustomizer> customizers) {
        return super.lettuceClientResources(customizers);
    }

    @Bean
    public LettuceConnectionFactory globalRedisConnectionFactory(
            ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
            @Qualifier("globalLettuceClientResources") ClientResources lettuceClientResources) {
        return super.redisConnectionFactory(builderCustomizers, lettuceClientResources);
    }

    @Bean
    public RedisTemplate<String, ?> globalRedisTemplate(
            @Qualifier("globalRedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        return super.redisTemplate(redisConnectionFactory);
    }

    @Bean
    public StringRedisTemplate globalStringRedisTemplate(
            @Qualifier("globalRedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        return super.stringRedisTemplate(redisConnectionFactory);
    }

    @Bean
    public RedisMessageListenerContainer globalRedisMessageListenerContainer(
            @Qualifier("globalRedisConnectionFactory") RedisConnectionFactory redisConnectionFactory,
            Optional<Executor> taskExecutor) {
        return super.redisMessageListenerContainer(redisConnectionFactory, taskExecutor);
    }

    @ConfigurationProperties(prefix = GlobalRedisProperties.PREFIX)
    public static class GlobalRedisProperties extends RedisProperties {

        public static final String PREFIX = "global.data.redis";

        @Autowired
        public GlobalRedisProperties(DefaultRedisProperties defaultRedisProperties) {
            BeanUtils.copyProperties(defaultRedisProperties, this);
        }

    }

}

spring.data.redis.host: xxx

global.data.redis.enabled: true
global.data.redis.host: xxx

Comment From: ChildrenGreens

@wilkinsona As shown in the code demo by @quaff , there is a substantial similarity between the code in RedisConfigurationSupport and RedisConnectionConfiguration, which seems to go against the principles of inheritance in object-oriented programming. I understand the need for privacy in the Boot project, but is there a possibility of finding a good balance between the two?

Comment From: wilkinsona

I hope there's a way, hence my question about what you're doing at the moment, but I don't think making configuration classes part of the public API is the right approach as we may back ourselves into a corner in terms of future improvements.

One option is to tackle this in a more general way:

  • https://github.com/spring-projects/spring-boot/issues/15732
  • https://github.com/spring-projects/spring-boot/issues/22403

Another option, that may be easier to implement, would be to offer something similar to DataSourceBuilder but for Redis.

Comment From: ChildrenGreens

@wilkinsona I think the #15732 method you just mentioned is very good. But when can the Spring Boot project team provide support? We really need it.

Comment From: wilkinsona

15732 is in the 3.x milestone at the moment. That means that we don't have any immediate plans for it and it won't be part of 3.3. It may be implemented in a later 3.x release depending on demand and whether there are other higher priorities. In the meantime, I would recommend that you continue defining the necessary beans yourself. It may be more cumbersome that we'd like, but it is at least possible to do what you want.

I'm going to close this one for now as I'm not sure how useful a Redis equivalent of DataSourceBuilder would be. I think the right long-term solution for this is #15732.

Comment From: ChildrenGreens

@quaff I developed a multi-datasource Spring Boot Starter, and recently, during my free time, I decided to open-source it. Here’s the link: https://github.com/ChildrenGreens/multi-source-spring-boot-starter.

Comment From: quaff

FYI, I created https://github.com/additional-beans/additional-beans to configure additional beans without backing off Spring Boot auto-configured one.