24

妈呀,Jackson 原来是这样写 JSON 的

 4 years ago
source link: https://mp.weixin.qq.com/s/p6cwP2BVrC8VxkN-T3uAxg
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

点击上方“ BAT的乌托邦 ”,选择“ 设为星标

后台回复“ 专栏 ”,开启 专栏模式 学习

uuYnaaZ.png!web

前言

各位好,我是A哥(YourBatman)。上篇文章 整体介绍了世界上最好的JSON库 -- Jackson,对它有了整体了解:知晓了它是个生态,其它的仅是个JSON库而已。

有人说Jackson小众?那么请先看看上篇文章吧。学Jackson性价比特别高,因为它使用广泛、会的人少,因此在团队内如果你能精通,附加价值的效应就会非常明显了...

我挠头想了想,本系列来不了虚的,只能肝。本系列教程不仅仅教授基本使用,目标是搞完后能够解决日常99.99%的问题,毕竟每个小团队都最好能有某些方面的小专家,毕竟大家都不乏遇见过一个技术问题卡一天的情况。 「只有从底层把握,方能游刃有余」zYRjUnI.png!web

命名为core的模块一般都不简单, jackson-core 自然也不例外。它是三大核心模块之一,并且是 「核心中的核心」 ,提供了对JSON数据的 「完整支持」 (包括各种读、写)。它是三者中最强大的模块,具有 「最低的」 开销和 「最快的」 读/写操作。

此模块提供了 「最具底层」 的Streaming JSON解析器/生成器,这组流式API属于Low-Level API,具有非常显著的特点:

  • 开销小,损耗小,性能极高

  • 因为是Low-Level API,所以灵活度极高

  • 又因为是Low-Level API,所以易错性高,可读性差

「jackson-core」模块提供了两种处理JSON的方式(纵缆整个Jackson共三种):

  1. 流式API:读取并将JSON内容写入作为离散事件 -> JsonParser 读取数据,而 JsonGenerator 负责写入数据
  2. 树模型:JSON文件在内存里以树形式表示。此种方式也很灵活,它类似于XML的DOM解析,层层嵌套的

作为“底层”技术,应用级开发中确实接触不多。为了引起你的重视,提前预告一下: Spring MVC 对JSON消息的转换器 AbstractJackson2HttpMessageConverter 它就用到了底层流式API -> JsonGenerator写数据。想不想拿下Spring呢?我想你的答案应该是Yes吧~ ABzQZrU.png!web

相信做 「难事必有所得」 ,你我他都会用的技术、都能解决的问题,那绝成不了你的核心竞争力,自然在团队内就难成发光体。

版本约定

原则:均选当前最新版本(忽略小版本)

  • Jackson版本: 2.11.0
  • Spring Framework版本: 5.2.6.RELEASE
  • Spring Boot版本: 2.3.0.RELEASE
    • 内置的Jackson和Spring版本均和:point_up_2:保持一致,避免了版本交叉

说明:类似2.11.0和2.11.x这种小版本号的差异,你权可认为没有区别

工程结构

鉴于是首次展示工程示例代码,将基本结构展示如下: MFjqIrq.png!webRzeMNzz.png!web

全部源码地址在本系列的 「最后一篇」 文章中会全部公示出来

正文

Jackson提供了一种对性能有极致要求的方式:流式API。它用于对性能有极致要求的场景,这个时候就可以使用此种方式来对JSON进行读写。

概念解释:流式、增量模式、JsonToken

  • 流式(Streaming):此概念和Java8中的Stream流是不同的。这里指的是 「IO流」 ,因此具有最低的开销和最快的读/写操作(记得关流哦)

  • 增量模式(incremental mode):它表示每个部分一个一个地往上增加,类似于垒砖。使用此流式API读写JSON的方式使用的 「均是增量模式」

  • JsonToken:每一部分都是一个独立的Token(有不同类型的Token),最终被“拼凑”起来就是一个JSON。这是流式API里很重要的一个抽象概念。

关于增量模式和Token概念,在Spirng的 「SpEL」 表达式中也有同样的概念,这在Spring相关专栏里你将会再次体会到

