3. Json序列化框架对比与最佳实践推荐
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
静态成员
public class SimpleBean implements Serializable {
private static final long serialVersionUID = -9111747337710917591L;
}
why?
- 声明为Serializable接口的对象,可以被序列化,jdk原生支持;一般来讲所有的序列化框架都认这个;如果一个对象没有实现这个接口,则不能保证所有的序列化框架都能正常序列化了
- 实现Serializable接口的,务必不要忘了初始化
serialVersionUID
(直接通过idea自动生成即可)- idea设置自动生成提示步骤:
settings -> inspections -> Serializable class without serialVersionUID
勾选
2. 忽略字段
若实体中,某些字段不希望被序列化时,各序列化框架都有自己的支持方式,如:
- FastJson,使用JSONField注解
- Gson,使用Expose注解
- Jackson,使用JsonIgnore注解
public class SimpleBean implements Serializable {
private static final long serialVersionUID = -9111747337710917591L;
// jackson 序列化时,如果 transient 关键字,也有 getter/setter方法,那么也会被序列化出来
@JsonIgnore
@JSONField(serialize = false, deserialize = false)
@Expose(serialize = false, deserialize = false)
private transient SimpleBean self;
}
这里强烈推荐使用jdk原生的关键字transient
来修饰不希望被反序列化的成员
- 优点:通用性更强
重点注意
在使用jackson序列化框架时,成员变量如果有get方法,即便它被transient
关键字修饰,输出json串的时候,也不会忽略它
说明链接: https://stackoverflow.com/questions/21745593/why-jackson-is-serializing-transient-member-also
两种解决办法:
// 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));
虽然jackson默认对transient
关键字适配不友好,但是依然推荐使用这个关键字,然后添加上面的配置,这样替换json框架的时候,不需要修改源码
3. 不要用Map/List接收json串
Java作为强类型语言在项目维护上有很高的优势,接收json串,推荐映射为对应的Java Bean,尽量不要用Map/List容器来接收,不然参数类型可能导致各种问题,可以看下面的默认值那一块说明
II. 不同框架的差异性
接下来将重点关注下三个框架在我们日常使用场景下的区别,定义一个Java Bean
@Data
@Accessors(chain = true)
public class SimpleBean implements Serializable {
private static final long serialVersionUID = -9111747337710917591L;
private Integer userId;
private String userName;
private double userMoney;
private List<String> userSkills;
private Map<String, Object> extra;
private String empty;
// jackson 序列化时,如果 transient 关键字,也有 getter/setter方法,那么也会被序列化出来
@JsonIgnore
@JSONField(serialize = false, deserialize = false)
@Expose(serialize = false, deserialize = false)
private transient SimpleBean self;
private String hello = "你好";
public SimpleBean() {
this.self = this;
}
}
1. json字段映射缺失场景
如果json字符串中,存在一个key,在定义的bean对象不存在时,上面三种序列化框架的表现形式也不一样
json串如下
{"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希望忽略异常,需要如下配置
// 反序列化时,找不到属性时,忽略字段
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
2. 字段没有get/set方法
若某个private字段没有get/set方法时,这个字段在序列化与反序列化时,表现不一致(public修饰话都可以序列化)
- gson: 可以序列化
- fastjson/jackson: 忽略这个字段
对于jackson,如果希望序列化一个没有get/set
方法的属性时,如下设置
objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY));
fastjson,貌似没有相关的方法
注意
- 建议对Java bean的字段添加get/set方法
- 若有
getXxx()
但是又没有属性xxx
,会发现在序列化之后会多一个xxx
3. value为null时,序列化时是否需要输出
如果java bean中某个成员为null,默认表现如下
- fastjson/gson: 忽略这个字段
- jackson: 保存这个字段,只是value为null
如jackson对应的json串
{
"empty": null
}
通常来讲,推荐忽略null,对此jackson的设置如下
// json串只包含非null的字段
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
如果null也希望输出(比如Swagger接口文档,需要把所有的key都捞出来),如下设置
fastjson配置如下:
// 输出hvalue为null的字段
return JSONObject.toJSONString(obj, SerializerFeature.WriteMapNullValue);
gson配置如下
Gson gson = new GsonBuilder().serializeNulls().create();
// 输出value为null的字段
gson.toJson(map)
说明
- 一般来讲,在序列化的时候,推荐忽略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额外配置如下
// 禁用浮点数转BigDecimal
int features = JSON.DEFAULT_PARSER_FEATURE & ~Feature.UseBigDecimal.getMask();
// 对象转Map,而不是JSONObject
features = features | Feature.CustomMapDeserializer.getMask();
- 数组转List而不是JSONArray,这个配置暂时未找到,可考虑自定义
ObjectDeserializer
来支持 - Object转有序Map的配置也未找到,
gson:
https://stackoverflow.com/questions/15507997/how-to-prevent-gson-from-expressing-integers-as-floats
对于gson而言,也没有配置可以直接设置整数转int/long而不是double,只能自己来适配
public class GsonNumberFixDeserializer implements JsonDeserializer<Map> {
@Override
public Map deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
return (Map) read(jsonElement);
}
public Object read(JsonElement in) {
if (in.isJsonArray()) {
List<Object> list = new ArrayList<>();
JsonArray arr = in.getAsJsonArray();
for (JsonElement anArr : arr) {
list.add(read(anArr));
}
return list;
} else if (in.isJsonObject()) {
Map<String, Object> map = new LinkedTreeMap<>();
JsonObject obj = in.getAsJsonObject();
Set<Map.Entry<String, JsonElement>> entitySet = obj.entrySet();
for (Map.Entry<String, JsonElement> entry : entitySet) {
map.put(entry.getKey(), read(entry.getValue()));
}
return map;
} else if (in.isJsonPrimitive()) {
JsonPrimitive prim = in.getAsJsonPrimitive();
if (prim.isBoolean()) {
return prim.getAsBoolean();
} else if (prim.isString()) {
return prim.getAsString();
} else if (prim.isNumber()) {
Number num = prim.getAsNumber();
if (Math.ceil(num.doubleValue()) != num.longValue()) {
return num.doubleValue();
}
if (num.doubleValue() > Integer.MAX_VALUE || num.doubleValue() < Integer.MIN_VALUE) {
return num.longValue();
}
return num.longValue();
}
}
return null;
}
}
然后注册到Gson
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(new TypeToken<Map>(){}.getType(), new GsonNumberFixDeserializer());
Gson gson = gsonBuilder.create();
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 输出
{null:"empty key", 12: "12"}
Gson输出
{"null":"empty key", "12": "12"}
Jackson直接抛异常
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的姿势如下
JSONObject.toJSONString(map,SerializerFeature.WriteNonStringKeyAsString)
对于key为null,jackson的兼容策略
// key 为null,不抛异常,改用"null"
objectMapper.getSerializerProvider().setNullKeySerializer(new JsonSerializer<Object>() {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeFieldName("null");
}
});
6. 类型不匹配
String转其他基本类型(int/long/float/double/boolean),若满足Integer.valueOf(str)
这种,则没有问题,否则抛异常
7. 未知属性
当json串中有一个key,在定义的bean中不存在,表现形式也不一样
- fastjson: 忽略这个key
- gson:忽略
- jackson: 抛异常
一般来说,忽略是比较好的处理策略,jackson的配置如下
// 反序列化时,找不到属性时,忽略字段
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
8. 循环引用
对于循环引用序列化时,不同的框架处理策略也不一致
@Data
public class SelfRefBean implements Serializable {
private static final long serialVersionUID = -2808787760792080759L;
private String name;
private SelfRefBean bean;
}
输出json串如下
// FastJson
{"bean":{"$ref":"@"},"name":"yh"}
// Gson
{"name":"yh"}
// Jackson 抛异常
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle
除了上面这种自引用的case,更常见的是另外一种循环引用
@Data
@Accessors(chain = true)
public class SelfRefBean implements Serializable {
private static final long serialVersionUID = -2808787760792080759L;
private String name;
private SelfRefBean2 bean;
}
@Data
@Accessors(chain = true)
public class SelfRefBean2 implements Serializable {
private static final long serialVersionUID = -2808787760792080759L;
private String name;
private SelfRefBean bean;
}
再次序列化,表现如下
// FastJson
{"bean":{"bean":{"$ref":".."},"name":"yhh"},"name":"yh"}
// Gson 栈溢出
Method threw 'java.lang.StackOverflowError' exception.
// Jackson 栈溢出
com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain:
从安全性来看,FastJson的处理方式是比较合适的,针对Gson/Jackson,到没有比较简单的设置方式
一般来说,如果有循环引用的场景,请忽略这个字段的序列化,推荐添加 transient
关键字
9. 驼峰与下划线
java采用驼峰命名格式,php下划线的风格,他们两个之间的交互通常会面临这个问题
FastJson | Gson | Jackson |
---|---|---|
默认支持智能转换,也可以通过@JSONField | @SerializedName | @JsonProperty |
虽然三种框架都提供了通过注解,来自定义输出json串的key的别名,但是更推荐使用全局的设置,来实现统一风格的转驼峰,转下划线
FastJson 驼峰转下换线
public static <T> String toUnderStr(T obj) {
// 驼峰转下划线
SerializeConfig serializeConfig = new SerializeConfig();
// CamelCase 常见的驼峰格式
// PascalCase 单次首字母大写驼峰
// SnakeCase 下划线
// KebabCase 中划线
serializeConfig.setPropertyNamingStrategy(PropertyNamingStrategy.SnakeCase);
return JSONObject.toJSONString(obj, serializeConfig, SerializerFeature.PrettyFormat, SerializerFeature.IgnoreNonFieldGetter);
}
Gson 实现驼峰与下换线互转
public static <T> String toUnderStr(T obj) {
GsonBuilder gsonBuilder = new GsonBuilder();
// 驼峰转下划线
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
Gson gson = gsonBuilder.create();
return gson.toJson(obj);
}
public static <T> T fromUnderStr(String str, Class<T> clz) {
GsonBuilder gsonBuilder = new GsonBuilder();
// 下划线的json串,反序列化为驼峰
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
Gson gson = gsonBuilder.create();
return gson.fromJson(str, clz);
}
Jackson实现驼峰与下划线的转换
/**
* 驼峰转下换线
*
* @param obj
* @return
*/
public static String toUnderStr(Object obj) {
ObjectMapper objectMapper = new ObjectMapper();
// 驼峰转下划线
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
// 忽略 transient 关键字修饰的字段
objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
// json串只包含非null的字段
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new UnsupportedOperationException(e);
}
}
public static <T> T fromUnderStr(String str, Class<T> clz) {
ObjectMapper objectMapper = new ObjectMapper();
// 忽略 transient 修饰的属性
objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
// 驼峰转下划线
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
// 忽略找不到的字段
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
try {
return objectMapper.readValue(str, clz);
} catch (JsonProcessingException e) {
throw new UnsupportedOperationException(e);
}
}
说明
- 对于Gson/Jackson而言,如果使用上面的驼峰转下划线的json串,那么反序列化的时候也需要使用对应的下划线转驼峰的方式
- FastJson则默认开启驼峰与下划线的互转
10. JsonObject,JsonArray
通常在java 生态中,更常见的是将Json串转为Java Bean,但某些场景也会希望直接获取JsonObject,JsonArray对象,当然是可以直接转为Map/List,使用前者的好处就是可以充分利用JsonElement的一些特性,如更安全的类型转换等
虽说三个框架的使用姿势不一样,但最终的表现差不多
FastJson
public static JSONObject toObj(String str) {
return JSONObject.parseObject(str);
}
public static JSONArray toAry(String str) {
return JSONArray.parseArray(str);
}
Gson
public static JsonObject toObj(String str) {
return JsonParser.parseString(str).getAsJsonObject();
}
public static JsonArray toAry(String str) {
return JsonParser.parseString(str).getAsJsonArray();
}
Jackson
public static JsonNode toObj(String str) {
try {
return objectMapper.readTree(str);
} catch (JsonProcessingException e) {
throw new UnsupportedOperationException(e);
}
}
上面这些没啥好说的,但是,请一定注意,不要多个json工具混用,比如Gson反序列化为JsonObject,然后又使用Jackson进行序列化,可能导致各种鬼畜的问题
简单来说,就是不要尝试对JSONObject/JSONArray
, JsonObject/JsonArray
, JsonNode
调用 jsonutil.encode
如果想输出json串,请直接调用 toString/toJSONString
,千万不要搞事情
11. 泛型
Json串,转泛型bean时,虽然各框架都有自己的TypeReference,但是底层的Type
都是一致的
FastJson
public static <T> T decode(String str, Type type) {
return JSONObject.parseObject(str, type);
}
// 使用姿势
FastjsonUtil.decode(str, new com.alibaba.fastjson.TypeReference<GenericBean<Map>>() {
}.getType());
Gson
public static <T> T decode(String str, Type type) {
return gson.fromJson(str, type);
}
// 使用姿势
GsonUtil.decode(str, new com.google.gson.reflect.TypeToken<GenericBean<Map>>() {
}.getType());
Jackson
public static <T> T decode(String str, Type type) {
try {
return objectMapper.readValue(str, objectMapper.getTypeFactory().constructType(type));
} catch (Exception e) {
throw new UnsupportedOperationException(e);
}
}
// 使用姿势
JacksonUtil.decode(str, new com.fasterxml.jackson.core.type.TypeReference<GenericBean<Map>>() {
}.getType());
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<br /><GenericBean<Map>>() {}.getType() | new com.google.gson.reflect.TypeToken<br /><GenericBean<Map>>() {}.getType() | new com.fasterxml.jackson.core.type.TypeReference<GenericBean<Map>>() {}.getType() |