写在前面
Hello,我是易元,记录工作问题,总结问题,实现自我突破。
1. 问题背景与现象
在 Spring Boot 项目中,我们通常会自定义 RedisTemplate<String, Object> Bean,并配置 Jackson2JsonRedisSerializer 作为 Key 和 Value 的序列化器,以实现 JSON 格式的数据存储。然而,在实际应用中,我们发现 ServerA 在调用方法时抛出了 SerializationException 异常,而 ServerB 和 ServerC 却运行正常。

疑问 :为什么
RedisTemplate在不同的服务中注入时会表现出不同的行为?为什么有的服务能正确获取到自定义的序列化器,而有的却回退到了默认的 JDK 序列化器?
排查发现当 RedisTemplate 配置方法中的 @Bean(name = "StringRedisTemplate") 参数值与 ServerA 中引入的 private RedisTemplate<String, Object> redisTemplate 属性名不一致时,就会出现序列化异常问题。
核心问题 :Spring IoC 容器中
RedisTemplate的自动配置和注入机制导致了不同实例的产生。
2. 问题根源分析
要理解上述问题,我们需要深入探讨 Spring IoC 容器中 Bean 的注册、匹配和注入机制,特别是 Spring Boot 的自动配置、@Bean 注解的不同用法以及 @Resource 的注入规则。
2.1. 关键点
-
Spring Boot 的自动配置机制 : Spring Boot 的核心之一是自动配置。当项目中引入
spring-boot-starter-data-redis等特定依赖时,Spring Boot 会根据类路径下的条件自动配置相关的 Bean。例如,当检测到spring-boot-starter-data-redis依赖时,RedisAutoConfiguration会被激活,默认创建一个RedisTemplate<Object, Object>类型的 Bean。这个默认的RedisTemplate通常使用JdkSerializationRedisSerializer进行 Key 和 Value 的序列化。 -
@Bean和@Bean(name = "...")的区别 :@Bean注解用于在 Spring 配置类中声明一个 Bean。它的名称决定了 Bean 在 IoC 容器中的唯一标识。@Bean(不指定name):默认情况下,Bean 的名称是其方法名。例如,public RedisTemplate redisTemplate(...)方法声明的 Bean,其名称就是redisTemplate。@Bean(name = "..."):允许开发者显式地为 Bean 指定一个或多个名称。例如,@Bean(name = "RedisTemplateObject")会将该 Bean 注册为RedisTemplateObject。这种显式命名在需要区分多个同类型 Bean 时非常有用。
-
@Resource的注入机制 :@Resource是 JSR-250 规范定义的注解,其注入机制与 Spring 自身的@Autowired有所不同,遵循一个两阶段的匹配规则:- 优先按名称 (
byName) 注入:@Resource首先会尝试根据其name属性(如果指定)或者被注解的字段/方法名(如果未指定name)来查找 IoC 容器中同名的 Bean。 - 回退到按类型 (
byType) 注入:如果按名称没有找到匹配的 Bean,@Resource会回退到按类型进行匹配。如果此时找到唯一匹配的 Bean,则进行注入;如果找到多个,则会抛出NoUniqueBeanDefinitionException。
- 优先按名称 (
2.2. 详细问题分析
场景描述
我们自定义了一个 RedisTemplate Bean,并显式命名为 StringRedisTemplate,配置了 Jackson2JsonRedisSerializer。同时,Spring Boot 自动配置也提供了一个默认的 RedisTemplate Bean,其名称为 redisTemplate,使用 JdkSerializationRedisSerializer。在 ServerA 中,我们使用 @Resource private RedisTemplate<String, Object> redisTemplate; 进行注入,结果却导致了 SerializationException。
问题根源分析
-
自定义
RedisTemplate的注册在
RedisTemplateConfig中,通过@Bean(name = "stringRedisTemplate")声明了一个RedisTemplate<String, Object>类型的 Bean,其名称为stringRedisTemplate。这个 Bean 使用了Jackson2JsonRedisSerializer。 -
Spring Boot 自动配置的
RedisTemplate由于项目中存在
spring-boot-starter-data-redis依赖,Spring Boot 的自动配置机制会创建一个默认的RedisTemplate<Object, Object>Bean。由于在自定义的 Bean 显式指定了名称stringRedisTemplate,并没有覆盖默认的redisTemplate名称,因此 Spring Boot 仍然会创建一个名为redisTemplate的默认 Bean。这个默认 Bean 使用JdkSerializationRedisSerializer。 -
ServerA中的注入行为在
ServerA中,我们使用了@Resource private RedisTemplate<String, Object> redisTemplate;进行注入。根据@Resource的匹配规则:- 首先尝试按名称匹配。由于字段名为
redisTemplate,会在 IoC 容器中寻找名为redisTemplate的 Bean。 - 此时 Spring 成功匹配了自动配置的那个名为
redisTemplate的 Bean。虽然 Bean 的类型是RedisTemplate<Object, Object>,与ServerA中定义的RedisTemplate<String, Object>泛型不完全一致,但由于 Java 泛型在运行时会被擦除,Spring 在进行类型匹配时,会认为RedisTemplate<Object, Object>和RedisTemplate<String, Object>都是RedisTemplate类型,因此按名称匹配成功后,会直接注入这个默认的RedisTemplate。 - 最后
ServerA实际使用的是自动配置的、使用 JDK 序列化的RedisTemplate。
- 首先尝试按名称匹配。由于字段名为
-
ServerB、ServerC中的注入行为 (对比)ServerC能够正常工作,其关键在于它使用了构造函数注入。Spring 在通过构造函数进行依赖注入时,其主要的匹配规则是按类型 (byType) 。对于RedisTemplate<String, Object>这个构造函数参数,Spring 会去查找所有可以赋值给它的 Bean。ServerB使用了@Autowired注解, 与构造函数注入等同。- 自定义的 Bean A:类型为
RedisTemplate<String, Object>,名称为StringRedisTemplate。 - Spring 自动配置的 Bean B:类型为
RedisTemplate<Object, Object>,名称为redisTemplate。 - 由于
RedisTemplate<String, Object>是一个比RedisTemplate<Object, Object>更具体、更精确 的类型,Spring 在按类型匹配时会优先选择泛型类型最匹配的那个。因此,ServerB成功注入了自定义的、使用Jackson2JsonRedisSerializer的RedisTemplate。
- 自定义的 Bean A:类型为
最终冲突 : ServerC 使用 Jackson 序列化器将数据(例如 User 类型的 user)序列化为 JSON 字符串并存入 Redis。而 ServerA 却注入了使用 JDK 序列化器的 RedisTemplate,当它尝试从 Redis 中获取数据并反序列化这些 JSON 字符串时,JDK 序列化器无法识别 JSON 格式,从而抛出 SerializationException。
3. 解决方案
为了确保所有需要注入自定义 RedisTemplate 的地方都能正确获取到我们期望的实例,可以采用以下几种方案。
3.1. 方案一:使用 @Primary
@Primary 注解是 Spring 框架提供的一种机制,用于解决当容器中存在多个相同类型的 Bean 时,指定一个首选 Bean 进行自动注入。当 Spring 在进行按类型注入时发现多个候选 Bean,如果其中一个被 @Primary 标记,那么将优先选择这个 Bean 进行注入。
通过在自定义 RedisTemplate 的 @Bean 定义上添加 @Primary,我们明确标记,当有多个 RedisTemplate 类型的 Bean 可供选择时,优先使用自定义的 Bean。这样,即使 Spring Boot 自动配置生成了另一个 RedisTemplate,我们的自定义 Bean 也会成为默认的选择。
代码示例:
less
@Configuration
public class RedisTemplateConfig {
@Primary
@Bean(name = "stringRedisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
Jackson2JsonRedisSerializer<Object> serializer = this.getSerializer();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
// 普通键值对的 Key 和 Value 序列化设置
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(serializer);
// Hash 结构的 Key 和 Value 序列化设置
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
// ... 其他内容
}
3.2. 方案二:统一注入方式为 @Qualifier
@Qualifier 注解是 Spring 框架提供的解决多个同类型 Bean 注入冲突的机制。它允许通过指定 Bean 的名称来精确地选择要注入的 Bean。当容器中存在多个相同类型的 Bean 时,@Qualifier 配合 @Autowired 或 @Resource 可以明确指定注入哪一个。
通过在注入点使用 @Qualifier("beanName"),直接告诉 Spring 容器,请注入名为 beanName 的那个 Bean。
代码示例:
-
在注入点使用
@Qualifier-
对于
ServerA(@Resource注解注入)less@Service public class ServerA { @Resource @Qualifier(value = "stringRedisTemplate") private RedisTemplate<String, Object> redisTemplate; public void test() { System.out.println(String.format("ServerA RedisTemplate 初始化地址: %s", redisTemplate)); redisTemplate.opsForValue().set("user:A", new User("张三", 20)); User user = (User) redisTemplate.opsForValue().get("user:A"); System.out.println(JSONObject.toJSONString(user)); redisTemplate.delete("user:A"); } } -
对于
ServerC(构造函数注入)typescript@Service public class ServerC { private final RedisTemplate<String, Object> redisTemplate; public ServerC( @Qualifier(value = "stringRedisTemplate") RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } public void test() { System.out.println(String.format("ServerC RedisTemplate 初始化地址: %s", redisTemplate)); redisTemplate.opsForValue().set("user:C", new User("张三", 20)); User user = (User) redisTemplate.opsForValue().get("user:C"); System.out.println(JSONObject.toJSONString(user)); redisTemplate.delete("user:C"); } }
-
3.3. 方案三:修改注入字段的名称
这种方案是利用 @Resource 默认按名称注入的特性,将注入点的字段名称直接修改为与目标 Bean 的名称一致。
原理 :@Resource 在未指定 name 属性时,会使用被注解的字段名作为 Bean 的名称进行查找。 因此,如果我们将 ServerA 中 redisTemplate 字段的名称修改为 stringRedisTemplate,那么 @Resource 就会直接找到我们自定义的 RedisTemplate。
代码示例:
typescript
@Component
public class ServerA {
// ...
@Resource // 此时会查找名为 'stringRedisTemplate' 的 Bean
private RedisTemplate<String, Object> stringRedisTemplate;
// ...
}
4. 总结
4.1. 回顾
通过一个 Spring RedisTemplate 注入的实际案例,深入了解了 Spring 依赖注入机制。其中包含以下核心知识点
@Bean注解 如何定义 Bean 及其命名规则,以及显式命名 (@Bean(name = "...")) 的作用。@AutowiredSpring 框架提供的自动注入注解,默认按类型 (byType) 匹配,可用于字段、构造函数和 Setter 方法。@ResourceJSR-250 规范定义的注解,其两阶段匹配规则是优先按名称 (byName),然后回退到按类型 (byType)。它只能用于字段和 Setter 方法。- 构造函数注入 Spring 推荐的最佳实践,默认按类型 (
byType) 匹配,能够保证依赖的不变性、非空性,并有助于发现循环依赖。 @Primary解决多个同类型 Bean 注入冲突的机制,标记首选 Bean。@Qualifier通过指定 Bean 名称来精确控制注入,适用于需要区分多个同类型 Bean 的场景。
4.2. 经验总结
- 泛型擦除与运行时行为 :Java 泛型在编译时擦除的特性,在 Spring IoC 容器进行类型匹配时可能会导致一些意想不到的行为。例如,
RedisTemplate<Object, Object>和RedisTemplate<String, Object>在运行时都被视为RedisTemplate类型。 - 避免隐式的行为 :
@Resource的默认按名称匹配行为,在存在同名 Bean 时,可能会导致注入的不是期望中的 Bean。因此,在关键依赖的注入上,尽量使用@Primary或@Qualifier进行显式控制,减少不确定性。