项目中遇到的Redis序列化的类型信息丢失问题

1. 问题场景

1.1 控制台报错

在使用 Spring Cache 和 Redis 缓存数据时,控制台出现以下错误:

org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Unexpected token (END_ARRAY), expected VALUE_STRING: need JSON String that contains type id (for subtype of java.lang.Object)

查询 Redis 数据库后发现,存储的数据为 [],确实丢失了类型信息。

1.2 定位问题代码

问题定位到以下代码中的 List.of() 返回值:

@Override
@Caching(
    cacheable = {
        // result 为 null 时,属于缓存穿透情况,缓存时间 30 分钟
        @Cacheable(value = RedisConstants.CacheName.HOT_SERVE, key = "#regionId", unless = "#result.size() != 0", cacheManager = RedisConstants.CacheManager.THIRTY_MINUTES),
        // result 不为 null 时,永久缓存
        @Cacheable(value = RedisConstants.CacheName.HOT_SERVE, key = "#regionId", unless = "#result.size() == 0", cacheManager = RedisConstants.CacheManager.FOREVER)
    }
)
public List<ServeAggregationSimpleResDTO> queryHotServeByRegionIdCache(Long regionId) {
    // 1. 校验当前城市是否为启用状态
    Region region = regionService.getById(regionId);
    // 2. 未启用则返回空列表
    if (ObjectUtil.isEmpty(region) || ObjectUtil.equal(FoundationStatusEnum.DISABLE.getStatus(), region.getActiveStatus())) {
        return List.of();
    }
    // 3. 查询热门服务数据
    return serveMapper.queryHotServeByRegionIdCache(regionId);
}

1.3 修改代码后的测试结果

将 List.of() 替换为 Collections.emptyList() 后再次测试,发现 Redis 上的数据变为:

["java.util.Collections$EmptyList", []]

这次包含了类型信息,问题得到解决。

2. 根本原因分析

2.1 序列化器配置

通过查看 Redis 序列化器配置,发现使用的是 Jackson2JsonRedisSerializer,并启用了 DefaultTyping.NON_FINAL:

private static final Jackson2JsonRedisSerializer<Object> JACKSON_SERIALIZER;

static {
    // 定义 Jackson 类型序列化对象
    JACKSON_SERIALIZER = new Jackson2JsonRedisSerializer<>(Object.class);
    ObjectMapper om = new ObjectMapper();

    // 添加自定义序列化器和反序列化器
    SimpleModule simpleModule = new SimpleModule()
        .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT)))
        .addSerializer(BigInteger.class, ToStringSerializer.instance)
        .addSerializer(Long.class, ToStringSerializer.instance)
        .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT)));
    om.registerModule(simpleModule);

    // 配置默认类型激活机制
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
    JACKSON_SERIALIZER.setObjectMapper(om);
}

其中,ObjectMapper.DefaultTyping.NON_FINAL 表示只对非 final 类的对象启用类型信息。

2.2 List.of() 和 Collections.emptyList() 的实现差异

虽然两者都返回不可变的空列表,但它们的实现方式不同:

  • List.of():实现类是 ImmutableCollections$ListN,这是一个 final 类。
  • Collections.emptyList(): 实现类是 Collections$EmptyList,这是一个非 final 类。

3. 理解 List.of() 和 Collections.emptyList() 的区别

3.1 List.of() 的底层实现

List.of() 是 Java 9 引入的一个方法,用于创建不可变列表。它的实现位于 java.util.ImmutableCollections 类中。具体来说:

  • 空列表的实现
    当调用 List.of() 创建一个空列表时,返回的是一个静态常量实例:
    static final List<?> EMPTY_LIST = new ListN<>();
    这里的 ListN 是 ImmutableCollections 的一个内部类,专门用来表示不可变列表。 因此,List.of() 创建的空列表是通过一个静态常量共享的,表现上类似于单例。
  • 非空列表的实现:
    当调用 List.of(e1, e2, ...) 创建一个包含元素的列表时,每次都会生成一个新的 ListN 实例:
    return new ListN<>(elements);
    对于非空列表:List.of() 每次都会创建一个新的实例,因此并不是单例。

3.2 Collections.emptyList() 的底层实现

返回一个静态常量实例:public static final List EMPTY_LIST = new EmptyList<>();,它是 Collections 类中的一个单例对象,

4. 项目中的序列化器Jackson2JsonRedisSerializer的参数分析

4.1 Jackson2JsonRedisSerializer

Jackson2JsonRedisSerializer 是 Spring Data Redis 提供的一个序列化器,用于将 Java 对象序列化为 JSON 格式,并存储到 Redis 中。它的主要依赖是 Jackson 的 ObjectMapper,通过 ObjectMapper 来完成对象的序列化和反序列化。
在我的项目中,JACKSON_SERIALIZER 被定义为一个静态常量,并且配置了一个自定义的 ObjectMapper 实例。

4.2 自定义 ObjectMapper 配置

  • (1) 解决日期和数字类型问题:
    SimpleModule simpleModule = new SimpleModule()
          // 添加反序列化器
          .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT)))
          // 添加序列化器
          .addSerializer(BigInteger.class, ToStringSerializer.instance)
          .addSerializer(Long.class, ToStringSerializer.instance)   // 实现 Long --> String 的序列化器
          .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT)));
    om.registerModule(simpleModule);
    
    • 这段代码创建了一个 SimpleModule,并注册了自定义的序列化器和反序列化器。
    • 目的:
      • 将 Long 类型序列化为字符串(避免在前端显示时丢失精度)。
      • 确保日期格式以指定的格式(例如 yyyy-MM-dd HH:mm:ss)进行序列化和反序列化。
  • (2) 设置属性可见性:
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    
    • 这行代码设置了 Jackson 的属性可见性规则。
    • 作用:
      • 允许 Jackson 自动检测所有字段(包括私有字段),无需显式使用 @JsonProperty 注解。
      • 默认情况下,Jackson 只会序列化公共字段或带有注解的字段,因此这一步是为了简化配置。
  • (3) 启用默认类型信息:
    om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
    
    • 这是整个配置中最关键的部分,它启用了 Jackson 的类型信息机制。
    • 参数解释:
      • LaissezFaireSubTypeValidator.instance:允许所有子类型被序列化。
      • ObjectMapper.DefaultTyping.NON_FINAL:表示对非 final 类型的对象启用类型信息。
      • JsonTypeInfo.As.WRAPPER_ARRAY:将类型信息作为 JSON 数组的包装器添加到序列化结果中。
posted @ 2025-03-31 16:32  cmk33  阅读(147)  评论(0)    收藏  举报