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

愿天堂没有BUG

共 7510字,需浏览 16分钟

 ·

2021-09-22 09:48

事情是这样的

项目中使用了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 单机配置?

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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



浏览 49
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报