Java 中 Redis 存储泛型对象 Map 的反序列化陷阱
背景介绍 本项目采用了双层缓存机制: 本地缓存使用 Caffeine 2.8.8 远程缓存使用 Redis 系统运行在 JDK 1.8 我们将一个包含多个字段的 Map 对象整体写入 Redis,其中 "list" 字段的值为一个 List 类型的对象列表: 写入 Redis 前:值是 Java 内存中的 List 从 Redis 反序列化后:值变成了 List,由于类型信息丢失,强制转换会抛出 ClassCastException 以下是相关缓存处理代码片段: // redis序列化代码 @Slf4j public class RedisObjectSerializer implements RedisSerializer { private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private final Converter deserializingConverter = new DeserializingConverter(); @Override public byte[] serialize(Object obj) { // 这个时候没有要序列化的对象出现,所以返回的字节数组应该就是一个空数组 if (obj == null) { return EMPTY_BYTE_ARRAY; } // 将对象变为字节数组 byte[] data = JSON.toJSONBytes(obj, SerializerFeature.WriteClassName); return data; } @Override public Object deserialize(byte[] data) { // 此时没有对象的内容信息 if (data == null || data.length == 0) { return null; } try { return JSON.parse(data, Feature.SupportAutoType); } catch (Exception e) { try { return this.deserializingConverter.convert(data); } catch (Exception e1) { log.info("无法反序列化redis数据:{}", new String(data, StandardCharsets.UTF_8), e); } throw e; } } } //数据上游,填充数据阶段,返回map Cache cache = GenericCacheManager .getWriteExpireCacheByName("queryData", 200, 120); String cacheKey = "cache-key"; String redisKey = "redisKey"; Map finalResult = cache.get(cacheKey, method -> { Map redisCache = (Map) redisTemplate.opsForValue().get(redisKey); if (null != redisCache) { return redisCache; } Map map = Maps.newHashMap(); List list = queryListData(); map.put("list", list); Map tCollect = list.stream().collect(Collectors.groupingBy(NewTournamentVO::getSportId)); map.put("tournament", tCollect); redisTemplate.opsForValue().set(redisKey, map, 150, TimeUnit.SECONDS); return map; }); // 获取缓存数据,强制类型转换(潜在风险点) List matchList = (List) finalResult.get("list"); // 后续处理 List process = processResult(matchList); private List processResult(List list) { List items = new ArrayList(); if (CollectionUtils.isEmpty(list)) { return items; } list = list.stream().distinct().collect(Collectors.toList()); // 在这里报错 java.lang.ClassCastException: java.util.ArrayList cannot be cast to com.qiutx.product.model.vo.NewLiveMatchVO list.forEach(e -> { List item = new ArrayList(); items.add(item); }); return items; } 根本原因 Fastjson 在序列化整个 Map 的时候,如果 Map 内的值(例如 List)没有显式类型信息(@type),它就不会自动对 List 内的泛型对象添加类型。 所以即使你用了 SerializerFeature.WriteClassName,最终只记录了 Map 和 Map 的 key 是 "list",value 是个 List,而 NewLiveMatchVO 的类型信息丢失了。 反序列化回来就是这个样子: Map finalResult = redisTemplate.opsForValue().get("xxx"); // 此时 finalResult.get("list") 实际是 List(不是 List) 去强转 List 就会炸: (List) finalResult.get("list"); // ❌ ClassCastException 处理办法有很多,比如:只存 JSON 字符串、使用泛型反序列化(不推荐 Map),使用专门的 VO 包装类

背景介绍
本项目采用了双层缓存机制:
- 本地缓存使用 Caffeine 2.8.8
- 远程缓存使用 Redis
- 系统运行在 JDK 1.8
我们将一个包含多个字段的 Map
对象整体写入 Redis,其中 "list"
字段的值为一个 List
类型的对象列表:
-
写入 Redis 前:值是 Java 内存中的
List
-
从 Redis 反序列化后:值变成了
List
,由于类型信息丢失,强制转换会抛出ClassCastException
以下是相关缓存处理代码片段:
// redis序列化代码
@Slf4j
public class RedisObjectSerializer implements RedisSerializer<Object> {
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private final Converter<byte[], Object> deserializingConverter = new DeserializingConverter();
@Override
public byte[] serialize(Object obj) {
// 这个时候没有要序列化的对象出现,所以返回的字节数组应该就是一个空数组
if (obj == null) {
return EMPTY_BYTE_ARRAY;
}
// 将对象变为字节数组
byte[] data = JSON.toJSONBytes(obj, SerializerFeature.WriteClassName);
return data;
}
@Override
public Object deserialize(byte[] data) {
// 此时没有对象的内容信息
if (data == null || data.length == 0) {
return null;
}
try {
return JSON.parse(data, Feature.SupportAutoType);
} catch (Exception e) {
try {
return this.deserializingConverter.convert(data);
} catch (Exception e1) {
log.info("无法反序列化redis数据:{}", new String(data, StandardCharsets.UTF_8), e);
}
throw e;
}
}
}
//数据上游,填充数据阶段,返回map
Cache<String, Map<String, Object>> cache = GenericCacheManager
.getWriteExpireCacheByName("queryData", 200, 120);
String cacheKey = "cache-key";
String redisKey = "redisKey";
Map<String, Object> finalResult = cache.get(cacheKey, method -> {
Map<String, Object> redisCache = (Map<String, Object>) redisTemplate.opsForValue().get(redisKey);
if (null != redisCache) {
return redisCache;
}
Map<String, Object> map = Maps.newHashMap();
List<NewLiveMatchVO> list = queryListData();
map.put("list", list);
Map<Integer, List<NewTournamentVO>> tCollect = list.stream().collect(Collectors.groupingBy(NewTournamentVO::getSportId));
map.put("tournament", tCollect);
redisTemplate.opsForValue().set(redisKey, map, 150, TimeUnit.SECONDS);
return map;
});
// 获取缓存数据,强制类型转换(潜在风险点)
List<NewLiveMatchVO> matchList = (List<NewLiveMatchVO>) finalResult.get("list");
// 后续处理
List> process = processResult(matchList);
private List processResult(List<NewLiveMatchVO> list) {
List items = new ArrayList();
if (CollectionUtils.isEmpty(list)) {
return items;
}
list = list.stream().distinct().collect(Collectors.toList());
// 在这里报错 java.lang.ClassCastException: java.util.ArrayList cannot be cast to com.qiutx.product.model.vo.NewLiveMatchVO
list.forEach(e -> {
List item = new ArrayList();
items.add(item);
});
return items;
}
根本原因
Fastjson 在序列化整个 Map 的时候,如果 Map 内的值(例如 List)没有显式类型信息(@type),它就不会自动对 List 内的泛型对象添加类型。
所以即使你用了 SerializerFeature.WriteClassName,最终只记录了 Map 和 Map 的 key 是 "list",value 是个 List,而 NewLiveMatchVO 的类型信息丢失了。
反序列化回来就是这个样子:
Map<String, Object> finalResult = redisTemplate.opsForValue().get("xxx");
// 此时 finalResult.get("list") 实际是 List(不是 List)
去强转 List 就会炸:
(List<NewLiveMatchVO>) finalResult.get("list"); // ❌ ClassCastException
处理办法有很多,比如:只存 JSON 字符串、使用泛型反序列化(不推荐 Map),使用专门的 VO 包装类