项目中遇到的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 数组的包装器添加到序列化结果中。

浙公网安备 33010602011771号