F3Yfyai.png!web 本文将看看它是如何写JSON数据的,也就是 JsonGenerator

JsonGenerator使用Demo

JsonGenerator 定义用于编写JSON内容的公共API的基类(抽象类)。实例使用的工厂方法创建,也就是 JsonFactory

小贴士:纵观整个Jackson,它更多的是使用抽象类而非接口,这是它的一大“特色”。因此你熟悉的面向接口编程,到这都要转变为面向抽象类编程喽。

话不多说,先来一个Demo感受一把:

@Test
public void test1() throws IOException {
JsonFactory factory = new JsonFactory();
// 本处只需演示,向控制台写(当然你可以向文件等任意地方写都是可以的)
JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8);

try {
jsonGenerator.writeStartObject(); //开始写,也就是这个符号 {

jsonGenerator.writeStringField("name", "YourBatman");
jsonGenerator.writeNumberField("age", 18);

jsonGenerator.writeEndObject(); //结束写,也就是这个符号 }
} finally {
jsonGenerator.close();
}
}

因为JsonGenerator实现了 AutoCloseable 接口,因此可以使用 try-with-resources 优雅关闭资源(这也是推荐的使用方式),代码改造如下:

@Test
public void test1() throws IOException {
JsonFactory factory = new JsonFactory();
// 本处只需演示,向控制台写(当然你可以向文件等任意地方写都是可以的)
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject(); //开始写,也就是这个符号 {

jsonGenerator.writeStringField("name", "YourBatman");
jsonGenerator.writeNumberField("age", 18);

jsonGenerator.writeEndObject(); //结束写,也就是这个符号 }
}
}

运行程序, 「控制台」 输出:

{"name":"YourBatman","age":18}

这是最简使用示例,这也就是所谓的 「序列化」 底层实现,从示例中对 「增量模式」 能够有所感受吧。

纯手动档有木有,灵活性和性能极高,但易出错。这就像头文字D的赛车一样,先要速度、高性能、灵活性,那必须上手动档。 NFzy2qy.png!web

JsonGenerator详细介绍

JsonGenerator是个抽象类,它的继承体系如下: RNrUBfM.png!web

  • WriterBasedJsonGenerator :基于java.io.Writer处理字符编码(话外音:使用Writer输出JSON)
    • 因为UTF-8编码基本标准化了,因此Jackson内部也提供了 SegmentedStringWriter/UTF8Writer 来简化操作
  • UTF8JsonGenerator :基于OutputStream + UTF-8处理字符编码(话外音:明确指定了使用UTF-8编码把字节变为字符)

默认情况下(不指定编码),Jackson默认会使用UTF-8进行编码,也就是说会使用 UTF8JsonGenerator 作为实际的JSON生成器实现类,具体逻辑将在讲述 JsonFactory 章节中有所体现,敬请关注。

值得注意的是,抽象基类 JsonGenerator 它只负责JSON的生成,至于把生成好的JSON写到哪里去它并不关心。比如示例中我给写到了控制台,当然你也可以写到文件、写到网络等等。

Spring MVC中的JSON消息转换器就是向 HttpOutputMessage (网络输出流)里写JSON数据

关键API

JsonGenerator 虽然仅是抽象基类,但Jackson它建议我们使用 JsonFactory 工厂来创建其实例,并不需要使用者去关心其底层实现类,因此我们仅需要 「面向此抽象类编程」 即可,此为对使用者非常友好的设计。

对于JSON生成器来说,写方法自然是它的灵魂所在。众所周知,JSON属于K-V数据结构,因此针对于一个JSON来说,每一段都k额分为 「写key」「写value」 两大阶段。 QB7ZZji.png!web

写JSON Key

JsonGenerator一共提供了3个方法用于写JSON的key: 73mea23.png!web

@Test
public void test2() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();

jsonGenerator.writeFieldName("zhName");

jsonGenerator.writeEndObject();
}
}

运行程序,输出:

{"zhName"}

