JSON反序列化Long变Integer或Double的问题及解决

一、背景

工作中可能会遇到对 Map 进行 JSON 序列化,其中值中包含 Long 类型的数据,反序列化后强转 Long 时报类型转换异常的问题。

本文简单探讨下该问题,并给出解决方案,如果你想直接看建议,直接翻到第三部分即可。

二、研究

本文主要以 jackson、 gson、fastjson 三个库为例,版本分别如下:

   
        
            com.fasterxml.jackson.core
            jackson-core
            2.13.0
        
        
        
            com.fasterxml.jackson.core
            jackson-databind
            2.13.0
        
        
        
            com.alibaba
            fastjson
            1.2.78
        
        
        
            com.google.code.gson
            gson
            2.8.8
        
   

代码示例

package json;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.GsonBuilder;
import java.util.HashMap;
import java.util.Map;
public class ObjectDemo {
    public static void main(String[] args) throws JsonProcessingException {
        Map dataMap = new HashMap<>(2);
        dataMap.put("aInteger", 1);
        dataMap.put("aLong", 2L);
        String jsonStr = JSON.toJSONString(dataMap);
        System.out.println(jsonStr);
        // fastjson
        System.out.println("--- fastjson -----");
        Map fastMap = JSON.parseObject(jsonStr, new com.alibaba.fastjson.TypeReference>() {
        });
        printMap(fastMap);
        System.out.println("--- gson -----");
        Map gsonMap = new GsonBuilder().create()
                .fromJson(jsonStr, (new TypeReference>(){}).getType() );
        printMap(gsonMap);
        System.out.println("--- jackson -----");
        ObjectMapper objectMapper = new ObjectMapper();
        Map jacksonMap = objectMapper.readValue(jsonStr, new TypeReference>() {
        });
        printMap(jacksonMap);
    }
    private static void printMap(Map map) {
        map.forEach((key, value) -> {
            System.out.println("key:" + key + ",value=" + value + ",valueClass=" + value.getClass());
        });
    }
}

运行结果:

{"aInteger":1,"aLong":2}
--- fastjson -----
key:aLong,value=2,valueClass=class java.lang.Integer
key:aInteger,value=1,valueClass=class java.lang.Integer
--- gson -----
key:aInteger,value=1.0,valueClass=class java.lang.Double
key:aLong,value=2.0,valueClass=class java.lang.Double
--- jackson -----
key:aInteger,value=1,valueClass=class java.lang.Integer
key:aLong,value=2,valueClass=class java.lang.Integer

aLong 虽然原始类型为 Long 但是 fastjson 和 jackson 中被反序列化为 Integer 类型,gson 中被映射为 Double 类型。

我们观察序列化后的 json 字符串:

{"aInteger":1,"aLong":2}

会发现其实 JSON 中并没有包含类型信息,而反序列化的类型为 Map.class 或者 Map 类型,当你只知道这些信息时,你无法得知 aLong 原始类型为 Long 。

因此不同的JSON 序列化工具给出了自己的默认处理行为。

当我们把 aLong 的值调整到 超过 (Integer.MAX_VALUE,Long.MAX_VALUE] 的范围之间时,fastjson 和 jackson 可以解析为 Long 类型。

 Map dataMap = new HashMap<>(2);
        dataMap.put("aInteger", 1);
        dataMap.put("aLong", Long.MAX_VALUE);

输出的结果:

{"aInteger":1,"aLong":9223372036854775807}
--- fastjson -----
key:aLong,value=9223372036854775807,valueClass=class java.lang.Long
key:aInteger,value=1,valueClass=class java.lang.Integer
--- gson -----
key:aInteger,value=1.0,valueClass=class java.lang.Double
key:aLong,value=9.223372036854776E18,valueClass=class java.lang.Double
--- jackson -----
key:aInteger,value=1,valueClass=class java.lang.Integer
key:aLong,value=9223372036854775807,valueClass=class java.lang.Long

我们大致了解到, fastjson 和 jackson 默认情况下整数类型优先选取 Integer ,超过 Integer 范围再选择 Long ,以此类推。

而当我们放入 Float 类型时,结果又有差异:

   Map dataMap = new HashMap<>(2);
        dataMap.put("aInteger", 1);
        dataMap.put("aFLoat", 0.1F);

运行结果:

{"aInteger":1,"aFLoat":0.1}
--- fastjson -----
key:aInteger,value=1,valueClass=class java.lang.Integer
key:aFLoat,value=0.1,valueClass=class java.math.BigDecimal
--- gson -----
key:aInteger,value=1.0,valueClass=class java.lang.Double
key:aFLoat,value=0.1,valueClass=class java.lang.Double
--- jackson -----
key:aInteger,value=1,valueClass=class java.lang.Integer
key:aFLoat,value=0.1,valueClass=class java.lang.Double

fastjson 中 Float 被解析为 BigDecimal, gson 和 jackson 中被解析为 Double 类型。

具体底层如何处理,大家可以对每个框架的反序列方法单步跟进去即可得到答案。

这里以 fastjson 为例,简单调试下:

fastjson 底通过 com.alibaba.fastjson.parser.ParserConfig#getDeserializer 方法获取当前类型的反序列化器为 MapDeserializer

执行其反序列化方法:

com.alibaba.fastjson.parser.deserializer.MapDeserializer#deserialze

