3. Json序列化框架对比与最佳实践推荐

一灰灰blog开源JsonJson约 4124 字大约 14 分钟

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

https://mvnrepository.com/open-source/json-librariesopen in new window

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-alsoopen in new window

两种解决办法:

// 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数据类型fastjsongsonjackson
浮点数BigDecimaldoubledouble
整数int/longdoubleint/long
对象JSONObjectLinkedTreeMapLinkedHashMap
数组JSONArrayArrayListArrayList
nullnullnullnull
输出MapHashMapLinkedTreeMapLinkedHashMap

如果希望三种框架保持一致,主要需要针对以下几个点:

  • 浮点数 -》 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-floatsopen in new window

对于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下划线的风格,他们两个之间的交互通常会面临这个问题

FastJsonGsonJackson
默认支持智能转换,也可以通过@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框架不一致,导致结果不同的场景
说明实践策略fastjsongsonjackson
Java Bean实现Serializable接口---
Java Beanget/set方法,无参构造函数---
key为null原则上不建议出现这种场景;如出现也不希望抛异常--objectMapper.getSerializerProvider().setNullKeySerializer
循环引用源头上避免这种场景本身兼容抛异常抛异常
key非String输出Json串的key转StringJSONObject.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);
反序列化默认值浮点数转doubleJSONObject.parseObject(str, Map.class,JSON.DEFAULT_PARSER_FEATURE & ~Feature.UseBigDecimal.getMask())无需适配无需适配
反序列化默认值整数转int/long无需适配自定义JsonDeserializer,见上文无需适配
反序列化默认值对象转mapJSON.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()
Loading...