我给SpringBoot提了个issue,被采纳了…
事情是这样的
项目中使用了springboot + spring data redis,但是公司规定,redis密码一律托管,只能远程获取。
开发环境使用的单实例redis,连接池用的是lettuce,同事的是实现是把Spring Data Redis自动装载的代码copy一份搬到项目里,原因从下面的分析中可以看出,Spring相关配置核心类都是包可见的,在外部根本无法继承和引用。
但是,好事者,也就是在下,觉得这“不够Spring”,于是,深挖了一番,并在一番分析之后,给社区提了一个比较中肯的Issue,并且被采纳。
Spring Data Redis 自动装配机制
在org.springframework.boot.autoconfigure.data.redis中有RedisAutoConfiguration, 其通过@Import依赖于LettuceConnectionConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
   @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;
   }
}
复制代码LettuceConnectionConfiguration 继承自RedisConnectionConfiguration,核心代码如下
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class)  // -->①
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)  // -->②
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
	LettuceConnectionConfiguration(RedisProperties properties,
			ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
			ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
		super(properties, sentinelConfigurationProvider, clusterConfigurationProvider);
	}
	@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)  // -->③
	LettuceConnectionFactory redisConnectionFactory(
			ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
			ClientResources clientResources) {
		LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
				getProperties().getLettuce().getPool());
		return createLettuceConnectionFactory(clientConfig);
	}
}
复制代码从中可以看出,Spring boot 自动装配Lettuce连接工厂的条件如下
① 存在 RedisClient , lettuce.io 中自带的redis 客户端类
② 项目中使用配置spring.redis.client-type 为lettuce
③ 项目代码中只要不定义RedisConnectionFactory , 便会自动按照配置文件创建 LettuceConnectionFactory
其中,包含两处关键,
- 构造函数 - LettuceConnectionConfiguration出现的- RedisProperties和两个- ObjectProvider,并且调用了父类构造函数
- redisConnectionFactory中包含两个重要方法- getLettuceClientConfiguration和- createLettuceConnectionFactory, 其中- getLettuceClientConfiguration主要处理Pool连接池的相关配置,不做赘述,从下面的分析也可以知道,- properties其实就是- RedisProperties,重点看- createLettuceConnectionFactory
下面,逐个解析这些关键点。
父类构造函数 RedisConnectionConfiguration
protected RedisConnectionConfiguration(RedisProperties properties,
      ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
      ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
   this.properties = properties;
   this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable();
   this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable();
}
复制代码理解这段代码的关键是ObjectProvider, 其实你如果细心留意,你会发现,Springboot的代码,特别是构造函数,大量的用到ObjectProvider
ObjectProvider
关于ObjectProvider ,  可以简单聊两句 Spring 4.3的一些改进
当构造方法的参数为单个构造参数时,可以不使用@Autowired进行注解
@Service
public class FooService {
    private final FooRepository repository;
    public FooService(FooRepository repository) {
        this.repository = repository
    }
}
复制代码比如,上面这段代码是spring 4.3之后的版本,不需要@Autowired 也可以正常运行。
同样是在Spring 4.3版本中,不仅隐式的注入了单构造参数的属性,还引入了
ObjectProvider接口。
//A variant of ObjectFactory designed specifically for injection points, allowing for programmatic optionality and lenient not-unique handling.
public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
    // ...省略了部分代码
    @Nullable
	T getIfAvailable() throws BeansException;
}
复制代码从源码注释中可以得知,ObjectProvider接口是ObjectFactory接口的扩展,专门为注入点设计的,可以让注入变得更加宽松和更具有可选项。
其中,由getIfAvailable()可见,当待注入参数的Bean为空或有多个时,便是ObjectProvider发挥作用的时候。
- 如果注入实例为空时,使用 - ObjectProvider则避免了强依赖导致的依赖对象不存在异常
- 如果有多个实例, - ObjectProvider的方法会根据Bean实现的- Ordered接口或- @Order注解指定的先后顺序获取一个- Bean, 从而了提供了一个更加宽松的依赖注入方式
回到,RedisConnectionConfiguration这个父类构造函数本身,其实就是实现这样的功能:如果用户提供了RedisSentinelConfiguration和 RedisSentinelConfiguration , 会在构造函数中加载进来,而RedisProperties则比较简单,就是redis的相关配置。
RedisProperties
从配置中读取redis的相关配置,最简单的单机redis配置的是简单的属性,sentinel是哨兵相关配置,cluster是集群相关配置,Pool是连接池的相关配置
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
	private int database = 0;
	private String url;
	private String host = "localhost";
	private int port = 6379;
	private String username;
	private String password;
	private Sentinel sentinel;
	private Cluster cluster;
	public static class Pool {}
	public static class Cluster {}
	public static class Sentinel {}
	// ... 省略非必要代码
}
复制代码小结一下,目前,我们可以看到RedisAutoConfiguration依赖于配置类LettuceConnectionConfiguration, 其构造函数读取了用户定义的redis配置,其中包含 单机配置+集群配置+哨兵配置+连接池配置,其中集群配置和哨兵配置是两个允许用户自定义的Bean。
createLettuceConnectionFactory
LettuceConnectionConfiguration中实现连接池的方法中调用了createLettuceConnectionFactory, 其实现如下
private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {
		if (getSentinelConfig() != null) {
			return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
		}
		if (getClusterConfiguration() != null) {
			return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
		}
		return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
	}
