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

写在前面

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


1. 问题背景与现象

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

疑问 :为什么 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 容器中的唯一标识。

    • @Bean (不指定 name):默认情况下,Bean 的名称是其方法名。例如,public RedisTemplate redisTemplate(...) 方法声明的 Bean,其名称就是 redisTemplate
    • @Bean(name = "..."):允许开发者显式地为 Bean 指定一个或多个名称。例如,@Bean(name = "RedisTemplateObject") 会将该 Bean 注册为 RedisTemplateObject。这种显式命名在需要区分多个同类型 Bean 时非常有用。
  3. @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

问题根源分析

  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 的匹配规则:

    • 首先尝试按名称匹配。由于字段名为 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
  4. ServerBServerC中的注入行为 (对比)

    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 成功注入了自定义的、使用 Jackson2JsonRedisSerializerRedisTemplate

最终冲突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。

代码示例

  1. 在注入点使用 @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 的名称进行查找。 因此,如果我们将 ServerAredisTemplate 字段的名称修改为 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 = "...")) 的作用。
  • @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 进行显式控制,减少不确定性。
相关推荐
程序员游老板11 小时前
基于SpringBoot3_vue3_MybatisPlus_Mysql_Maven的社区养老系统/养老院管理系统
java·spring boot·mysql·毕业设计·软件工程·信息与通信·毕设
码事漫谈12 小时前
VS Code 1.107 更新:多智能体协同与开发体验升级
后端
码事漫谈12 小时前
从概念开始开始C++管道编程
后端
@淡 定12 小时前
Spring中@Autowired注解的实现原理
java·后端·spring
serendipity_hky12 小时前
【go语言 | 第2篇】Go变量声明 + 常用数据类型的使用
开发语言·后端·golang
疯狂的程序猴13 小时前
App Store上架完整流程与注意事项详解
后端
开心就好202513 小时前
把 H5 应用上架 App Store,并不是套个壳这么简单
后端
tirelyl13 小时前
LangChain.js 1.0 + NestJS 入门 Demo
后端
王中阳Go背后的男人13 小时前
GoFrame vs Laravel:从ORM到CLI工具的全面对比与迁移指南
后端·go
aiopencode13 小时前
uni-app 上架 iOS,并不是卡在技术,而是卡在流程理解
后端