Java序列化与Json
java序列化与Json的三个库 如果一个运行时的数据需要被持久化,或者说需要通过网络进行通信,那么就涉及到对象的序列化问题。比如 说你在程序中写了一个sql,大概是这个样子:
1 INSERT into student (student_id,`name`) values ( 121212 ,null)
这个sql要传输到数据库,就必须进行序列化,比如说,null为什么会被识别为Null类型,而不是字符串”null”, 121212 为什么被识别为一个数字,而不是字符串。这就依赖于序列化和反序列化的约定。 序列化和反序列化在网络编程中更是无处不在。比如现在流行的前后端分离项目,后端接口向前端返回一个对 象,在spring boot框架下经常会写成这样代码:
1 2 3 4 5 6 7 8 @RestController @RequestMapping("/student") public class StudentController { @GetMapping("/random") public Student random(){ return new Student( 121212 ,"章银莱"); } }
如果不做其他设置,前端将收到这样的结果:
1 2 3 4 { "studentId": 121212 "name": "章银莱" }
那么就应该好奇,为什么返回了一个json,而不是xml,不是yaml。答案是springboot默认调用jackson,将我 们的类对象转换成了一个json字符串。
java 对序列化的原生支持
以前,java提供了一个标记接口,java.io.Serializable,任何实现了这个接口的类,都可以使用java自己 的序列化机制,实际上,因为这个接口是个标记接口,在大多数情况下,你什么都不用做。如果一个类要完整 的序列化,那么他的包含的所有字段都必须实现该序列化接口,不然就会出现异常。 java通过一个序列化序号来识别一个对象,也就是我们经常在实现了java.io.Serializable接口的类里看 到这个字段的原因:
1 private static final long serialVersionUID = 1L;
如果不提供的话,java会用某种默认生成机制生成。在反序列化时,如果序列化号对不上,反序列化会失败。 java的默认序列化策略有很多问题:
序列化的声明过于繁琐。字段必须也实现该接口,否则序列化会失败;父类必须也实现该接口,否则父 类的部分不会被序列化。有时候父类我们是改变不了的,那就只能接受无法序列化的结果。
仅适用于java平台相关语言。其他语言没法直接反序列化,或许通过一些中间件可以支持,但是在有些 复杂。在前后端分离的大趋势下,这个限制已经令人无法忍受。
对修改不太友好。假如说我们没有指定序列化号,那么默认的序列号生成策略会考虑到类的现有字段, 假如说你对类的字段进行了修改,那序列化号会发生改变。导致反序列化失败。类的改名也有一样的负 面影响。 限制众多,显然我们需要更方便、通用性、兼容性更好的序列化方式。
json or xml 序列化并不是某一个具体的行为,而是一类行为的统称。我个人认为,序列化的最终目的和最终结果,是将丰富多样的数据转换成一串二进制流,毕竟无论是存储在硬盘中以序列化,还是通过网络接口通信,最后的物理设备都只能识别和传输二进制。所以,一个靠谱的序列化方案,显然需要做到二进制层面的一致。那么就有了两种序列化的风格,一种是直接序列化为二进制流,另一种是现将纷繁复杂的数据转化为字符串,再将字符串编码为二进制流。 java.io.Serializable采用了第一种方案。但很多更流行的序列化方案都采取了第二种风格。比如最经常 使用的json和xml。xml确实有它的优势,但作为资源传递的媒介来说,一堆什么DTD,XSD的规范,可能绕几 个月也不一定绕清楚xml的所有规范,另外废话也太多,网络传输中会比json占用更多的宽带。 相比之下,json就更有优势。JavaScript原生支持,规范要求应当使用unicode,编码方式默认使用utf-8,字符 串必须用双引号,key只能是字符串,必须用双引号,等等。加上spring默认使用json作为REST的资源表述方 式,至少在java web开发领域,json几乎已经一统天下。 json相比java.io.Serializable有很多优势:
独立于java语言,可以非常便利地支持多语言。
不怕类名、类字段修改。因为只要修改对不上,这个字段的值将被直接置为null,而多余的字段将会被直 接丢弃。至少不会引发异常——其实还可以通过可选字段名的扩展来实现字段名修改前后的兼容。
序列化支持非常广泛。因为是将字段直接映射为key:value这种格式,不需要其父类、包含的所有字段 实现什么特定接口。 所以,在目前的编程环境下,已经不太常见java.io.Serializable这种东西,最多的是看到json大行其 道。 扩展阅读:数据类型和Json格式,json的标准定义
look into json 一个 example 类
1 2 3 import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.time.LocalDateTime; @Data @AllArgsConstructor @NoArgsConstructor public class Student { private String id; private String name; private Gender gender; private ZonedDateTime birthday; private Integer age; private Teacher teacher; public enum Gender { MALE, FEMALE, } }
常用的三个 java 处理 json 库
google的Gson,alibaba的fastJson,还有springBoot默认的jackson。性能如何,这里就不测试了,对于非极端 环境下,作为这种非常优秀且使用广泛的库,基本也不需要我们担心这个问题。 引入的pom如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF- 8 </project.reporting.outputEncoding> <java.version>1.8</java.version> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.source>1.8</maven.compiler.source> <jackson.version>2.9.8</jackson.version> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.55</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.5</version> </dependency>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.20</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>${jackson.version}</version> </dependency> </dependencies>
可见jackson需要引用的pom最多,这有时候也不太方便。三个库的基本用法如下:
1 2 3 4 5 6 7 8 9 10 11 Gson gson = new GsonBuilder().create(); ObjectMapper mapper = new ObjectMapper(); //fastJson都是静态方法,所以不需要实例化一个解析器 Student student = new Student("1", "刘", Student.Gender.MALE, ZonedDateTime.now(ZoneId.of("Asia/Shanghai")), 18 , teacher); String fastJsonStr = JSON.toJSONString(student); String gsonStr = gson.toJson(student); String jacksonStr = mapper.writeValueAsString(student); Student student1 = JSON.parseObject(fastJsonStr, Student.class); Student student2 = gson.fromJson(gsonStr, Student.class); Student student3 = mapper.readValue(jacksonStr, Student.class);
特殊类型会被怎么处理
1 2 日期会被怎么处理 假如我们有一个LocalDateTime的类,默认策略下,三个库对其序列化的支持如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Gson:{"date":{"year":2019,"month":4,"day":1},"time": {"hour":16,"minute":6,"second":47,"nano":247000000}} Jackson: {"year":2019,"month":"APRIL","dayOfMonth":1,"dayOfWeek":"MONDAY","dayOfYear" :91,"hour":16,"minute":6,"second":47,"monthValue":4,"nano":247000000,"chrono logy":{"id":"ISO","calendarType":"iso8601"}} FastJson:2019-04-01T16:06: 如果我们有一个ZonedDateTime类,默认策略下,三个库对其序列化支持如下: Gson:{"dateTime":{"date":{"year":2019,"month":4,"day":2},"time": {"hour":17,"minute":25,"second":25,"nano":721000000}},"offset": {"totalSeconds":28800},"zone":{"id":"Asia/Shanghai"}} JackSon: 请各位自行测试,长到令人崩溃,大概整整两页 FastJson:2019-04-02T17:25:25.721+08:00[Asia/Shanghai] 可以看到gson和jackson对于时间的记录比较完整,而fastJson就有数据的丢失,但绝绝大部分情况下, 好像也不会有什么影响,而且fastJson看起来更舒服。 然而,jackson在反序列化自己序列化的时间的时候,会出现错误,解决方法在下面。 枚举怎么处理(默认场景) 三个类十分统一,都将结果转化为:"gender":"MALE" null字段怎么处理(默认场景) Gson和fastJson:直接忽略 jackson:写入一个null字段。 到底哪一种处理方式更好。显然是见仁见智的事情了。
面对泛型
根据上面的扩展阅读,一个JSON文本是一个对象(即Map)或者数组(即Array)的序列化结果。因此,我们 在任何时候,都可以将一个json文本反序列化一个map或一个array(List, etc…)。比如下面的代码:
1 2 3 4 5 6 7 String fastJsonStr = JSON.toJSONString(student, SerializerFeature.WriteMapNullValue); String gsonStr = gson.toJson(student); String jacksonStr = mapper.writeValueAsString(student); Object object1 = JSON.parseObject(fastJsonStr, Map.class); Object object2 = gson.fromJson(gsonStr, Map.class); Object object3 = mapper.readValue(jacksonStr, Map.class);
甚至在默认情况下,比如下面的代码: 1 2 Object object2 = gson.fromJson(gsonStr, Object.class); Object object3 = mapper.readValue(jacksonStr, Object.class);
这两个类都会直接将结果转化为一个map,如果要将其转为我们需要的类,则必须给他传入一个类型信息。 如:
1 2 3 Student student1 = JSON.parseObject(fastJsonStr, Student.class); Student student2 = gson.fromJson(gsonStr, Student.class); Student student3 = mapper.readValue(jacksonStr, Student.class);
这样对简单对象是可以,但当面对泛型时,json往往无能为力,比如将List序列化后,很可能是 这样:
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 [ { "id": "1", "aaaName": "刘", "gender": "MALE", "birthday": "2019-04-03T07:23:07.623Z", "age": 18, "teacher": { "students": null } }, { "id": "2", "aaaName": "马", "gender": "MALE", "birthday": "2019-04-03T07:23:07.623Z", "age": 18, "teacher": { "students": null } }, { "id": "3", "aaaName": "张", "gender": "FEMALE", "birthday": "2019-04-03T07:23:07.623Z", "age": 18, "teacher": { "students": null } } ]
将其反序列化时,如果想使用gson.fromJson(gsonStr, List.class)就会出现问题。编译 器会提示无法获得List的类型,因为java的泛型是java5才加入的,为了兼容老代码,采取运行时 擦除类型信息的方式实现。所以List在运行时和List是一种类型,这造成了一些有意 思(也令人困惑)的现象,其中之一就是: 泛型类并没有自己独有的Class类对象。比如并不存在List.class或是 List.class,而只有List.class; 为解决这个问题,三个库都提供了自己的解决方案,主要是运行时获得Type。 Gson解决方案
1 2 3 4 Type studentListType = new TypeToken<List<Student>>() { }.getType(); List<Student> students2 = gson.fromJson(gson.toJson(students), studentListType);
1 2 3 4 5 import com.alibaba.fastjson.TypeReference List<Student> students2 = JSON.parseObject(JSON.toJSONString(students), new TypeReference<List<Student>>() {});
1 2 3 4 5 import com.fasterxml.jackson.core.type.TypeReference List<Student> students2 = mapper.readValue(mapper.writeValueAsString(students), new TypeReference<List<Student>> () {});
上述方法对于复杂泛型嵌套一样是可用的,如:
1 2 3 4 5 import lombok.Data; import java.util.HashMap; import java.util.List; @Data public class Result<T> {
1 2 3 private String result = "result"; private HashMap<String, List<T>> data = new HashMap<>(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Student student = new Student("1", "刘", Student.Gender.MALE, ZonedDateTime.now(ZoneId.of("Asia/Shanghai")), 18 , teacher); Student student22 = new Student("2", "马", Student.Gender.MALE, ZonedDateTime.now(), 18 , teacher); Student student33 = new Student("3", "张", Student.Gender.FEMALE, ZonedDateTime.now(), 18 , teacher); List<Student> students = Arrays.asList(student, student22, student33); Result<Student> studentResult = new Result<>(); studentResult.getData().put("students", students); Result<Student> students1 = gson.fromJson(gson.toJson(studentResult), new TypeToken<Result<Student>>() { }.getType()); Result<Student> students2 = JSON.parseObject(JSON.toJSONString(studentResult), new com.alibaba.fastjson.TypeReference<Result<Student>>() { }); Result<Student> students3 = mapper.readValue(mapper.writeValueAsString(studentResult), new com.fasterxml.jackson.core.type.TypeReference<Result<Student>>() { });
扩展阅读:java泛型
自定义处理
字段名自定义
1 2 3 4 @com.alibaba.fastjson.annotation.JSONField(name = "aName") @com.fasterxml.jackson.annotation.JsonProperty("aaName") @com.google.gson.annotations.SerializedName("aaaName") private String name;
分别对应了三个库的字段名自定义。
日期格式自定义
在讨论这个问题之前,需要注意的是,只有fastJson可以不做任何处理地支持java8新增的各种时间类(虽然处理 结果未必是你想要的),其他两个库都需要对解析器做一定的处理。 其中jackson需要引入新的pom:
1 2 3 4 5 <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>${jackson.version}</version> </dependency>
同时代码做以下调整:
1 2 3 ObjectMapper mapper = new ObjectMapper() .registerModule(new JavaTimeModule()) .setTimeZone(TimeZone.getTimeZone(ZoneId.of("Asia/Shanghai")));
而Gson的更复杂一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Gson gson = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new JsonSerializer<ZonedDateTime>() { @Override public JsonElement serialize(ZonedDateTime src, Type typeOfSrc, JsonSerializationContext context) { return new JsonPrimitive(src.toInstant().toString()); } }).registerTypeAdapter(ZonedDateTime.class, new JsonDeserializer<ZonedDateTime>() { @Override public ZonedDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { String datetime = json.getAsJsonPrimitive().getAsString(); return Instant.parse(datetime).atZone(ZoneId.of("Asia/Shanghai")); } }).create();
实际上是直接注册了自定义的类序列化工具,如何注册自定义的类序列化工具,这个在下面再详细分析。 更改过后,新的序列化结果是: Gson:”2019-04-02T11:33:25.847Z” Jackson:1554204805. 反序列化都能正常进行。 还可以通过注解简单定制序列化的行为:
1 2 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSSZ", timezone = "Asia/Shanghai")
1 2 @JSONField(format = "yyyy-MM-dd HH:mm:ss.SSSZ") private ZonedDateTime birthday;
其中Z是时区的意思,缺失的话反序列化就会出现问题,但即使如此,Fastjson在采用这种方式序列化之后,反 序列化时会仍然会丢失时区信息。 因为我们刚才的Gson实际上已经完成了完全的自定义序列化和反序列化,所以结果取决于自定义的行为是什 么。
不输出某个字段
FastJson: @JSONField(serialize = false) Gson:直接在字段上加transient关键字,如private transient Gender gender; 但因为transient是java的关键字,也会影响java的java.io.Serializable接口,因此要慎用。更详细的情况 可参照Gson之排除字段的常见方式 jackson:@JsonIgnore
null 值是否输出
Gson和FastJson默认不输出,因此使用下面的语句打开。 Gson:Gson gson = new GsonBuilder().serializeNulls().create() FastJson:JSON.toJSONString(student, SerializerFeature.WriteMapNullValue); Jackson默认输出,因此这样打开: ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
把多个字段映射为一个属性
比如我们从多个源接收数据,想将他们转为反序列化为同一个类,他们的主要字段相同,但字段名却不尽相 同。比如我们作为一家app公司,主要是免费加广告的方式发布app,现在有三家广告商,他们提供接口来获取 广告收入数据,大概格式都是从哪个时间到哪个时间,点击了多少次,收益多少这样的数据,但字段名不一 样,有些叫timeStart,有些叫beginTime,都是json现在传过来了,而我们想统一处理,这里举个例子:
1 2 3 {"序号":1,"名字":"小刘","性别":"Male","年龄":22,"生日":"2019-04-14"} {"age":22,"birthday":"2019-04-14","gender":"Male","name":"小 刘","studentId":1}
1 2 3 @Data public class Student { private Integer studentId = 1 ;
1 2 3 4 5 6 7 8 9 private String name = "小刘"; private Gender gender = Gender.Male; private LocalDate birthday = LocalDate.now(); private Integer age = 22 ; public enum Gender { Male, Female } }
FastJson:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Data public class Student { @JSONField(alternateNames = "序号") private Integer studentId = 1 ; @JSONField(alternateNames = "姓名") private String name = "小刘"; @JSONField(alternateNames = "性别") private Gender gender = Gender.Male; @JSONField(alternateNames = "年龄") private Integer age = 22 ; public enum Gender { Male, Female } }
则以上两个字符串反序列化结果一致。Gson:
1 2 3 4 5 6 7 8 @Data public class Student { @SerializedName(value = "studentId", alternate = "序号") private Integer studentId = 1 ; @SerializedName(value = "name", alternate = "姓名") private String name = "小刘"; @SerializedName(value = "gender", alternate = "性别") private Gender gender = Gender.Male;
1 2 3 4 5 6 7 8 9 // 因为Gson对Date的自定义格式化比较复杂,为演示去掉了这个字段 // private LocalDate birthday = LocalDate.now(); @SerializedName(value = "age", alternate = "年龄") private Integer age = 22 ; public enum Gender { Male, Female } }
jackson :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data //@JsonSerialize(using = StudentSerializer.class) //@JsonDeserialize(using = StudentDeserializer.class) public class Student { @JsonAlias("序号") private Integer studentId = 1 ; @JsonAlias("姓名") private String name = "小刘"; @JsonAlias("性别") private Gender gender = Gender.Male; @JsonAlias("年龄") private Integer age = 22 ; public enum Gender { Male, Female } }
完全自定义的序列化与反序列化
上面已经示例了Gson的自定义序列化与反序列化,即通过registerTypeAdapter方法来注册 JsonSerializer和JsonDeserializer。其实三个库都差不太多,都是手动实现序列化和反序列化,然后 通过某种方式注册到解析器里面去。FastJson :
因为FastJson的序列化和反序列化都是静态方法,所以需要在需要序列化和反序列化的类上添加注解,或者在 每次序列化和反序列化时传入自定义配置。 首先,实现自定义序列化和反序列化类:
1 2 3 4 5 6 7 8 9 10 11 public class LocalDateSerializer implements ObjectSerializer { public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) { SerializeWriter out = serializer.getWriter(); if (object == null) { serializer.getWriter().writeNull(); return; } out.write("\"活在当下\""); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @SuppressWarnings("unchecked") public class LocalDateDeserializer implements ObjectDeserializer { //不管咋样,我都给你整成当前时间,反正是测试用 public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) { JSONLexer lexer = parser.getLexer(); lexer.nextToken( 2 ); //随意设置一个int都可以,但不设置会出异常,不明原 理 return (T) LocalDate.now().plusDays( 1 ); } public int getFastMatchToken() { //意义不明,打断点发现从来没被吊用过 return 0 ; } }
1 2 3 4 5 6 7 8 9 public static void main(String[] args) { Student student = new Student(); SerializeConfig config = new SerializeConfig(); config.put(LocalDate.class, new LocalDateSerializer()); String jsonStr = JSON.toJSONString(student, config); System.out.println(jsonStr); ParserConfig parserConfig = new ParserConfig(); parserConfig.putDeserializer(LocalDate.class, new LocalDateDeserializer());
1 2 3 Student student1 = JSON.parseObject(jsonStr, Student.class); System.out.println(student1); }
1 2 {"age":22,"birthday":"活在当下","gender":"Male","name":"小刘","studentId":1} Student(studentId=1, name=小刘, gender=Male, birthday=2019-04-15, age=22)
如果需要全局注册,则:
1 2 3 4 SerializeConfig.getGlobalInstance().put(LocalDate.class, new LocalDateSerializer()); ParserConfig.getGlobalInstance().putDeserializer(LocalDate.class, new LocalDateDeserializer());
如果想只针对特定的字段进行定制,则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data public class Student { private Integer studentId = 1 ; private String name = "小刘"; private Gender gender = Gender.Male; @JSONField(serializeUsing = LocalDateSerializer.class, deserializeUsing = LocalDateDeserializer.class) private LocalDate birthday = LocalDate.now(); private Integer age = 22 ; public enum Gender { Male, Female } }
@JSONField 的设置项非常丰富,基本全部是关于自定义的东西,详细了解一下应该很有收获。 另外:FastJson还有一类被称为Filter的类,也可以对结果进行一定的自定义,但并非完全的自定义。大部分是 用来判断哪些字段序列化哪些字段不序列化,或者改变序列化后某个字段的名字、值这样的。有兴趣可以了解 一下。Jackson : 套路都一样,首先是实现自己的序列化和反序列化类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class StudentSerializer extends JsonSerializer<Student> { @Override public void serialize(Student value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { jsonGenerator.writeStartObject(); jsonGenerator.writeNumberField("序号", value.getStudentId()); jsonGenerator.writeStringField("名字", value.getName()); jsonGenerator.writeStringField("性别", value.getGender().toString()); jsonGenerator.writeStringField("年龄", value.getAge() + "years- old"); jsonGenerator.writeStringField("生日", value.getBirthday().toString()); jsonGenerator.writeEndObject(); } }
1 2 3 4 5 6 7 public class StudentDeserializer extends JsonDeserializer<Student> { @Override public Student deserialize(JsonParser jp, DeserializationContext ctxt) { return new Student(); } }
1 2 3 4 5 6 7 8 9 Student student = new Student(); SimpleModule module = new SimpleModule(); module.addSerializer(Student.class, new StudentSerializer()); module.addDeserializer(Student.class, new StudentDeserializer()); ObjectMapper mapper = new ObjectMapper().registerModule(module); String jsonStr = mapper.writeValueAsString(student); System.out.println(jsonStr); Student student2 = mapper.readValue(jsonStr, Student.class); System.out.println(student2);
可以看到主要是注册module来实现自定义,更简单的办法是直接在类上添加:
1 2 3 4 5 6 @Data @JsonSerialize(using = StudentSerializer.class) @JsonDeserialize(using = StudentDeserializer.class) public class Student { ... }
这两个注解同时可用于字段级别。 再看 Gson : 可以看到,另外两个库都有通过注解自定义的方法,gson也有@JsonAdapter()注解,但gson用这个注解同 时来序列化与反序列化的自定义工作,鉴于一个类或者字段不可能标注两个同样的注解,因此Gson的序列化和 反序列化是写在同一个类里面的,如下:
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 public class StudentTypeAdapter extends TypeAdapter<Student> { public void write(JsonWriter out, Student value) throws IOException { out.beginObject(); out.name("序号"); out.value(value.getStudentId()); out.name("姓名"); out.value(value.getName()); out.endObject(); } public Student read(JsonReader in) throws IOException { Student student = new Student(); student.setGender(Student.Gender.Female); in.beginObject(); String fieldname = null; while (in.hasNext()) { JsonToken token = in.peek(); if (token.equals(JsonToken.NAME)) { //get the current token fieldname = in.nextName(); } if ("序号".equals(fieldname)) { student.setStudentId(in.nextInt()); } else if ("姓名".equals(fieldname)) { student.setName(in.nextString()); } else in.skipValue(); } in.endObject(); return student; }
之后用@JsonAdapter(StudentTypeAdapter.class)注解即可。
面对循环引用
设想这样的实体类:
1 2 3 4 5 6 @Data public class Student { private Integer studentId = 1 ; private String name = "小刘"; private Teacher teacher; }
1 2 3 4 5 6 @Data public class Teacher { private Integer teacherId = 2 ; private String name = "老王"; private Student student; }
这是一个简化版的一对一家教的数据库表。常见于@OneToOne的场景下。 之后会有这样的代码:
1 2 3 4 Student student = new Student(); Teacher teacher = new Teacher(); student.setTeacher(teacher); teacher.setStudent(student);
那么序列化时会发生什么问题?我们测试一下:
1 2 3 4 5 ObjectMapper mapper = new ObjectMapper(); Gson gson = new GsonBuilder().create(); System.out.println(JSON.toJSONString(teacher)); System.out.println(gson.toJson(teacher)); System.out.println(mapper.writeValueAsString(teacher));
FastJson的输出是:{“student”:{“name”:”小刘”,”studentId”:1,”teacher”:{“$ref”:”..”}}} Gson和Jackson爆栈异常,其实我个人觉得爆栈异常更合理,这种互相持有引用的情况应该特殊处理, fastJson替我们处理了有好有坏,比如其他语言可能无法反序列化这样它的序列化结果,因而引发异常。所以官 方也提供手动关闭的功能:
1 2 //全局关闭 JSON.DEFAULT_GENERATE_FEATURE |=
1 2 3 SerializerFeature.DisableCircularReferenceDetect.getMask(); //单次关闭 JSON.toJSONString(obj, SerializerFeature.DisableCircularReferenceDetect);
那么假设我们真的要处理这样的情况,该如何是好呢? 一种方式是在某个类的对另一个类的引用上忽略另一个类,即不输出这个字段。这样断开循环引用。另外的方 式就是通过某种方式来指明引用方式: jackson下最全的:Jackson – Bidirectional Relationships,其中和FastJson表现形式最接近的是 @JsonIdentityInfo方法。要注意的是@JsonIdentityInfo必须提供的参数是generator,其实是指定 了ID的生成策略,之后通过ID解除相互引用的问题。随便选择一个可用的即可,比如:
1 2 3 4 5 6 7 8 @Data public class Teacher { private Integer teacherId = 2 ; private String name = "老王"; @JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class) private Student student; }
1 2 3 4 5 6 7 8 @Data public class Student { private Integer studentId = 1 ; private String name = "小刘"; @JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class) private Teacher teacher; }
Gson没有查到类似策略,似乎只能通过忽略字段来断开无限循环。 当然了,以上问题都可以通过完全自定义的序列化与反序列化完成工作
三个库与Spring框架整合 目前Spring框架默认使用Jackson来序列化和反序列化json,如果细致一点观察Spring框架,会发现Spring框架 正是使用HttpMessageConverter来实现HTTP信息处理的,其他两个Gson库也提供了和Spring框架整合的能 力,得益于Spring框架本身的松耦合,替换HttpMessageConverter是轻而易举的: FastJson:官方文档:集成Spring框架 Gson: Configure gson in spring using GsonHttpMessageConverter
如果已经决定替换掉jackson,那还应该在pom文件中去除jackson的引用,如下:
1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>jackson-databind</artifactId> <groupId>com.fasterxml.jackson.core</groupId> </exclusion> </exclusions> </dependency>
我该如何选择呢? 整体上讲,三个框架各有特点吧。jackson能被spring官方选中自然有他的道理,对标准的严格执行,强大的可 扩展性、稳定性,速度也很快,缺点是接口不那么易用,要引的包也着实有点多。Gson是谷歌的产品,我们单 位用的比较多,但被fastJson公然嘲讽速度慢: Gson应该叫龟速Json。 fastJson速度快,接口简单,但批评者认为,使用Json本身就不是看中他的速度,而是他的兼容性和灵活性, fastJson硬编码的东西多,灵活性不够,等等。见知乎讨论:fastjson这么快老外为啥还是热衷 jackson,但是它 确实很简单易用,速度也挺快。 从功能上来讲,三者可以说都是完备的。如果是我选择,想偷懒应该会用FastJson,想安全稳定面对特别复杂 的需求,应该还是用jackson。Gson……不知道为什么就是不太喜欢它……