Spring Boot 中 RedisTemplate 是如何生效的?从 @Configuration、@Bean 到依赖注入
在项目里接入 Redis 的时候,我遇到了一个比较困惑的问题:
Spring Boot 已经自动配置 Redis 了,为什么还需要自己定义一个
RedisTemplate<String, Object>?
答案其实很简单:为了让 Redis 里存的数据是能看懂的。
Spring Boot 默认提供的 RedisTemplate 用的是 JDK 二进制序列化,存进去的 key 带一串乱码前缀,value 更是一堆看不懂的二进制。所以我加了这个配置类,把 key 换成字符串序列化、value 换成 JSON 序列化。
但真正让我困惑的是:这个配置类到底是怎么生效的?它凭什么能顶掉自动配置里那个 RedisTemplate?
顺着这个问题,我把 Spring 的 Bean 注册与注入机制从头理了一遍,这篇文章就是整理的结果。
当时看到这样的配置:
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// key 使用字符串序列化
StringRedisSerializer stringSerializer =
new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// value 使用 JSON 序列化
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
问题来了:
这个配置类到底是怎么生效的?
一、先理解整个过程
先不要急着看代码,可以把 Spring Boot 启动过程简化成下面这样:
text
Spring Boot 启动
↓
扫描项目中的组件
↓
发现 RedisConfig
↓
@Configuration
↓
解析 @Bean 方法
↓
执行 redisTemplate()
↓
得到 RedisTemplate<String, Object>
↓
注册到 Spring IoC 容器
↓
其他组件需要 RedisTemplate
↓
从 IoC 容器中注入
所以真正起作用的并不是 RedisConfig 自己主动调用了什么,而是:
Spring 在启动的时候发现了这个配置类,然后根据
@Bean创建对象并放进 IoC 容器。
二、@Configuration 到底做了什么?
我们的配置类:
java
@Configuration
public class RedisConfig {
}
@Configuration 的作用可以简单理解成:
告诉 Spring:这是一个配置类,请处理这个类中的 Bean 定义。
但是 Spring 怎么找到这个类?
这就涉及 @SpringBootApplication。
例如启动类:
java
@SpringBootApplication
public class OJApplication {
public static void main(String[] args) {
SpringApplication.run(OJApplication.class, args);
}
}
@SpringBootApplication 本身包含了组件扫描能力。
假设项目结构是:
text
com.lxy.oj
├── OJApplication.java
├── config
│ └── RedisConfig.java
├── controller
├── service
└── mapper
启动类位于:
text
com.lxy.oj
Spring Boot 会扫描这个包及其子包。
因此能够找到:
text
com.lxy.oj.config.RedisConfig
发现它有:
java
@Configuration
于是 Spring 就知道:
这个类是一个配置类,需要进一步解析。
三、@Bean 才是真正创建 RedisTemplate 的地方
配置类里面有:
java
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
// ...
return template;
}
这里的 @Bean 可以理解成:
把这个方法返回的对象交给 Spring 管理。
因此:
java
return template;
返回的:
text
RedisTemplate<String, Object>
最终会进入:
text
Spring IoC 容器
可以画成:
text
RedisConfig
│
│ @Bean
↓
redisTemplate()
│
│ return
↓
RedisTemplate<String, Object>
│
↓
Spring IoC 容器
四、为什么 RedisConnectionFactory 不需要自己创建?
注意我们的代码:
java
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
这里需要一个:
java
RedisConnectionFactory
但是我们没有:
java
new RedisConnectionFactory()
这是为什么?
因为 Spring Boot 的 Redis 自动配置已经帮我们创建好了相关 Bean。
所以整个过程实际上是:
text
Spring Boot Redis 自动配置
↓
RedisConnectionFactory
↓
Spring IoC 容器
然后 Spring 发现:
java
redisTemplate(RedisConnectionFactory connectionFactory)
需要这个 Bean。
于是自动从容器中找到:
text
RedisConnectionFactory
再传给:
java
redisTemplate(...)
最终:
text
RedisConnectionFactory
│
↓
redisTemplate(connectionFactory)
│
↓
RedisTemplate<String,Object>
│
↓
Spring IoC 容器
这就是 Spring 的依赖注入。
五、所以 RedisTemplate<String,Object> 到底是什么?
执行完配置类之后,可以简单认为 Spring 容器里面多了一个 Bean:
text
Bean名称:
redisTemplate
Bean类型:
RedisTemplate<String,Object>
为什么名称是 redisTemplate?
因为:
java
@Bean
public RedisTemplate<String, Object> redisTemplate(...)
默认情况下,@Bean 方法名就是 Bean 名称。
所以:
text
@Bean
public RedisTemplate<String,Object> redisTemplate()
对应:
text
Bean Name = redisTemplate
六、其他地方为什么可以直接注入?
既然这个对象已经放进 Spring IoC 容器,那么其他组件就可以使用它。
例如:
java
@Resource
private RedisTemplate<String, Object> redisTemplate;
或者:
java
@Autowired
private RedisTemplate<String, Object> redisTemplate;
Spring 就会从容器中寻找符合条件的 Bean。
可以理解成:
text
Spring IoC 容器
│
│
RedisTemplate
│
↓
┌─────────────────┐
│ Service │
│ │
│ @Autowired │
│ RedisTemplate │
└─────────────────┘
所以:
@Bean负责把对象放进去,@Autowired/@Resource负责把对象拿出来。
这是理解 Spring IoC 的一个非常重要的思路。
七、RedisTemplate<Object,Object> 和 RedisTemplate<String,Object> 有什么区别?
这里是最容易产生疑问的地方。
先说结论:这两个 Bean 不会同时存在。
Spring Boot 的 RedisAutoConfiguration 源码是这样的:
java
@AutoConfiguration
@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;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
注意这里两个 Bean 的条件注解写法不一样:
text
redisTemplate → @ConditionalOnMissingBean(name = "redisTemplate") 按【名称】判断
stringRedisTemplate → @ConditionalOnMissingBean 按【类型】判断
为什么 redisTemplate 偏偏要按名称判断?
因为泛型。
Spring Boot 想提供的默认值是 RedisTemplate<Object, Object>,而用户自定义的通常是 RedisTemplate<String, Object>。
假设这里按类型 判断:RedisTemplate<String, Object> 的运行时类型也是 RedisTemplate,条件会认为"容器里已经有 RedisTemplate 了",于是自动配置的照常创建 ------ 用户的自定义就永远覆盖不掉默认值。
所以 Spring Boot 特意写成 name = "redisTemplate":
只要你定义了一个名字叫
redisTemplate的 Bean,不管泛型写成什么,我都让位。
所以我们那段配置实际做了什么
java
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory)
@Bean 不指定名字时,方法名就是 Bean 名称 ,所以注册出来的 Bean 名字正好是 redisTemplate。
启动时的顺序是:
text
处理 RedisConfig
↓
注册一个名为 redisTemplate 的 Bean
│
↓
处理 RedisAutoConfiguration
↓
检查 @ConditionalOnMissingBean(name = "redisTemplate")
↓
容器里已经有这个名字了
↓
条件不满足 → 自动配置的 RedisTemplate<Object,Object> 不创建
所以容器里始终只有一个 RedisTemplate,就是我们自己定义的那个。
那么泛型的区别体现在哪里?体现在注入时能不能匹配上:
text
RedisTemplate<Object,Object> ≠ RedisTemplate<String,Object>
这也是为什么当某个组件明确要求 RedisTemplate<String,Object> 时,不能简单认为:
"反正已经有一个 RedisTemplate 了,直接用不就行了?"
不一定 ------ 容器里那个默认的 RedisTemplate<Object,Object> 泛型对不上,注入会失败。而我们的配置之所以能覆盖掉默认值,靠的正是 Bean 名称相同。
八、@Autowired 和 @Resource 有什么区别?
这两个注解都可以进行依赖注入,但是匹配思路不同。
1. @Autowired
最常见的理解:
主要按照类型进行匹配。
例如:
java
@Autowired
private RedisTemplate<String, Object> redisTemplate;
Spring 会尝试寻找匹配的:
text
RedisTemplate<String,Object>
如果有多个候选 Bean,就可能产生:
text
NoUniqueBeanDefinitionException
这时需要额外指定用哪一个,具体做法见第九节。
2. @Resource
@Resource 可以简单记成:
先按照名称找,再考虑类型。
例如:
java
@Resource
private RedisTemplate<String, Object> redisTemplate;
字段名称是:
text
redisTemplate
所以会优先尝试寻找:
text
Bean名称 = redisTemplate
而我们定义:
java
@Bean
public RedisTemplate<String, Object> redisTemplate(...)
恰好默认 Bean 名称也是:
text
redisTemplate
于是就匹配上了。
小结
text
@Autowired 类型优先,名称只是多候选时的兜底
@Resource 名称优先,名称找不到才按类型
九、什么时候才会真的出现两个 RedisTemplate?
前面说过,只要 Bean 名字叫 redisTemplate,自动配置就会让位。那反过来 ------ 如果名字不叫 redisTemplate 呢?
把方法名改一下:
java
@Bean
public RedisTemplate<String, Object> myRedisTemplate(RedisConnectionFactory connectionFactory) {
...
}
这时 Bean 名称变成 myRedisTemplate,redisTemplate 这个名字就空出来了,自动配置的条件重新满足,于是两个 Bean 会同时存在:
text
Bean 类型
────────────────────────────────────────
redisTemplate RedisTemplate<Object,Object> ← 自动配置创建
myRedisTemplate RedisTemplate<String,Object> ← 我们的配置创建
这时才轮到泛型匹配出场。注入 RedisTemplate<String,Object> 时:
java
@Autowired
private RedisTemplate<String, Object> template;
Spring 会按泛型筛选,只有 myRedisTemplate 的泛型完全匹配,所以能正常注入。
但如果两个 Bean 的泛型也完全一样:
text
Bean 类型
────────────────────────────────────────
redisTemplate RedisTemplate<String,Object>
anotherRedisTemplate RedisTemplate<String,Object>
那就真的分不清了,会报:
text
NoUniqueBeanDefinitionException
可以这样解决:
java
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String,Object> redisTemplate;
而 @Resource 可以直接指定名称:
java
@Resource(name = "redisTemplate")
private RedisTemplate<String,Object> redisTemplate;
小结 :泛型匹配和 @Qualifier 只在"容器里真的存在多个候选"时才有意义。默认情况下,因为我们占用了 redisTemplate 这个名字,容器里只有一个 RedisTemplate,根本不会冲突。
十、把几个概念放在一起
现在可以把整个过程串起来:
text
@SpringBootApplication
│
↓
组件扫描
│
↓
发现 @Configuration
│
↓
RedisConfig
│
↓
发现 @Bean
│
↓
执行 redisTemplate()
│
↓
RedisTemplate<String,Object>
│
↓
放入 Spring IoC 容器(Bean 名称:redisTemplate)
│
↓
RedisAutoConfiguration 检查 @ConditionalOnMissingBean(name = "redisTemplate")
│
↓
名字已被占用 → 自动配置的 RedisTemplate<Object,Object> 不创建
│
↓
┌────────────────────────────┐
│ Spring IoC 容器 │
│ │
│ RedisConnectionFactory │
│ RedisTemplate<String,Object>│ ← 只有这一个
│ StringRedisTemplate │
└────────────────────────────┘
│
↓
@Autowired / @Resource
│
↓
其他组件获得 RedisTemplate
这样就能理解:
Spring Boot 的自动配置和我们的自定义配置,是"默认值"与"覆盖值"的关系:一旦我们自己提供了同名的 Bean,自动配置就会主动让位,而不是两者并存。
十一、默认的序列化到底有什么问题?
前面说过,这个配置类的作用是换掉序列化方式。但有一点可能反直觉:
就算把配置类删掉,程序照样能启动。
实测:把配置类注掉,照样能启动
我把 RedisConfig 上的 @Configuration 注释掉,重新编译启动:
text
Started MainApplication in 7.47 seconds
正常启动,没有任何 Bean 相关的报错。
原因很简单:Spring Boot 的自动配置本来就提供了 RedisTemplate<Object, Object>,容器里从来就不缺 RedisTemplate。我们自定义的那个,只是把它换掉了。
那问题就变成了:既然不缺,为什么还要换掉它?
因为默认的序列化方式不好用
关键在于 Spring Boot 默认的 RedisTemplate<Object, Object> 用的是 JdkSerializationRedisSerializer,它会把对象按 Java 的二进制格式写进 Redis。
实际效果是这样:
text
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x0blogin:user:1"
127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x0blogin:user:1"
"\xac\xed\x00\x05sr\x00\x11com.lxy.oj.User\xa3\xf1\x1d..."
key 前面多了一串二进制前缀,value 是一堆看不懂的乱码。
这会带来三个问题:
- 没法排查问题 ------ 线上出问题时用
redis-cli连上去,什么都看不出来 - 跨语言不兼容 ------ 其他语言的服务读到这串二进制没法解析
- 存储空间浪费 ------ 二进制序列化会带上类名、类型信息等额外开销
换成 JSON 序列化
所以我们把两个序列化器换掉:
java
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// key 用字符串序列化
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// value 用 JSON 序列化
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
换完之后再看:
text
127.0.0.1:6379> keys *
1) "login:user:1"
127.0.0.1:6379> get "login:user:1"
"{\"id\":1,\"name\":\"张三\",\"age\":20}"
key 是普通字符串,value 是能直接看懂的 JSON。
对比一下
text
自动配置的 我们自定义的
类型 RedisTemplate<Object, Object> RedisTemplate<String, Object>
key 序列化 JdkSerializationRedisSerializer StringRedisSerializer
value 序列化 JdkSerializationRedisSerializer Jackson2JsonRedisSerializer
redis-cli 看 乱码 可读
所以这段配置的定位
它不是为了让程序能跑起来 (不写也能跑),而是为了让 Redis 里的数据可读、可排查、跨语言兼容。
顺带一提,afterPropertiesSet() 也别漏 ------ 它负责校验 connectionFactory 是否设置、初始化序列化器,不调用的话部分配置不会生效。
十二、最后总结
理解这部分,其实只需要抓住五个核心概念:
@Configuration
告诉 Spring:
text
这是一个配置类
@Bean
告诉 Spring:
text
这个方法返回的对象交给 IoC 容器管理
默认情况下,方法名就是 Bean 名称。
@ConditionalOnMissingBean
自动配置里的"保险丝":
text
容器里没有指定的 Bean 时,我才创建
写成 name = "xxx" 时按名称判断,不写时按类型判断。
@Autowired
可以简单理解为:
text
主要按照类型寻找 Bean
@Resource
可以简单理解为:
text
优先按照名称寻找 Bean,再考虑类型
最终形成:
text
@Configuration
↓
发现配置类
@Bean
↓
创建并注册 Bean(名字 = 方法名)
IoC 容器
↓
保存 Bean
@ConditionalOnMissingBean
↓
自动配置检查同名/同类型 Bean 是否已存在
已存在 → 让位,不创建
@Autowired / @Resource
↓
注入 Bean
而这次遇到的 Redis 问题,本质上就是:
Spring Boot 默认提供的
RedisTemplate<Object,Object>用的是 JDK 二进制序列化,存进 Redis 的数据在redis-cli里是一堆乱码,满足不了业务需要。于是我们通过@Configuration + @Bean定义了一个名为redisTemplate的RedisTemplate<String,Object>(String key + JSON value)。名字相同,自动配置就会让位,最终容器里生效的就是我们自定义的这个。