可以发现, 「key可以独立存在(无需value)」 ,但value是不能独立存在的哦,下面你会看到效果。而3个方法中的 「其它2个方法」

public abstract void writeFieldName(SerializableString name) throws IOException;

public void writeFieldId(long id) throws IOException {
writeFieldName(Long.toString(id));
}

这两个方法,你可以忘了吧,记住 writeFieldName() 就足够了。

总的来说,写JSON的key非常简单的,这得益于JSON的key有且仅可能是String类型,所以情况单一。下面继续了解较为复杂的写Value的情况。

写JSON Value

我们知道在Java中数据存在的形式(类型)非常之多,比如String、int、Reader、char[]...,而在JSON中 「值的类型」 只能是如下形式:

  • 字符串(如 { "name":"YourBatman" }
  • 数字(如 { "age":18 }
  • 对象(JSON 对象)(如 { "person":{ "name":"YourBatman", "age":18}}
  • 数组(如 {"names":[ "YourBatman", "A哥" ]}
  • 布尔(如 { "success":true }
  • null(如: { "name":null }

小贴士:像数组、对象等这些“高级”类型可以互相无限嵌套

很明显,Java中的数据类型和JSON中的值类型并不是一一对应的关系,那么这就需要 JsonGenerator 在写入时起到一个桥梁(适配)作用: f2a2YvV.png!web 下面针对不同的Value类型分别作出API讲解,给出示例说明。在此之前,请先记住两个结论,会更有利于你理解示例:

  • JSON的顺序,和你write的顺序保持一致

  • 写任何类型的Value之前请记得先write写key,否则可能无效

q2uiqqQ.png!web 可把Java中的String类型、Reader类型、char[]字符数组类型等等写为JSON的字符串形式。

@Test
public void test3() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();

jsonGenerator.writeFieldName("zhName");
jsonGenerator.writeString("A哥");

jsonGenerator.writeFieldName("enName");
jsonGenerator.writeString("YourBatman");

jsonGenerator.writeEndObject();
}
}

运行程序,输出:

{"zhName":"A哥","enName":"YourBatman"}

ueuE3u3.png!web 参考上例,不解释。

对象(JSON 对象)

7ZFb2u6.png!web
@Test
public void test4() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();

jsonGenerator.writeFieldName("zhName");
jsonGenerator.writeString("A哥");

// 写对象(记得先写key 否则无效)
jsonGenerator.writeFieldName("person");
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("enName");
jsonGenerator.writeString("YourBatman");
jsonGenerator.writeFieldName("age");
jsonGenerator.writeNumber(18);
jsonGenerator.writeEndObject();

jsonGenerator.writeEndObject();
}
}

运行程序,输出:

{"zhName":"A哥","person":{"enName":"YourBatman","age":18}}

对象属于一个比较特殊的value值类型,可以实现各种嵌套。也就是我们平时所说的JSON套JSON

写数组和写对象有点类似,也会有先start再end的闭环思路。 IZvIjiu.png!web 如何向数组里写入Value值?我们知道JSON数组里可以装任何数据类型,因此往里写值的方法都可使用,形如这样:

@Test
public void test5() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();

jsonGenerator.writeFieldName("zhName");
jsonGenerator.writeString("A哥");

// 写数组(记得先写key 否则无效)
jsonGenerator.writeFieldName("objects");
jsonGenerator.writeStartArray();
// 1、写字符串
jsonGenerator.writeString("YourBatman");
// 2、写对象
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("enName", "YourBatman");
jsonGenerator.writeEndObject();
// 3、写数字
jsonGenerator.writeNumber(18);
jsonGenerator.writeEndArray();

jsonGenerator.writeEndObject();
}
}

运行程序,输出:

{"zhName":"A哥","objects":["YourBatman",{"enName":"YourBatman"},18]}

理论上JSON数组里的每个元素可以是不同类型,但 「原则上」 请确保是同一类型哦

对于JSON数组类型,很多时候里面装载的是数字或者普通字符串类型,因此 JsonGenerator 也很暖心的为此提供了专用方法(可以调用该方法来一次性便捷的写入单个数组): zE3uEbI.png!web

