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作为序列化器,通过分析源码,可知反序列化的处理流程:

image-20250207120318822

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();