Spring 应用记录(Bean的注册与注入机制)

写在前面

Hello,我是易元,记录工作问题,总结问题,实现自我突破。


1. 问题背景与现象

在 Spring Boot 项目中,我们一般会自定义 RedisTemplate<String, Object> Bean,并配置
Jackson2JsonRedisSerializer 作为 Key 和 Value 的序列化器,以实现 JSON 格式的数据存储。不过,在实际应用中,我们发现 ServerA 在调用方法时抛出了 SerializationException 异常,而 ServerB 和 ServerC 却运行正常。

Spring 应用记录(Bean的注册与注入机制)

疑问:为什么 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. 关键点

  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 的序列化。
  2. @Bean 和 @Bean(name = “…”) 的区别: @Bean 注解用于在 Spring 配置类中声明一个 Bean。它的名称决定了 Bean 在 IoC 容器中的唯一标识。
  3. @Bean (不指定 name):默认情况下,Bean 的名称是其方法名。例如,public RedisTemplate redisTemplate(…) 方法声明的 Bean,其名称就是 redisTemplate。
  4. @Bean(name = “…”):允许开发者显式地为 Bean 指定一个或多个名称。例如,@Bean(name = “RedisTemplateObject”) 会将该 Bean 注册为 RedisTemplateObject。这种显式命名在需要区分多个同类型 Bean 时超级有用。
  5. @Resource 的注入机制: @Resource 是 JSR-250 规范定义的注解,其注入机制与 Spring 自身的 @Autowired 有所不同,遵循一个两阶段的匹配规则:
  6. 优先按名称 (byName) 注入:@Resource 第一会尝试根据其 name 属性(如果指定)或者被注解的字段/方法名(如果未指定 name)来查找 IoC 容器中同名的 Bean。
  7. 回退到按类型 (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。

问题根源分析

  1. 自定义 RedisTemplate 的注册 在 RedisTemplateConfig 中,通过 @Bean(name = “stringRedisTemplate”) 声明了一个 RedisTemplate<String, Object> 类型的 Bean,其名称为 stringRedisTemplate。这个 Bean 使用了 Jackson2JsonRedisSerializer。
  2. Spring Boot 自动配置的 RedisTemplate 由于项目中存在 spring-boot-starter-data-redis 依赖,Spring Boot 的自动配置机制会创建一个默认的 RedisTemplate<Object, Object> Bean。由于在自定义的 Bean 显式指定了名称 stringRedisTemplate,并没有覆盖默认的 redisTemplate 名称,因此 Spring Boot 依旧会创建一个名为 redisTemplate 的默认 Bean。这个默认 Bean 使用 JdkSerializationRedisSerializer。
  3. ServerA 中的注入行为 在 ServerA 中,我们使用了 @Resource private RedisTemplate<String, Object> redisTemplate; 进行注入。根据 @Resource 的匹配规则:
  4. 第一尝试按名称匹配。由于字段名为 redisTemplate,会在 IoC 容器中寻找名为 redisTemplate 的 Bean。
  5. 此时 Spring 成功匹配了自动配置的那个名为 redisTemplate 的 Bean。虽然 Bean 的类型是 RedisTemplate<Object, Object>,与 ServerA中定义的 RedisTemplate<String, Object> 泛型不完全一致,但由于 Java 泛型在运行时会被擦除,Spring 在进行类型匹配时,会认为 RedisTemplate<Object, Object> 和 RedisTemplate<String, Object> 都是 RedisTemplate 类型,因此按名称匹配成功后,会直接注入这个默认的 RedisTemplate。
  6. 最后ServerA 实际使用的是**自动配置的、使用 JDK 序列化的 RedisTemplate**。
  7. ServerB、ServerC中的注入行为 (对比) ServerC 能够正常工作,其关键在于它使用了构造函数注入。Spring 在通过构造函数进行依赖注入时,其主要的匹配规则是**按类型 (byType)**。对于 RedisTemplate<String, Object> 这个构造函数参数,Spring 会去查找所有可以赋值给它的 Bean。 ServerB 使用了 @Autowired 注解, 与构造函数注入等同。
  8. 自定义的 Bean A:类型为 RedisTemplate<String, Object>,名称为 StringRedisTemplate。
  9. Spring 自动配置的 Bean B:类型为 RedisTemplate<Object, Object>,名称为 redisTemplate。
  10. 由于 RedisTemplate<String, Object> 是一个比 RedisTemplate<Object, Object> 更具体、更准确的类型,Spring 在按类型匹配时会优先选择泛型类型最匹配的那个。因此,ServerB 成功注入了自定义的、使用 Jackson2JsonRedisSerializer 的 RedisTemplate。

最终冲突: 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 也会成为默认的选择。

代码示例

@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。

代码示例

  1. 在注入点使用 @Qualifier
  2. 对于 ServerA (@Resource注解注入) @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”);
    }

    }

  3. 对于 ServerC (构造函数注入) @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。

代码示例

@Component
public class ServerA {
    // ...
    @Resource // 此时会查找名为 'stringRedisTemplate' 的 Bean
    private RedisTemplate<String, Object> stringRedisTemplate;
    // ...
}

4. 总结

4.1. 回顾

通过一个 Spring RedisTemplate 注入的实际案例,深入了解了 Spring 依赖注入机制。其中包含以下核心知识点

  • @Bean 注解 如何定义 Bean 及其命名规则,以及显式命名 (@Bean(name = “…”)) 的作用。
  • @Autowired Spring 框架提供的自动注入注解,默认按类型 (byType) 匹配,可用于字段、构造函数和 Setter 方法。
  • @Resource JSR-250 规范定义的注解,其两阶段匹配规则是**优先按名称 (byName),然后回退到按类型 (byType)**。它只能用于字段和 Setter 方法。
  • 构造函数注入 Spring 推荐的最佳实践,默认按类型 (byType) 匹配,能够保证依赖的不变性、非空性,并有助于发现循环依赖。
  • @Primary 解决多个同类型 Bean 注入冲突的机制,标记首选 Bean。
  • @Qualifier 通过指定 Bean 名称来准确控制注入,适用于需要区分多个同类型 Bean 的场景。

4.2. 经验总结

  1. 泛型擦除与运行时行为:Java 泛型在编译时擦除的特性,在 Spring IoC 容器进行类型匹配时可能会导致一些意想不到的行为。例如,RedisTemplate<Object, Object> 和 RedisTemplate<String, Object> 在运行时都被视为 RedisTemplate 类型。
  2. 避免隐式的行为:@Resource 的默认按名称匹配行为,在存在同名 Bean 时,可能会导致注入的不是期望中的 Bean。因此,在关键依赖的注入上,尽量使用 @Primary 或 @Qualifier 进行显式控制,减少不确定性。
© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
清明跳河图的头像 - 鹿快
评论 共1条

请登录后发表评论