Jackson反序列化后Long类型变为Integer类型
现象
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.18.1</version>
</dependency>
Redis缓存数据,使用GenericJackson2JsonRedisSerializer
作为value和hash value的序列化器。
使用如下方式存储哈希对象:
// 不使用任何查询条件,从数据库中获取所有博文信息
List<Article> articles = articleMapper.selectList(null);
Map<String, Long> viewCountMap = articles.stream()
.collect(Collectors.toMap(article -> article.getId().toString(), Article::getViewCount));
// 将id、浏览量存入redis
redisTemplate.opsForHash().putAll("article:viewCount", viewCountMap);
当需要从中取出数据时,如果viewCountMap的Long
类型的value数值大小可以被Integer
所存储,反序列化的数据会变成Integer
类型:
// 从redis读出浏览量,需要先指定泛型,如果直接调用到entries只能得到Object类型
BoundHashOperations<String, String, Long> boundHashOps = redisTemplate.boundHashOps("article:viewCount");
Map<String, Long> viewCountMap = boundHashOps.entries();
assert viewCountMap != null;
List<Article> articleList = viewCountMap.entrySet()
.stream()
.map(entry -> new Article(Long.valueOf(entry.getKey()),
entry.getValue()))
.toList();
呈现出来的现象就是:对Map进行 JSON 序列化,其中值中包含Long
类型的数据,反序列化后强转Long
时报了类型转换异常。
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader ‘bootstrap’)
分析
这是由反序列化机制引起的,因此需要聚焦于序列化器,此处使用的是Jackson作为序列化器,通过分析源码,可知反序列化的处理流程:
ParserBase
中的_parseNumericValue()
方法就是用于解析数值字符串,并转换为Java支持的数值类型,优先选择最合适的数值类型,以提高效率和准确性:
protected void _parseNumericValue(int expType) throws IOException
{
...
// 整数解析
if (_currToken == JsonToken.VALUE_NUMBER_INT) {
final int len = _intLength;
// 整数长度 <= 9,可以安全地转为int类型
if (len <= 9) {
_numberInt = _textBuffer.contentsAsInt(_numberNegative);
_numTypesValid = NR_INT;
return;
}
// 长度 <= 18,需要细分
if (len <= 18) {
long l = _textBuffer.contentsAsLong(_numberNegative);
// 长度为10的情况,在int类型范围内转换为int类型
if (len == 10) {
if (_numberNegative) {
if (l >= MIN_INT_L) {
_numberInt = (int) l;
_numTypesValid = NR_INT;
return;
}
} else {
if (l <= MAX_INT_L) {
_numberInt = (int) l;
_numTypesValid = NR_INT;
return;
}
}
}
// 使用long类型
_numberLong = l;
_numTypesValid = NR_LONG;
return;
}
// 更复杂的整数解析
if (len == 19) {
char[] buf = _textBuffer.getTextBuffer();
int offset = _textBuffer.getTextOffset();
if (_numberNegative) {
++offset;
}
if (NumberInput.inLongRange(buf, offset, len, _numberNegative)) {
_numberLong = NumberInput.parseLong19(buf, offset, _numberNegative);
_numTypesValid = NR_LONG;
return;
}
}
_parseSlowInt(expType);
return;
}
// 浮点数解析
if (_currToken == JsonToken.VALUE_NUMBER_FLOAT) {
_parseSlowFloat(expType);
return;
}
_reportError("Current token (%s) not numeric, can not use numeric value accessors", _currToken);
}
解决方式
从redis中取出hash对象时,将数值的类型限定为公共父类Number
,并在转换时转换为Long
类型。
// 从redis读出浏览量,需要先指定泛型,如果直接调用到entries只能得到Object类型
BoundHashOperations<String, String, Number> boundHashOps = redisTemplate.boundHashOps("article:viewCount");
Map<String, Number> viewCountMap = boundHashOps.entries();
assert viewCountMap != null;
List<Article> articleList = viewCountMap.entrySet()
.stream()
.map(entry -> new Article(Long.valueOf(entry.getKey()),
entry.getValue().longValue()))
.toList();