我给SpringBoot提了个issue,被采纳了…

4,117

事情是这样的

项目中使用了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连接工厂的条件如下

① 存在 RedisClientlettuce.io 中自带的redis 客户端类

② 项目中使用配置spring.redis.client-typelettuce

③ 项目代码中只要不定义RedisConnectionFactory , 便会自动按照配置文件创建 LettuceConnectionFactory


其中,包含两处关键,

  • 构造函数LettuceConnectionConfiguration 出现的RedisProperties 和两个ObjectProvider,并且调用了父类构造函数
  • redisConnectionFactory 中包含两个重要方法getLettuceClientConfigurationcreateLettuceConnectionFactory, 其中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这个父类构造函数本身,其实就是实现这样的功能:如果用户提供了RedisSentinelConfigurationRedisSentinelConfiguration , 会在构造函数中加载进来,而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 单机配置?

image-20210917000348170.png

protected final RedisStandaloneConfiguration getStandaloneConfig() {
   RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
   if (StringUtils.hasText(this.properties.getUrl())) {
      // 省略装载代码
   }
   else {
      // 省略装载代码
   }
   config.setDatabase(this.properties.getDatabase());
   return config;
}

是的,你没有看错,单身狗不配……

src=http___dn-p-tystore.qbox.me_p_chapter_attachment_eBjVEBIWES_Egfveg6VeB6segjUetbteluCfn9rG7Ldgn5Gig5aJHeOHuHa90ueJ6S.jpg&refer=http___dn-p-tystore.qbox.jpg

总结起来就是,在构造函数中获取合适的配置bean,然后在创建连接池的方法里面查找,如果没有就用配置文件构造一个,但是不支持单实例的redis。

提一个issue吧

保护单身狗,人人有责,于是,我以“单身狗保护协会”的名义给SpringBoot社区提了一个issue

image-20210917000825318.png 然后,大佬回复,可以保护可以支持,很开心。

image-20210917000939528.png 其中,有提到使用BeanPostProcessor的方法去改写RedisProperties的配置,中途我有想到,所以把issue关了,沉吟一阵,觉得不优雅,不开心,又把issue给打开了,很感谢开源团队的支持和理解,备受鼓舞。