210715-Json序列化框架对比与最佳实践推荐

文章目录
  1. I. 推荐规范
    1. 1. Java Bean实现Serializable接口
    2. 2. 忽略字段
    3. 3. 不要用Map/List接收json串
  2. II. 不同框架的差异性
    1. 1. json字段映射缺失场景
    2. 2. 字段没有get/set方法
    3. 3. value为null时,序列化时是否需要输出
    4. 4. 默认值
    5. 5. key非String类型
    6. 6. 类型不匹配
    7. 7. 未知属性
    8. 8. 循环引用
    9. 9. 驼峰与下划线
    10. 10. JsonObject,JsonArray
    11. 11. 泛型
  3. III. 小结
  4. II. 其他
    1. 1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
    2. 2. 声明
    3. 3. 扫描关注

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

https://mvnrepository.com/open-source/json-libraries

json协议虽然是一致的,但是不同的框架对json的序列化支持却不尽相同,那么在项目中如何使用这些框架,怎样的使用才算优雅呢?

Spring本身提供了比较多这种case,比如RestTemplate, RedisTemplate,可以让底层的redis\http依赖包无缝切换;因此我们在使用序列化框架的时,也应该尽量向它靠齐

以下为我认为在使用json序列化时,比较好的习惯

I. 推荐规范

1. Java Bean实现Serializable接口

遵循jdk的规范,如果一个Java Bean会被序列化(如对外提供VO/DTO对象)、持久化(如数据库实体Entity),建议实现Serializable接口,并持有一个serialVersionUID静态成员

1
2
3
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注解
1
2
3
4
5
6
7
8
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

两种解决办法:

1
2
3
4
5
6
7
8
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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串如下

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
// 反序列化时,找不到属性时,忽略字段
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

2. 字段没有get/set方法

若某个private字段没有get/set方法时,这个字段在序列化与反序列化时,表现不一致(public修饰话都可以序列化)

  • gson: 可以序列化
  • fastjson/jackson: 忽略这个字段

对于jackson,如果希望序列化一个没有get/set方法的属性时,如下设置

1
2
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串

1
2
3
{
"empty": null
}

通常来讲,推荐忽略null,对此jackson的设置如下

1
2
// json串只包含非null的字段
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

如果null也希望输出(比如Swagger接口文档,需要把所有的key都捞出来),如下设置

fastjson配置如下:

1
2
// 输出hvalue为null的字段
return JSONObject.toJSONString(obj, SerializerFeature.WriteMapNullValue);

gson配置如下

1
2
3
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额外配置如下

1
2
3
4
// 禁用浮点数转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,只能自己来适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

1
2
3
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 输出

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
2
3
4
5
6
7
// 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的配置如下

1
2
// 反序列化时,找不到属性时,忽略字段
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

8. 循环引用

对于循环引用序列化时,不同的框架处理策略也不一致

1
2
3
4
5
6
7
8
@Data
public class SelfRefBean implements Serializable {
private static final long serialVersionUID = -2808787760792080759L;

private String name;

private SelfRefBean bean;
}

输出json串如下

1
2
3
4
5
6
7
8
// FastJson
{"bean":{"$ref":"@"},"name":"yh"}

// Gson
{"name":"yh"}

// Jackson 抛异常
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle

除了上面这种自引用的case,更常见的是另外一种循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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;
}

再次序列化,表现如下

1
2
3
4
5
6
7
8
// 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 驼峰转下换线

1
2
3
4
5
6
7
8
9
10
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 实现驼峰与下换线互转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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实现驼峰与下划线的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 驼峰转下换线
*
* @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

1
2
3
4
5
6
7
public static JSONObject toObj(String str) {
return JSONObject.parseObject(str);
}

public static JSONArray toAry(String str) {
return JSONArray.parseArray(str);
}

Gson

1
2
3
4
5
6
7
public static JsonObject toObj(String str) {
return JsonParser.parseString(str).getAsJsonObject();
}

public static JsonArray toAry(String str) {
return JsonParser.parseString(str).getAsJsonArray();
}

Jackson

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
11
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
<GenericBean>() {}.getType()
new com.google.gson.reflect.TypeToken<br /><GenericBean<Map>>() {}.getType() new com.fasterxml.jackson.core.type.TypeReference<GenericBean<Map>>() {}.getType()

II. 其他

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

QrCode

# Json

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×