JSON 标准中 Map 的 key 必须是字符串类型
问题产生的背景:
一个计算分数的API。发生一个类型转换异常,并且是在第一次查询时会产生这个类型转换异常,后面如果再调用就不会发生这个异常。
问题代码:
红色的部分会产生类型转换异常:
TypeScript
public class IndexSelectScore extends BaseScore{
@Autowired
public IndexSelectScore(RedisCache redisCache, AppEduProjectStandardMapper mapper,
ObjectMapper objectMapper, RedissonClient redissonClient) {
super(redisCache, mapper, objectMapper, redissonClient);
}
@Override
public BigDecimal getScore(ExamScoreDto dto, EduExamProject examProject) {
// 项目评分标准id
String projectStandardId = parsePerformance(dto.getPerformance(), String.class);
Map<Object, Object> projectStandard = getProjectStandard(dto);
if (projectStandard.containsKey(projectStandardId)) {
return new BigDecimal(projectStandard.get(projectStandardId).toString());
}
return new BigDecimal(0);
}
/**
* @return
*/
@Override
public GradeInputType getType() {
return GradeInputType.INDEX_SELECT;
}
/**
* 构建成绩标准map
*
* @param list
* @return
*/
@Override
protected Map<Object, Object> buildStandardMap(List<EduProjectStandard> list) {
TreeMap<Object, Object> treeMap = new TreeMap<>();
list.forEach(standard -> treeMap.put(standard.getId(), standard.getScore()));
return treeMap;
}
}异常详情:
Java
请求地址'/app/standard/getScore',发生未知异常.
java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
at java.lang.String.compareTo(String.java:111)
at java.util.TreeMap.getEntry(TreeMap.java:352)
at java.util.TreeMap.containsKey(TreeMap.java:232)
at com.ruoyi.app.handler.score.IndexSelectScore.getScore(IndexSelectScore.java:39)
at com.ruoyi.app.service.impl.AppEduProjectStandardServiceImpl.getScore(AppEduProjectStandardServiceImpl.java:183)
at com.ruoyi.app.service.impl.AppEduProjectStandardServiceImpl$$FastClassBySpringCGLIB$$cbb2cdbd.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793)问题描述:
在计算分数时如果Redis中没有该标准会从数据库中读取标准,然后进行分数计算。在Redis中没有分数时进行计算分数(也就是从数据库中读取时会产生这个问题),但是如果redis中有这个数据再从redis中读取时这个异常就不会发生。
为什么会产生这个问题?:
在数据库中读取回来的是Long类型,从redis中读取出来的是String类型。所以会发生类型转换异常。
问题的核心:
在缓存代码中进行了序列化,objectMapper.writeValueAsString(map) 序列化后 Redis 中的键会变成 String类型。
代码如下:
TypeScript
private void saveToRedis(String key, Map<Object, Object> map) {
try {
// 将Map序列化为JSON字符串
String jsonStr = objectMapper.writeValueAsString(map);
redisCache.setCacheObject(key, jsonStr, Constants.PROJECT_STANDARD_EXPIRE_TIME, TimeUnit.MINUTES);
} catch (JsonProcessingException e) {
log.error("项目评分标准序列化失败:{}", e);
throw new ServiceException("项目评分标准序列化失败");
}
}在 JSON 格式中(RFC 8259),对象(也就是 Java 中的 Map)的 key 必须是字符串类型。例如:
JSON
{
"1001": "张三",
"1002": "李四"
}即使在 Java 中写的是:
Java
Map<Long, String> map = new HashMap<>();
map.put(1001L, "张三");
map.put(1002L, "李四");当用 Jackson(ObjectMapper)序列化为 JSON 字符串时,它会被转换成:
JSON
{
"1001": "张三",
"1002": "李四"
}注意看 "1001" 是一个 字符串形式的 key,而不是数字或 Long 类型。
所以从 Redis 取出来再反序列化回来时:
调用类似这样的代码:
Java
String jsonStr = redisCache.getCacheObject(key);
Map<String, Object> map = objectMapper.readValue(jsonStr, new TypeReference<>() {});结果就是:Redis 里存储的是 JSON 字符串,反序列化出来的 Map 的 key 就只能是 String 类型。
这就是为什么第一次从数据库读取时用的是 Long 或者 .toString() 得到的 String,但一旦经过 Redis 的序列化/反序列化之后,所有的 key 都变成了 String 类型 —— 这是 JSON 协议的天然限制。
解决方案:
使用 String 类型作为 Map 的 key
Java
@Override
protected Map<String, Object> buildStandardMap(List<EduProjectStandard> list) {
TreeMap<String, Object> treeMap = new TreeMap<>();
list.forEach(standard -> treeMap.put(standard.getId().toString(), standard.getScore()));
return treeMap;
}总结:
| 问题 | 原因 |
|---|---|
| Redis 中的 key 变成了 String 类型 | 因为使用了 ObjectMapper 把 Map 序列化成 JSON,而 JSON 要求 key 是字符串 |
| 第一次正常,第二次报错 | 因为第一次是从数据库来的数据,key 是 Long;第二次从 Redis 读取后变成 String,导致类型不一致 |