Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I suggest setting RedisConnectionConfiguration to protected #39160

Closed
ChildrenGreens opened this issue Jan 17, 2024 · 7 comments
Closed

I suggest setting RedisConnectionConfiguration to protected #39160

ChildrenGreens opened this issue Jan 17, 2024 · 7 comments
Labels
status: declined A suggestion or change that we don't feel we should currently apply

Comments

@ChildrenGreens
Copy link

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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 17, 2024
@wilkinsona
Copy link
Member

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?

@wilkinsona wilkinsona added the status: waiting-for-feedback We need additional information before we can continue label Jan 17, 2024
@quaff
Copy link
Contributor

quaff commented Jan 17, 2024

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

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jan 17, 2024
@ChildrenGreens
Copy link
Author

@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?

@ChildrenGreens ChildrenGreens closed this as not planned Won't fix, can't repro, duplicate, stale Jan 17, 2024
@wilkinsona
Copy link
Member

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:

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

@ChildrenGreens
Copy link
Author

@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.

@wilkinsona
Copy link
Member

#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.

@wilkinsona wilkinsona closed this as not planned Won't fix, can't repro, duplicate, stale Jan 17, 2024
@wilkinsona wilkinsona added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged status: feedback-provided Feedback has been provided labels Jan 17, 2024
@ChildrenGreens
Copy link
Author

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

4 participants