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 包装类

May 1, 2025 - 09:14
 0
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<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 包装类