@Test
public void test6() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();

jsonGenerator.writeFieldName("zhName");
jsonGenerator.writeString("A哥");

// 快捷写入数组(从第index = 2位开始,取3个)
jsonGenerator.writeFieldName("values");
jsonGenerator.writeArray(new int[]{1, 2, 3, 4, 5, 6}, 2, 3);

jsonGenerator.writeEndObject();
}
}

运行程序,输出:

{"zhName":"A哥","values":[3,4,5]}

布尔和null

比较简单,JsonGenerator各提供了一个方法供你使用:

public abstract void writeBoolean(boolean state) throws IOException;
public abstract void writeNull() throws IOException;

示例代码:

@Test
public void test7() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();

jsonGenerator.writeFieldName("success");
jsonGenerator.writeBoolean(true);
jsonGenerator.writeFieldName("myName");
jsonGenerator.writeNull();

jsonGenerator.writeEndObject();
}
}

运行程序,输出:

{"success":true,"myName":null}

组合写JSON Key和Value

在写每个value之前,都必须写key。为了 「简化书写」 ,JsonGenerator提供了二合一的组合方法,一个顶两: IbQ7RjE.png!web

@Test
public void test8() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();

jsonGenerator.writeStringField("zhName","A哥");
jsonGenerator.writeBooleanField("success",true);
jsonGenerator.writeNullField("myName");
// jsonGenerator.writeObjectFieldStart();
// jsonGenerator.writeArrayFieldStart();

jsonGenerator.writeEndObject();
}
}

运行程序,输出:

{"zhName":"A哥","success":true,"myName":null}

实际使用时, 「推荐使用这些组合方法」 去简化书写,毕竟新盖中盖高钙片,一片能顶过去2片,效率高。 rQFFZnR.png!web

其它写方法

如果说上面写方法是必修课,那下面的write写方法就当选修课吧。

「writeRaw()和writeRawValue()」: nmIFBbJ.png!web 该方法将强制生成器 「不做任何修改」 地逐字复制输入文本(包括不进行转义,也不添加分隔符,即使上下文[array,object]可能需要这样做)。如果需要这样的分隔符,请改用writeRawValue方法。

绝大多数情况下,使用writeRaw()就够了,writeRawValue的使用场景愈发的少

@Test
public void test9() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeRaw("{'name':'YourBatman'}");
}
}

运行程序,输出:

{'name':'YourBatman'}

如果换成 writeString() 方法,结果为(请注意比较差异):

"{'name':'YourBatman'}"

「writeBinary()」: JvmIVnB.png!web 使用Base64编码把数据写进去。

「writeEmbeddedObject()」: 2.8版本新增的方法。看看此方法的源码你就知道它是什么意思,不解释:

public void writeEmbeddedObject(Object object) throws IOException {
// 01-Sep-2016, tatu: As per [core#318], handle small number of cases
if (object == null) {
writeNull();
return;
}
if (object instanceof byte[]) {
writeBinary((byte[]) object);
return;
}
throw new JsonGenerationException(...);
}

「writeObject()」(重要): 写POJO,但前提是你必须给 JsonGenerator 指定一个 ObjectCodec 解码器才能正常work,否则抛出异常:

java.lang.IllegalStateException: No ObjectCodec defined for the generator, can only serialize simple wrapper types (type passed cn.yourbatman.jackson.core.beans.User)

at com.fasterxml.jackson.core.JsonGenerator._writeSimpleObject(JsonGenerator.java:2238)
at com.fasterxml.jackson.core.base.GeneratorBase.writeObject(GeneratorBase.java:391)
...

值得注意的是,Jackson里我们最为熟悉的API ObjectMapper 它就是一个ObjectCodec解码器,具体我们在 「数据绑定」 章节会再详细讨论,下面我给出个简单的使用示例模拟一把:

准备一个User对象,以及解码器UserObjectCodec:

@Data
public class User {
private String name = "YourBatman";
private Integer age = 18;
}

