为什么 Spring Boot 自动配置了 Redis,还要自己写 RedisTemplate?

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 是一堆看不懂的乱码。

这会带来三个问题:

  1. 没法排查问题 ------ 线上出问题时用 redis-cli 连上去,什么都看不出来
  2. 跨语言不兼容 ------ 其他语言的服务读到这串二进制没法解析
  3. 存储空间浪费 ------ 二进制序列化会带上类名、类型信息等额外开销

换成 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)。名字相同,自动配置就会让位,最终容器里生效的就是我们自定义的这个。

相关推荐
xixiaoyunya1 小时前
Spring Boot 3 统一响应与全局异常处理实战
java·spring boot·后端
JavaGuide2 小时前
DeepSeek Harness 官方桌面端终于有了!
前端·后端
SFLYQ2 小时前
隔离内网下 AI Agent 工程实战
后端·agent·ai编程
凤山老林2 小时前
Spring Boot 集成 Netty 构建高性能 TCP 长连接网关:协议解析、心跳检测与集群广播实战
spring boot·后端·tcp/ip
Json____3 小时前
基于 Spring Boot + Vue3 的在线考试管理系统实战
java·spring boot·后端·it学习·wwwoop.com
Sam_Deep_Thinking3 小时前
如何理解java的信号量
java·后端·面试·程序员
东风破_3 小时前
NestJS 快速入门:先别背装饰器,把一条请求跑明白
后端
晴空蓝天3 小时前
MDC traceId 全链路日志追踪:Spring Boot 3.5 里把日志串成一条线
java·spring boot·后端·python
明月_清风3 小时前
干了 6 年前端,我是怎么一步步转型到 AI 的?
前端·后端·ai编程