复制代码其实就是依次读取哨兵的配置,集群的配置 以及 单机的配置,如果有就创建连接池返回。
其中getSentinelConfig() 和 getClusterConfiguration() 是父类的方法,其实现如下,
protected final RedisSentinelConfiguration getSentinelConfig() {
   if (this.sentinelConfiguration != null) {
      return this.sentinelConfiguration;
   }
   RedisProperties.Sentinel sentinelProperties = this.properties.getSentinel();
   if (sentinelProperties != null) {
      RedisSentinelConfiguration config = new RedisSentinelConfiguration();
      // 省略装载代码
      config.setDatabase(this.properties.getDatabase());
      return config;
   }
   return null;
}
protected final RedisClusterConfiguration getClusterConfiguration() {
   if (this.clusterConfiguration != null) {
      return this.clusterConfiguration;
   }
   if (this.properties.getCluster() == null) {
      return null;
   }
   RedisProperties.Cluster clusterProperties = this.properties.getCluster();
   RedisClusterConfiguration config = new RedisClusterConfiguration(clusterProperties.getNodes());
   // 省略装载代码
   return config;
}
复制代码从中,我们可以知道,其优先读取在构造函数中由ObjectProvider引入的可能存在的用户自定义配置Bean,如果没有,再通过读取RedisProperties完成装配。
但是,细心的读者要问了,How about 单机配置?

protected final RedisStandaloneConfiguration getStandaloneConfig() {
   RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
   if (StringUtils.hasText(this.properties.getUrl())) {
      // 省略装载代码
   }
   else {
      // 省略装载代码
   }
   config.setDatabase(this.properties.getDatabase());
   return config;
}
复制代码是的,你没有看错,单身狗不配……

总结起来就是,在构造函数中获取合适的配置bean,然后在创建连接池的方法里面查找,如果没有就用配置文件构造一个,但是不支持单实例的redis。
提一个issue吧
保护单身狗,人人有责,于是,我以“单身狗保护协会”的名义给SpringBoot社区提了一个issue
 然后,大佬回复,可以保护可以支持,很开心。
然后,大佬回复,可以保护可以支持,很开心。
 其中,有提到使用
其中,有提到使用BeanPostProcessor的方法去改写RedisProperties的配置,中途我有想到,所以把issue关了,沉吟一阵,觉得不优雅,不开心,又把issue给打开了,很感谢开源团队的支持和理解,备受鼓舞。
作者:PeakSong
链接:https://juejin.cn/post/7008568299361402911
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