// 自定义ObjectCodec解码器 用于把User写为JSON
// 因为本例只关注write写,因此只需要实现此这一个方法即可
public class UserObjectCodec extends ObjectCodec {
...
@Override
public void writeValue(JsonGenerator gen, Object value) throws IOException {
User user = User.class.cast(value);

gen.writeStartObject();
gen.writeStringField("name",user.getName());
gen.writeNumberField("age",user.getAge());
gen.writeEndObject();
}
...
}

测试用例:

@Test
public void test11() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
jsonGenerator.setCodec(new UserObjectCodec());

jsonGenerator.writeObject(new User());
}
}

运行程序,输出:

{"name":"YourBatman","age":18}

:smile:这就是 ObjectMapper 的原理雏形,是不是开始着道了?:smile:

「writeTree()」: 顾名思义,它便是Jackson大名鼎鼎的 「树模型」 。可惜的是core模块并没有提供树模型TreeNode的实现,以及它也是得依赖于ObjectCodec才能正常完成解码。

方法用来编写给定的JSON树(表示为树,其中给定的JsonNode是根)。这通常只调用给定节点的writeObject,但添加它是为了方便起见,并使代码在专门处理树的情况下更显式。

可能你会想,已经有了 writeObject() 方法还要它干啥呢?这其实是蛮有必要的,因为有时候你并不想定义POJO时,就可以用它快速写/读数据,同时它也可以达到 「模糊掉类型的概念」 ,做到更抽象和更公用。

说到模糊掉类型的的操作,你也可以辅以Spring的 AnnotationAttributes 的设计和使用来理解

准备一个TreeNode的实现UserTreeNode:

public class UserTreeNode implements TreeNode {

private User user;

public User getUser() {
return user;
}

public UserTreeNode(User user) {
this.user = user;
}
...
}

UserObjectCodec改写如下:

public class UserObjectCodec extends ObjectCodec {
...
@Override
public void writeValue(JsonGenerator gen, Object value) throws IOException {
User user = null;
if (value instanceof User) {
user = User.class.cast(value);
} else if (value instanceof TreeNode) {
user = UserTreeNode.class.cast(value).getUser();
}

gen.writeStartObject();
gen.writeStringField("name", user.getName());
gen.writeNumberField("age", user.getAge());
gen.writeEndObject();
}
...
}

书写测试用例:

@Test
public void test12() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
jsonGenerator.setCodec(new UserObjectCodec());
jsonGenerator.writeObject(new UserTreeNode(new User()));
}
}

运行程序,输出:

{"name":"YourBatman","age":18}

本案例绕过了 TreeNode 的真实处理逻辑,是因为 「树模型」 这块会放在databind数据绑定模块进行更加详细的描述,后面再会喽。

说明:Jackson的树模型是比较重要的,当然直接使用core模块的树模型没有意义,所以这里先卖个关子,保持好奇心哈:smile:

思考题

国人很喜欢把Jackson的序列化(写JSON)效率和Fastjson进行对比,那么你敢使用本文的流式API和Fastjson比吗?结果你猜一下呢?

总结

本文介绍了jackson-core模块的流式API,以及JsonGenerator写JSON的使用,相信对你理解Jackson生成JSON方面是有帮助的。它作为JSON处理的基石,虽然并不推荐直接使用,但仅仅是 「应用开发级别」 不推荐哦,如果你是个框架、中间件开发者,这些原理你很可能绕不过。

还是那句话,本文介绍它的目的并不是建议大家去项目上使用,而是为了后面理解 ObjectMapper 夯实基础,毕竟做技术的要知其然,知其所以然了后,面对问题才能坦然。

FBF7VvQ.gif

拒绝浅尝辄止,我们是认真的 (关注公众号回复“知识星球”领券后再轻装入驻)

NbqQry3.jpg!web

NVFzUvI.gif

关注A哥,开启专栏式学习

zuiU3eN.jpg!web2Yju2yE.png!web

扫码关注后,回复“专栏”进入更多Spring专栏学习

右侧为私人微信(加好友备注:Java入群)

个人站点 (开白申请) :https://www.yourbatman.cn


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK