我給SpringBoot提了個(gè)issue,被采納了…
事情是這樣的
項(xiàng)目中使用了springboot + spring data redis,但是公司規(guī)定,redis密碼一律托管,只能遠(yuǎn)程獲取。
開發(fā)環(huán)境使用的單實(shí)例redis,連接池用的是lettuce,同事的是實(shí)現(xiàn)是把Spring Data Redis自動(dòng)裝載的代碼copy一份搬到項(xiàng)目里,原因從下面的分析中可以看出,Spring相關(guān)配置核心類都是包可見的,在外部根本無法繼承和引用。
但是,好事者,也就是在下,覺得這“不夠Spring”,于是,深挖了一番,并在一番分析之后,給社區(qū)提了一個(gè)比較中肯的Issue,并且被采納。
Spring Data Redis 自動(dòng)裝配機(jī)制
在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;
}
}
復(fù)制代碼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);
}
}
復(fù)制代碼從中可以看出,Spring boot 自動(dòng)裝配Lettuce連接工廠的條件如下
① 存在 RedisClient , lettuce.io 中自帶的redis 客戶端類
② 項(xiàng)目中使用配置spring.redis.client-type 為lettuce
③ 項(xiàng)目代碼中只要不定義RedisConnectionFactory , 便會(huì)自動(dòng)按照配置文件創(chuàng)建 LettuceConnectionFactory
其中,包含兩處關(guān)鍵,
構(gòu)造函數(shù)
LettuceConnectionConfiguration出現(xiàn)的RedisProperties和兩個(gè)ObjectProvider,并且調(diào)用了父類構(gòu)造函數(shù)redisConnectionFactory中包含兩個(gè)重要方法getLettuceClientConfiguration和createLettuceConnectionFactory, 其中getLettuceClientConfiguration主要處理Pool連接池的相關(guān)配置,不做贅述,從下面的分析也可以知道,properties其實(shí)就是RedisProperties,重點(diǎn)看createLettuceConnectionFactory
下面,逐個(gè)解析這些關(guān)鍵點(diǎn)。
父類構(gòu)造函數(shù) RedisConnectionConfiguration
protected RedisConnectionConfiguration(RedisProperties properties,
ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
this.properties = properties;
this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable();
this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable();
}
復(fù)制代碼理解這段代碼的關(guān)鍵是ObjectProvider, 其實(shí)你如果細(xì)心留意,你會(huì)發(fā)現(xiàn),Springboot的代碼,特別是構(gòu)造函數(shù),大量的用到ObjectProvider
ObjectProvider
關(guān)于ObjectProvider , 可以簡(jiǎn)單聊兩句 Spring 4.3的一些改進(jìn)
當(dāng)構(gòu)造方法的參數(shù)為單個(gè)構(gòu)造參數(shù)時(shí),可以不使用@Autowired進(jìn)行注解
@Service
public class FooService {
private final FooRepository repository;
public FooService(FooRepository repository) {
this.repository = repository
}
}
復(fù)制代碼比如,上面這段代碼是spring 4.3之后的版本,不需要@Autowired 也可以正常運(yùn)行。
同樣是在Spring 4.3版本中,不僅隱式的注入了單構(gòu)造參數(shù)的屬性,還引入了
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;
}
復(fù)制代碼從源碼注釋中可以得知,ObjectProvider接口是ObjectFactory接口的擴(kuò)展,專門為注入點(diǎn)設(shè)計(jì)的,可以讓注入變得更加寬松和更具有可選項(xiàng)。
其中,由getIfAvailable()可見,當(dāng)待注入?yún)?shù)的Bean為空或有多個(gè)時(shí),便是ObjectProvider發(fā)揮作用的時(shí)候。
如果注入實(shí)例為空時(shí),使用
ObjectProvider則避免了強(qiáng)依賴導(dǎo)致的依賴對(duì)象不存在異常如果有多個(gè)實(shí)例,
ObjectProvider的方法會(huì)根據(jù)Bean實(shí)現(xiàn)的Ordered接口或@Order注解指定的先后順序獲取一個(gè)Bean, 從而了提供了一個(gè)更加寬松的依賴注入方式
回到,RedisConnectionConfiguration這個(gè)父類構(gòu)造函數(shù)本身,其實(shí)就是實(shí)現(xiàn)這樣的功能:如果用戶提供了RedisSentinelConfiguration和 RedisSentinelConfiguration , 會(huì)在構(gòu)造函數(shù)中加載進(jìn)來,而RedisProperties則比較簡(jiǎn)單,就是redis的相關(guān)配置。
RedisProperties
從配置中讀取redis的相關(guān)配置,最簡(jiǎn)單的單機(jī)redis配置的是簡(jiǎn)單的屬性,sentinel是哨兵相關(guān)配置,cluster是集群相關(guān)配置,Pool是連接池的相關(guān)配置
@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 {}
// ... 省略非必要代碼
}
復(fù)制代碼小結(jié)一下,目前,我們可以看到RedisAutoConfiguration依賴于配置類LettuceConnectionConfiguration, 其構(gòu)造函數(shù)讀取了用戶定義的redis配置,其中包含 單機(jī)配置+集群配置+哨兵配置+連接池配置,其中集群配置和哨兵配置是兩個(gè)允許用戶自定義的Bean。
createLettuceConnectionFactory
LettuceConnectionConfiguration中實(shí)現(xiàn)連接池的方法中調(diào)用了createLettuceConnectionFactory, 其實(shí)現(xiàn)如下
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);
}
復(fù)制代碼其實(shí)就是依次讀取哨兵的配置,集群的配置 以及 單機(jī)的配置,如果有就創(chuàng)建連接池返回。
其中getSentinelConfig() 和 getClusterConfiguration() 是父類的方法,其實(shí)現(xiàn)如下,
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;
}
復(fù)制代碼從中,我們可以知道,其優(yōu)先讀取在構(gòu)造函數(shù)中由ObjectProvider引入的可能存在的用戶自定義配置Bean,如果沒有,再通過讀取RedisProperties完成裝配。
但是,細(xì)心的讀者要問了,How about 單機(jī)配置?

protected final RedisStandaloneConfiguration getStandaloneConfig() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
if (StringUtils.hasText(this.properties.getUrl())) {
// 省略裝載代碼
}
else {
// 省略裝載代碼
}
config.setDatabase(this.properties.getDatabase());
return config;
}
復(fù)制代碼是的,你沒有看錯(cuò),單身狗不配……

總結(jié)起來就是,在構(gòu)造函數(shù)中獲取合適的配置bean,然后在創(chuàng)建連接池的方法里面查找,如果沒有就用配置文件構(gòu)造一個(gè),但是不支持單實(shí)例的redis。
提一個(gè)issue吧
保護(hù)單身狗,人人有責(zé),于是,我以“單身狗保護(hù)協(xié)會(huì)”的名義給SpringBoot社區(qū)提了一個(gè)issue
然后,大佬回復(fù),可以保護(hù)可以支持,很開心。
其中,有提到使用BeanPostProcessor的方法去改寫RedisProperties的配置,中途我有想到,所以把issue關(guān)了,沉吟一陣,覺得不優(yōu)雅,不開心,又把issue給打開了,很感謝開源團(tuán)隊(duì)的支持和理解,備受鼓舞。
作者:PeakSong
鏈接:https://juejin.cn/post/7008568299361402911
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
