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