Java 生态中,最最常见的json序列化工具有三个jackson, gson, fastsjon,当然我们常用的也就是这几个

json协议虽然是一致的,但是不同的框架对json的序列化支持却不尽相同,那么在项目中如何使用这些框架,怎样的使用才算优雅呢?
Spring本身提供了比较多这种case,比如RestTemplate, RedisTemplate,可以让底层的redis\http依赖包无缝切换;因此我们在使用序列化框架的时,也应该尽量向它靠齐
以下为我认为在使用json序列化时,比较好的习惯
I. 推荐规范
1. Java Bean实现Serializable接口
遵循jdk的规范,如果一个Java Bean会被序列化(如对外提供VO/DTO对象)、持久化(如数据库实体Entity),建议实现Serializable接口,并持有一个serialVersionUID静态成员
| 1 | public class SimpleBean implements Serializable { | 
why?
- 声明为Serializable接口的对象,可以被序列化,jdk原生支持;一般来讲所有的序列化框架都认这个;如果一个对象没有实现这个接口,则不能保证所有的序列化框架都能正常序列化了
- 实现Serializable接口的,务必不要忘了初始化serialVersionUID(直接通过idea自动生成即可)- idea设置自动生成提示步骤:
- settings -> inspections -> Serializable class without serialVersionUID勾选
 
2. 忽略字段
若实体中,某些字段不希望被序列化时,各序列化框架都有自己的支持方式,如:
- FastJson,使用JSONField注解
- Gson,使用Expose注解
- Jackson,使用JsonIgnore注解
| 1 | public class SimpleBean implements Serializable { | 
这里强烈推荐使用jdk原生的关键字transient来修饰不希望被反序列化的成员
- 优点:通用性更强
重点注意
在使用jackson序列化框架时,成员变量如果有get方法,即便它被transient关键字修饰,输出json串的时候,也不会忽略它
说明链接: https://stackoverflow.com/questions/21745593/why-jackson-is-serializing-transient-member-also
两种解决办法:
| 1 | // case1 | 
虽然jackson默认对transient关键字适配不友好,但是依然推荐使用这个关键字,然后添加上面的配置,这样替换json框架的时候,不需要修改源码
3. 不要用Map/List接收json串
Java作为强类型语言在项目维护上有很高的优势,接收json串,推荐映射为对应的Java Bean,尽量不要用Map/List容器来接收,不然参数类型可能导致各种问题,可以看下面的默认值那一块说明
II. 不同框架的差异性
接下来将重点关注下三个框架在我们日常使用场景下的区别,定义一个Java Bean
| 1 | @Data | 
1. json字段映射缺失场景
如果json字符串中,存在一个key,在定义的bean对象不存在时,上面三种序列化框架的表现形式也不一样
json串如下
| 1 | {"extra":{"a":"123","b":345,"c":["1","2","3"],"d":35.1},"userId":12,"userMoney":12.3,"userName":"yh","userSkills2":["1","2","3"]} | 
上面这个json中,userSkills2这个字段,和SimpleBean映射不上,如果进行反序列化,会出现下面的场景
- fastjson, gson 会忽略json字符串中未匹配的key;jackson会抛异常
若jackson希望忽略异常,需要如下配置
| 1 | // 反序列化时,找不到属性时,忽略字段 | 
2. 字段没有get/set方法
若某个private字段没有get/set方法时,这个字段在序列化与反序列化时,表现不一致(public修饰话都可以序列化)
- gson: 可以序列化
- fastjson/jackson: 忽略这个字段
对于jackson,如果希望序列化一个没有get/set方法的属性时,如下设置
| 1 | objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker() | 
fastjson,貌似没有相关的方法
注意
- 建议对Java bean的字段添加get/set方法
- 若有 getXxx()但是又没有属性xxx,会发现在序列化之后会多一个xxx
3. value为null时,序列化时是否需要输出
如果java bean中某个成员为null,默认表现如下
- fastjson/gson: 忽略这个字段
- jackson: 保存这个字段,只是value为null
如jackson对应的json串
| 1 | { | 
通常来讲,推荐忽略null,对此jackson的设置如下
| 1 | // json串只包含非null的字段 | 
如果null也希望输出(比如Swagger接口文档,需要把所有的key都捞出来),如下设置
fastjson配置如下:
| 1 | // 输出hvalue为null的字段 | 
gson配置如下
| 1 | Gson gson = new GsonBuilder().serializeNulls().create(); | 
说明
- 一般来讲,在序列化的时候,推荐忽略value为null的字段
- jackson默认不会忽略,需要设置关闭
4. 默认值
将一个json串,转换为Map/List时,可以看到不同的数据类型与java数据类型的映射关系,下面是一些特殊的场景:
| json数据类型 | fastjson | gson | jackson | 
|---|---|---|---|
| 浮点数 | BigDecimal | double | double | 
| 整数 | int/long | double | int/long | 
| 对象 | JSONObject | LinkedTreeMap | LinkedHashMap | 
| 数组 | JSONArray | ArrayList | ArrayList | 
| null | null | null | null | 
| 输出Map | HashMap | LinkedTreeMap | LinkedHashMap | 
如果希望三种框架保持一致,主要需要针对以下几个点:
- 浮点数 -》 double
- 整数 -》 int/long
- 数组 -》 ArrayList
- Map -》是否有序
输出map,虽然类型不一致,一般来说问题不大,最大的区别就是gson/jackson保证了顺序,而FastJson则没有
fastjson额外配置如下
| 1 | // 禁用浮点数转BigDecimal | 
- 数组转List而不是JSONArray,这个配置暂时未找到,可考虑自定义ObjectDeserializer来支持
- Object转有序Map的配置也未找到,
gson:
https://stackoverflow.com/questions/15507997/how-to-prevent-gson-from-expressing-integers-as-floats
对于gson而言,也没有配置可以直接设置整数转int/long而不是double,只能自己来适配
| 1 | public class GsonNumberFixDeserializer implements JsonDeserializer<Map> { | 
然后注册到Gson
| 1 | GsonBuilder gsonBuilder = new GsonBuilder(); | 
jackson 就没有什么好说的了
在json字符串映射到Java的Map/List容器时,获取到的数据对象和预期的可能不一样,不同的框架处理方式不同;所以最佳的实践是:
- json字符串映射到Java bean,而不是容器
- 如果映射到容器时,取数据时,做好类型兼容,完全遵循json的规范- String:对应java的字符串
- boolean: 对应java的Boolean
- 数值:对应Java的double- 原则上建议不要直接存数值类型,对于浮点数会有精度问题,用String类型进行替换最好
- 如确实为数值,为了保证不出问题,可以多绕一圈,如
- Double.valueOf(String.valueOf(xxx)).xxxValue()
 
 
5. key非String类型
一般来说不存在key为null的情况,但是map允许key为null,所以将一个map序列化为json串的时候,就有可能出现这种场景
FastJson 输出
| 1 | {null:"empty key", 12: "12"} | 
Gson输出
| 1 | {"null":"empty key", "12": "12"} | 
Jackson直接抛异常
| 1 | Null key for a Map not allowed in JSON (use a converting NullKeySerializer?) | 
说明
- 对于FastJson而言,若key不是String,那么输出为Json串时,key上不会有双引号,这种是不满足json规范的
- gson则不管key是什么类型,都会转string
- jackson 若key为非string类型,非null,则会转String
推荐采用gson/jackson的使用姿势,key都转String,因此FastJson的姿势如下
| 1 | JSONObject.toJSONString(map,SerializerFeature.WriteNonStringKeyAsString) | 
对于key为null,jackson的兼容策略
| 1 | // key 为null,不抛异常,改用"null" | 
6. 类型不匹配
String转其他基本类型(int/long/float/double/boolean),若满足Integer.valueOf(str)这种,则没有问题,否则抛异常
7. 未知属性
当json串中有一个key,在定义的bean中不存在,表现形式也不一样
- fastjson: 忽略这个key
- gson:忽略
- jackson: 抛异常
一般来说,忽略是比较好的处理策略,jackson的配置如下
| 1 | // 反序列化时,找不到属性时,忽略字段 | 
8. 循环引用
对于循环引用序列化时,不同的框架处理策略也不一致
| 1 | @Data | 
输出json串如下
| 1 | // FastJson | 
除了上面这种自引用的case,更常见的是另外一种循环引用
| 1 | @Data | 
再次序列化,表现如下
| 1 | // FastJson | 
从安全性来看,FastJson的处理方式是比较合适的,针对Gson/Jackson,到没有比较简单的设置方式
一般来说,如果有循环引用的场景,请忽略这个字段的序列化,推荐添加 transient关键字
9. 驼峰与下划线
java采用驼峰命名格式,php下划线的风格,他们两个之间的交互通常会面临这个问题
| FastJson | Gson | Jackson | 
|---|---|---|
| 默认支持智能转换,也可以通过 @JSONField | @SerializedName | @JsonProperty | 
虽然三种框架都提供了通过注解,来自定义输出json串的key的别名,但是更推荐使用全局的设置,来实现统一风格的转驼峰,转下划线
FastJson 驼峰转下换线
| 1 | public static <T> String toUnderStr(T obj) { | 
Gson 实现驼峰与下换线互转
| 1 | public static <T> String toUnderStr(T obj) { | 
Jackson实现驼峰与下划线的转换
| 1 | /** | 
说明
- 对于Gson/Jackson而言,如果使用上面的驼峰转下划线的json串,那么反序列化的时候也需要使用对应的下划线转驼峰的方式
- FastJson则默认开启驼峰与下划线的互转
10. JsonObject,JsonArray
通常在java 生态中,更常见的是将Json串转为Java Bean,但某些场景也会希望直接获取JsonObject,JsonArray对象,当然是可以直接转为Map/List,使用前者的好处就是可以充分利用JsonElement的一些特性,如更安全的类型转换等
虽说三个框架的使用姿势不一样,但最终的表现差不多
FastJson
| 1 | public static JSONObject toObj(String str) { | 
Gson
| 1 | public static JsonObject toObj(String str) { | 
Jackson
| 1 | public static JsonNode toObj(String str) { | 
上面这些没啥好说的,但是,请一定注意,不要多个json工具混用,比如Gson反序列化为JsonObject,然后又使用Jackson进行序列化,可能导致各种鬼畜的问题
简单来说,就是不要尝试对JSONObject/JSONArray, JsonObject/JsonArray, JsonNode调用 jsonutil.encode
如果想输出json串,请直接调用 toString/toJSONString,千万不要搞事情
11. 泛型
Json串,转泛型bean时,虽然各框架都有自己的TypeReference,但是底层的Type都是一致的
FastJson
| 1 | public static <T> T decode(String str, Type type) { | 
Gson
| 1 | public static <T> T decode(String str, Type type) { | 
Jackson
| 1 | public static <T> T decode(String str, Type type) { | 
III. 小结
上面内容比较多,下面是提炼的干货
序列化
- java bean - 继承Serializable接口,持有serialVersionUID属性
- 每个需要序列化的,都需要有get/set方法
- 无参构造方法
 
- 继承
- 忽略字段- 不希望输出的属性,使用关键字transient修饰,注意jackson需要额外配置
 
- 不希望输出的属性,使用关键字
- 循环引用- 源头上避免出现这种场景,推荐直接在属性上添加 transient关键字
 
- 源头上避免出现这种场景,推荐直接在属性上添加 
- 忽略value为null的属性
- 遵循原生的json规范- 即不要用单引号替换双引号
- key都要用双引号包裹
 
- 不要出现key为null的场景
反序列化
- 默认值- 浮点型:转double,fastjson默认转为BigDecimal,需要额外处理
- 整数:转int/long- gson 默认转为double,需要额外处理
 
- 对象:  转Map- fastJson需要额外处理
 
- 数组: 转List- fastJson转成了JSONArray,需要注意
 
 
- 未知属性,忽略- json串中有一个bean未定义的属性,建议直接忽略掉
- jackson需要额外配置
 
- 泛型:- 使用Type来精准的反序列化
 
驼峰与下划线的互转
- 建议规则统一,如果输出下划线,就所有的都是下划线风格;不要出现混搭
- 不建议使用注解的别名方式来处理,直接在工具层进行统一是更好的选择,不会出现因为json框架不一致,导致结果不同的场景
| 说明 | 实践策略 | fastjson | gson | jackson | 
|---|---|---|---|---|
| Java Bean | 实现Serializable接口 | - | - | - | 
| Java Bean | get/set方法,无参构造函数 | - | - | - | 
| key为null | 原则上不建议出现这种场景;如出现也不希望抛异常 | - | - | objectMapper.getSerializerProvider().setNullKeySerializer | 
| 循环引用 | 源头上避免这种场景 | 本身兼容 | 抛异常 | 抛异常 | 
| key非String | 输出Json串的key转String | JSONObject.toJSONString (map,SerializerFeature. WriteNonStringKeyAsString) | - | - | 
| 忽略字段 | transient 关键字 | 无需适配 | 无需适配 | case1: objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);case2: objectMapper.setVisibility(objectMapper.getSerializationConfig() .getDefaultVisibilityChecker() .withFieldVisibility(JsonAutoDetect.Visibility.ANY) .withGetterVisibility(JsonAutoDetect.Visibility.NONE) .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)); | 
| 值为null | 忽略 | 无需适配 | 无需适配 | objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); | 
| 属性找不到 | 忽略 | 无需适配 | 无需适配 | objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); | 
| 反序列化默认值 | 浮点数转double | JSONObject.parseObject(str, Map.class,JSON.DEFAULT_PARSER_FEATURE & ~Feature.UseBigDecimal.getMask()) | 无需适配 | 无需适配 | 
| 反序列化默认值 | 整数转int/long | 无需适配 | 自定义JsonDeserializer,见上文 | 无需适配 | 
| 反序列化默认值 | 对象转map | JSON.DEFAULT_PARSER_FEATURE 1 Feature.CustomMapDeserializer.getMask() | 无需适配 | 无需适配 | 
| 驼峰与下划线 | 统一处理 | 反序列化自动适配,序列化见上文 | 驼峰转下划线 下划线转驼峰必须配套使用 | 驼峰转下划线 下划线转驼峰必须配套使用 | 
| 泛型 | Type是最好的选择 | new com.alibaba.fastjson.TypeReference <GenericBean | new com.google.gson.reflect.TypeToken<br /><GenericBean<Map>>() {}.getType() | new com.fasterxml.jackson.core.type.TypeReference<GenericBean<Map>>() {}.getType() | 
II. 其他
1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
2. 声明
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840
3. 扫描关注
一灰灰blog


