JSON反序列化Long變Integer或Double的問題及解決
一、背景
工作中可能會遇到對 Map<String,Object> 進行 JSON 序列化,其中值中包含 Long 類型的數據,反序列化後強轉 Long 時報類型轉換異常的問題。
本文簡單探討下該問題,並給出解決方案,如果你想直接看建議,直接翻到第三部分即可。
二、研究
本文主要以 jackson、 gson、fastjson 三個庫為例,版本分別如下:
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.13.0</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.0</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency> <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.8</version> </dependency>
代碼示例
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<String, Object> 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<String, Object> fastMap = JSON.parseObject(jsonStr, new com.alibaba.fastjson.TypeReference<Map<String, Object>>() { }); printMap(fastMap); System.out.println("--- gson -----"); Map<String, Object> gsonMap = new GsonBuilder().create() .fromJson(jsonStr, (new TypeReference<Map<String, Object>>(){}).getType() ); printMap(gsonMap); System.out.println("--- jackson -----"); ObjectMapper objectMapper = new ObjectMapper(); Map<String, Object> jacksonMap = objectMapper.readValue(jsonStr, new TypeReference<Map<String, Object>>() { }); printMap(jacksonMap); } private static void printMap(Map<String, Object> 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<String,Object> 類型,當你隻知道這些信息時,你無法得知 aLong 原始類型為 Long 。
因此不同的JSON 序列化工具給出瞭自己的默認處理行為。
當我們把 aLong 的值調整到 超過 (Integer.MAX_VALUE,Long.MAX_VALUE] 的范圍之間時,fastjson 和 jackson 可以解析為 Long 類型。
Map<String, Object> 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<String, Object> 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
通過 com.alibaba.fastjson.parser.deserializer.MapDeserializer#parseMap 對 Map 類型進行解析。
由於 Map<String, Object>的 valueType 類型為 Object,因此對 aFloat 使用 JavaObjectDeserializer 反序列化器進行解析。
跟進 lexer.decimalValue 看下:
最終通過 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<String, Object> 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<String, Object> fastMap = JSON.parseObject(jsonStr, new com.alibaba.fastjson.TypeReference<Map<String, Object>>() { }); printMap(fastMap); } private static void printMap(Map<String, Object> 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<String, Object> jacksonMap = objectMapper.readValue(jsonStr, new TypeReference<Map<String, Object>>() { }); 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<String,Object> 序列化
強烈建議不要怕麻煩,直接定義 POJO 類。
不僅不受 JSON 框架的約束,而且對方解析時也非常明確,不容易出錯。
如工作中在發送MQ 消息時很多人圖方便,不想定義POJO 對象,因為這樣通常需要打包比較麻煩,就將要傳輸給其他系統的數據定義為 Map 類型,下遊再根據 key 去解析,這是一個非常不好的習慣。
很容易造成上下遊類型不一致,造成更換 JSON 反序列化工具時出現故障。
因此發送 MQ 消息時,最好給出相應的 POJO 類。
實際工作中,還遇到有同學將 Map<String,Object> 使用 JSON 序列化的方式存儲到 Redis 中,然後反序列化後,將原本 Long 類型的值,強轉為 Long 導致線上出現BUG(前面講到,這種情況下使用 fastjson 時,如果值小於整數最大值,反序列化為 Integer 類型,強轉必然會報錯)。
3.2 反序列化自定義類
如果上遊序列化是 Map<String,Object>, 如果類型核實清楚,我們依然可以自定義 POJO 類來反序列化。
@lombok.Data public class Data { private Float aFloat; private Integer aInteger; }
Map<String, Object> 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
https://www.javainuse.com/pojo
3.3 其他
可能網上還會有其他解決方案,比如自定義序列化和反序列化器。
我個人不太建議這麼做,因為這樣不夠通用,跨系統使用不太方便。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 如何將Java對象轉換為JSON實例詳解
- java比較兩個json文件的差異及說明
- 給JavaBean賦默認值並且轉Json字符串的實例
- JSON序列化導致Long類型被搞成Integer的坑及解決
- 解決json串和實體類字段不一致的問題