JSON反序列化Long变Integer或Double的问题及解决_第1张图片

通过 com.alibaba.fastjson.parser.deserializer.MapDeserializer#parseMap 对 Map 类型进行解析。

JSON反序列化Long变Integer或Double的问题及解决_第2张图片

由于 Map的 valueType 类型为 Object,因此对 aFloat 使用 JavaObjectDeserializer 反序列化器进行解析。

JSON反序列化Long变Integer或Double的问题及解决_第3张图片

跟进 lexer.decimalValue 看下:

JSON反序列化Long变Integer或Double的问题及解决_第4张图片

最终通过 com.alibaba.fastjson.parser.JSONScanner#decimalValue 将 aFloat 解析为 BigDecimal 类型。

三、如何解决

3.0 将类型写入 JSON 字符串中

如果我们能将原始类型写入到 JSON 字符串中,那么反序列化时自然就可以复原原始的类型。

在 fastjson 中可以使用 SerializerFeature.WriteClassName

package json;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import java.util.HashMap;
import java.util.Map;
public class JsonDemo {
    public static void main(String[] args) {
        Map dataMap = new HashMap<>(2);
        dataMap.put("aInteger", 1);
        dataMap.put("aLong", 2L);
        dataMap.put("aFloat", 3F);
        String jsonStr = JSON.toJSONString(dataMap, SerializerFeature.WriteClassName);
        System.out.println(jsonStr);
        // fastjson
        System.out.println("--- fastjson -----");
        Map fastMap = JSON.parseObject(jsonStr, new com.alibaba.fastjson.TypeReference>() {
        });
        printMap(fastMap);
    }
    private static void printMap(Map map) {
        map.forEach((key, value) -> {
            System.out.println("key:" + key + ",value=" + value + ",valueClass=" + value.getClass());
        });
    }
}

打印的结果

{"@type":"java.util.HashMap","aFloat":3.0F,"aInteger":1,"aLong":2L}
--- fastjson -----
key:aLong,value=2,valueClass=class java.lang.Long
key:aFloat,value=3.0,valueClass=class java.lang.Float
key:aInteger,value=1,valueClass=class java.lang.Integer

虽然,这种方法可以解决问题,但是这也通常要求序列化和反序列化使用同一个 JSON 工具

比如上面的 {"@type":"java.util.HashMap","aFloat":3.0F,"aInteger":1,"aLong":2L} 直接使用 jackson 进行反序列化会报错:

 System.out.println("--- jackson -----");
        ObjectMapper objectMapper = new ObjectMapper();
        Map jacksonMap = objectMapper.readValue(jsonStr, new TypeReference>() {
        });
        printMap(jacksonMap);

报错内容:

--- jackson -----
Exception in thread "main" com.fasterxml.jackson.core.JsonParseException: Unexpected character ('F' (code 70)): was expecting comma to separate Object entries
 at [Source: (String)"{"@type":"java.util.HashMap","aFloat":3.0F,"aInteger":1,"aLong":2L}"; line: 1, column: 43]
    at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2391)
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:735)
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportUnexpectedChar(ParserMinimalBase.java:659)

3.1 提供 POJO 类,慎对 Map 序列化

强烈建议不要怕麻烦,直接定义 POJO 类。

不仅不受 JSON 框架的约束,而且对方解析时也非常明确,不容易出错。

如工作中在发送MQ 消息时很多人图方便,不想定义POJO 对象,因为这样通常需要打包比较麻烦,就将要传输给其他系统的数据定义为 Map 类型,下游再根据 key 去解析,这是一个非常不好的习惯。

很容易造成上下游类型不一致,造成更换 JSON 反序列化工具时出现故障。

因此发送 MQ 消息时,最好给出相应的 POJO 类。

实际工作中,还遇到有同学将 Map 使用 JSON 序列化的方式存储到 Redis 中,然后反序列化后,将原本 Long 类型的值,强转为 Long 导致线上出现BUG(前面讲到,这种情况下使用 fastjson 时,如果值小于整数最大值,反序列化为 Integer 类型,强转必然会报错)。

3.2 反序列化自定义类

如果上游序列化是 Map, 如果类型核实清楚,我们依然可以自定义 POJO 类来反序列化。

@lombok.Data
public class Data {
    private Float aFloat;
    private Integer aInteger;
}
  Map dataMap = new HashMap<>(2);
        dataMap.put("aInteger", 1);
        dataMap.put("aFLoat", 0.1F);
        String jsonStr = JSON.toJSONString(dataMap);
        Data data = JSON.parseObject(jsonStr, Data.class);
        System.out.println(data);

输出结果:

Data(aFloat=0.1, aInteger=1)

可能有些同学会觉得定义 POJO 类很麻烦,其实我们可以使用 IDEA 插件或者在线工具实现 JSON 字符串生成 POJO 类。

如 Json2Pojo IDEA 插件

和一些在线生成工具:

https://json2csharp.com/json-to-pojo

JSON反序列化Long变Integer或Double的问题及解决_第5张图片

https://www.javainuse.com/pojo

JSON反序列化Long变Integer或Double的问题及解决_第6张图片

3.3 其他

可能网上还会有其他解决方案,比如自定义序列化和反序列化器。

我个人不太建议这么做,因为这样不够通用,跨系统使用不太方便。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

你可能感兴趣的:(JSON反序列化Long变Integer或Double的问题及解决)