写点什么

3. 懂了这些,方敢在简历上说会用 Jackson 写 JSON

用户头像
YourBatman
关注
发布于: 2020 年 07 月 27 日
3. 懂了这些,方敢在简历上说会用Jackson写JSON

你必须非常努力,才能看起来毫不费力。本文已被 https://www.yourbatman.cn 收录,里面一并有 Spring 技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT 的乌托邦】逐个击破,深入掌握,


前言

各位好,我是 A 哥(YourBatman)。上篇文章:2. 妈呀,Jackson原来是这样写JSON的 知道了 Jackson 写 JSON 的姿势,切实感受了一把 ObjectMapper 原来是这样完成序列化的...本文继续深入讨论 JsonGenerator 写 JSON 的细节。


先闲聊几句题外话哈。我们在书写简历的时候,都会用一定篇幅展示自己的技能点(亮点),就像这样:

这一 part 非常重要,它决定了面试官是否有跟你聊的兴趣,决定了你是否能在浩如烟海的简历中够脱颖而出。如何做到差异性?在当下如此发达的信息社会里,信息的获取唾手可得,所以在知识的广度方面,我认为人与人之间的差异其实并不大:

你知道 DDD 领域驱动、读过架构整洁之道、知道六边形架构、知道 DevOps......难道你还在想凭一些概念卖钱?拉出差距?

你在用 Spring 技术栈、在用 Redis、在用 ElasticSearch......难道你还以为现在像 10 年前一样,会用就能加分?


一聊就会,一问就退,一写就废。这是很多公司程序员的真实写照,基/中层管理者尤甚。早早的和技术渐行渐远,导致裁员潮到来时很容易获得一张“飞机票”,年纪越大,焦虑感越强。


在你的公司是否有过这种场景:四五个人指挥一个人干活。对,就像这样:


扎不扎心,老铁😄。不过不用悲观,从这应该你看到的是机会,习 xx 都说了实干才能兴邦嘛,2019 年裁员潮洗牌后,适者生存,不适者很多回老家了,这也让大批很有实力的程序员享受到了红利。应正了那句:当大潮褪去,才知道谁在裸泳


扯远了,言归正传。Jackson 单会简单使用我认为还不足矣立足,那就跟我来吧~


版本约定

  • Jackson 版本:2.11.0

  • Spring Framework 版本:5.2.6.RELEASE

  • Spring Boot 版本:2.3.0.RELEASE


正文

一个框架/库好不好,不是看它的核心功能做得怎么样,而是非核心功能处理得如何。比如后台页面做得咋样?容错机制呢?定制化、可配置化,扩展性等等。


Jackson 称得上优秀(甚至最佳)最主要是得益于它优秀的 module 模块化设计,在接触其之前,我们先完成本章节的内容:JsonGenerator写 JSON 的行为控制(配置)。


配置属于程序的一部分,它影响着程序执行的方方面面。Spring使用 Environment/PropertySource 管理配置,对应的在 Jackson 里会看到有很多 Feature 类来控制 Jackson 的读/写行为,均是使用 enum 枚举类型来管理。


上篇文章 我们学会了如何使用 JsonGenerator 去写一个 JSON,本文将来学习它的需要掌握的使用细节。同样的,为围绕着 JsonGenerator 展开。


JsonGenerator 的 Feature

它是 JsonGenerator 的一个内部枚举类,共 10 个枚举值:

public enum Feature {
// Low-level I/O AUTO_CLOSE_TARGET(true), AUTO_CLOSE_JSON_CONTENT(true), FLUSH_PASSED_TO_STREAM(true),
// Quoting-related features @Deprecated QUOTE_FIELD_NAMES(true), @Deprecated QUOTE_NON_NUMERIC_NUMBERS(true), @Deprecated ESCAPE_NON_ASCII(false), @Deprecated WRITE_NUMBERS_AS_STRINGS(false),
// Schema/Validity support features WRITE_BIGDECIMAL_AS_PLAIN(false), STRICT_DUPLICATE_DETECTION(false), IGNORE_UNKNOWN(false); ...}
复制代码

小贴士:枚举值均为 bool 类型,括号内为默认值


这个 Feature 的每个枚举值都控制着JsonGenerator写 JSON 时的不同行为,并且可分为三大类(源码处我也有标注):

  • Low-level I/O:底层 I/O 流相关。


Jackson 的流式 API 指的是 I/O 流,因此就涉及到关流、flush 刷新流等操作


  • Quoting-related:双引号""引用相关。


JSON 规范规定 key 都必须有双引号,但这对于某些场景下并不需要


  • Schema/Validity support:约束/规范/校验相关。


JSON 作为 K-V 结构的数据,那么允许相同 key 出现吗?这便由这些特征去控制


下面分别来认识认识它们。


AUTOCLOSETARGET(true)

含义即为字面意:自动关闭目标(流)。

  • true:调用JsonGenerator#close()便会自动关闭底层的 I/O 流,你无需再关心

  • false:底层 I/O 流请手动关闭


自动关闭:

@Testpublic void test1() throws IOException {    JsonFactory factory = new JsonFactory();    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {        // doSomething    }}
复制代码

如果改为 false:那么你就需要自己手动去 close 底层使用的 OutputStream 或者 Writer。形如这样:

@Testpublic void test2() throws IOException {    JsonFactory factory = new JsonFactory();    try (PrintStream err = System.err; JsonGenerator jg = factory.createGenerator(err, JsonEncoding.UTF8)) {        // 特征置为false 采用手动关流的方式        jg.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
// doSomething }}
复制代码

小贴士:例子均采用try-with-resources方式关流,所以并没有显示调用 close()方法,你应该能懂吧😄


AUTOCLOSEJSON_CONTENT(true)

先来看下面这段代码:

@Testpublic void test3() throws IOException {    JsonFactory factory = new JsonFactory();    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {        jg.writeStartObject();        jg.writeFieldName("names");
// 写数组 jg.writeStartArray(); jg.writeString("A哥"); jg.writeString("YourBatman"); }}
复制代码

运行程序,输出:

{"names":["A哥","YourBatman"]}
复制代码

wow,竟然输出一切正常。细心的你会发现,我的代码是缺胳膊少腿的:不管是 Object 还是 Array 都只 start 了,并没有显示调用 end 进行闭合。但是呢,结果却正常得很,这便是此 Feature 的作用了。

  • true:自动补齐(闭合)JsonToken#START_ARRAYJsonToken#START_OBJECT类型的内容

  • false:啥都不做(不会主动抛错哦)


不过还是要啰嗦一句:虽然 Jackson 通过此 Feature 做了容错,但是自己在使用时,请务必显示书写闭合


FLUSHPASSEDTO_STREAM(true)

在使用带有缓冲区的 I/O 写数据时,缺少“临门一脚”是初学者很容易犯的错误,比如下面这个例子:

@Testpublic void test4() throws IOException {    JsonFactory factory = new JsonFactory();    JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8);
jg.writeStartObject(); jg.writeStringField("name","A哥"); jg.writeEndObject();
// jg.flush(); // jg.close();}
复制代码

运行程序,控制台没有任何输出。把注释代码放开任何一行,再次运行程序,控制台正常输出:

{"name":"A哥"}
复制代码
  • true:当 JsonGenerator 调用 close()/flush()方法时,自动强刷 I/O 流里面的数据

  • false:请手动处理


为何需要 flush()?

对于此问题这里小科普一下。因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统(话外音:这是操作系统为之)并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个 byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多 IO 设备来说,一次写一个字节和一次写 1000 个字节,花费的时间几乎是完全一样的,所以 OutputStream 有个 flush()方法,能强制把缓冲区内容输出。

小贴士:InputStream 是没有 flush()方法的哦


通常情况下,我们不需要调用这个 flush()方法,因为缓冲区写满了,OutputStream 会自动调用它,并且,在调用 close()方法关闭 OutputStream 之前,也会自动调用 flush()方法强制刷一次缓冲区。但是,在某些情况下,我们必须手动调用 flush()方法,比如上例子,比如发 IM 消息...


~~QUOTEFIELDNAMES(true)~~

此属性自2.10版本后已过期,使用JsonWriteFeature#QUOTE_FIELD_NAMES代替,应用在 JsonFactory 上,后文详解


JSON 对象字段名是否为使用""双引号括起来,这是 JSON 规范(RFC4627)规定的。

  • true:字段名使用""括起来 -> 遵循 JSON 规范

  • false:字段名使用""括起来 -> 遵循 JSON 规范

@Testpublic void test5() throws IOException {    JsonFactory factory = new JsonFactory();    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {		// jg.disable(QUOTE_FIELD_NAMES);		        jg.writeStartObject();        jg.writeStringField("name","A哥");        jg.writeEndObject();    }}
复制代码

运行程序,输出:

{"name":"A哥"}
复制代码

99.99%的情况下我们不需要改变默认值。Jackson 添加了禁用引号的功能以支持那非常不常见的情况,最常见的情况直接从 Javascript 中使用时可能会发生。


打开注释掉的语句,再次运行程序,输出:

{name:"A哥"}
复制代码


~~QUOTENONNUMERIC_NUMBERS(true)~~

此属性自2.10版本后已过期,使用JsonWriteFeature#WRITE_NAN_AS_STRINGS代替,应用在 JsonFactory 上,后文详解


这个特征挺有意思,看例子(以写 Float 为例):

@Testpublic void test6() throws IOException {    JsonFactory factory = new JsonFactory();    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {    	// jg.disable(JsonGenerator.Feature.QUOTE_NON_NUMERIC_NUMBERS);
jg.writeNumber(0.9); jg.writeNumber(1.9);
jg.writeNumber(Float.NaN); jg.writeNumber(Float.NEGATIVE_INFINITY); jg.writeNumber(Float.POSITIVE_INFINITY); }}
复制代码

运行程序,输出:

0.9 1.9 "NaN" "-Infinity" "Infinity"
复制代码

同为 Float 数字类型,有的输出有""双引号包着,有的没有。放开注释的语句(禁用此特征),再次运行程序,输出:

0.9 1.9 NaN -Infinity Infinity
复制代码

很明显,如果你是这么输出为一个 JSON 的话,那它就会是非法的 JSON,是不符合 JSON 标准的(因为像 NaN、Infinity 这种明显是字符串嘛,必须用""包起来才是合法的 value 值)。


由于 JSON 规范中对数字的严格定义,加上 Java 可能具有的开放式数字集(如上例中 Float 类型并不 100%是数字),很难做到既安全又方便,因此有了此特征让你根据需要来控制。


~~ESCAPENONASCII(false)~~

此属性自2.10版本后已过期,使用JsonWriteFeature#ESCAPE_NON_ASCII代替,应用在 JsonFactory 上,后文详解


@Testpublic void test7() throws IOException {    JsonFactory factory = new JsonFactory();    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {        // jg.enable(ESCAPE_NON_ASCII);        jg.writeString("A哥");    }}
复制代码

运行程序,输出:

"A哥"
复制代码

放开注掉的代码(开启此属性),再次运行,输出:

"A\u54E5"
复制代码


~~WRITENUMBERSAS_STRINGS(false)~~

此属性自2.10版本后已过期,使用JsonWriteFeature#WRITE_NUMBERS_AS_STRINGS代替,应用在 JsonFactory 上,后文详解


该特性强制将*所有*Java 数字写成字符串,即使底层数据格式真的是数字。

  • true:所有数字强制写为字符串

  • false:不做处理


@Testpublic void test8() throws IOException {    JsonFactory factory = new JsonFactory();    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {        // jg.enable(WRITE_NUMBERS_AS_STRINGS);
Long num = Long.MAX_VALUE; jg.writeNumber(num); }}
复制代码

运行程序,输出:

9223372036854775807
复制代码

放开注释代码(开启此特征),再次运行程序,输出:

"9223372036854775807"
复制代码

有什么使用场景?一个用例是避免 Javascript 限制的问题:因为 Javascript 标准规定所有的数字处理都应该使用 64 位 ieee754 浮点值来完成,结果是一些 64 位整数值不能被精确表示(因为尾数只有 51 位宽)。

采坑提醒:时间戳后端用 Long 类型反给前端是没有问题的。但如果你是很大的一个 Long 值(如雪花算法算出的很大的 Long 值),直接返回前端的话,Javascript 就会出现精度丢失的 bug


WRITEBIGDECIMALAS_PLAIN(false)

控制写java.math.BigDecimal的行为:

  • true:使用BigDecimal#toPlainString()方法输出

  • false: 使用默认输出方式(取决于 BigDecimal 是如何构造的)


@Testpublic void test7() throws IOException {    JsonFactory factory = new JsonFactory();    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {        // jg.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
BigDecimal bigDecimal1 = new BigDecimal(1.0); BigDecimal bigDecimal2 = new BigDecimal("1.0"); BigDecimal bigDecimal3 = new BigDecimal("1E11"); jg.writeNumber(bigDecimal1); jg.writeNumber(bigDecimal2); jg.writeNumber(bigDecimal3); }}
复制代码

运行程序,输出:

1 1.0 1E+11
复制代码

放开注释代码,再次运行程序,输出:

1 1.0 100000000000
复制代码


STRICTDUPLICATEDETECTION(false)

是否去严格的检测重复属性名。

  • true:检测是否有重复字段名,若有,则抛出JsonParseException异常

  • false:不检测 JSON 对象重复的字段名,即:相同字段名都要解析


@Testpublic void test8() throws IOException {    JsonFactory factory = new JsonFactory();    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {        // jg.enable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION);
jg.writeStartObject(); jg.writeStringField("name","YourBatman"); jg.writeStringField("name","A哥"); jg.writeEndObject(); }}
复制代码

运行程序,输出:

{"name":"YourBatman","name":"A哥"}
复制代码

打开注释掉的哪行代码:开启此特征值为 true。再次运行程序,输出:

com.fasterxml.jackson.core.JsonGenerationException: Duplicate field 'name'
at com.fasterxml.jackson.core.json.JsonWriteContext._checkDup(JsonWriteContext.java:224) at com.fasterxml.jackson.core.json.JsonWriteContext.writeFieldName(JsonWriteContext.java:217) ...
复制代码

注意:谨慎打开此开关,如果检查的话性能会下降 20%-30%。


IGNORE_UNKNOWN(false)

如果底层数据格式需要输出所有属性,以及如果找不到调用者试图写入的属性的定义,则该特性确定是否要执行的操作。


可能你听完还一脸懵逼,什么底层数据格式,什么找不到,我明明是写 JSON 啊,何解?其实这不是针对于写 JSON 来说的,对于 JSON,这个特性没有效果,因为属性不需要预先定义。通常,大多数文本数据格式不需要模式信息,而某些二进制数据格式需要定义(如 Avro、protobuf),因此这个属性是为它们而生(Smile、BSON 等这些二进制也是不需要预定模式信息的哦)。

强调:JsonGenerator不是只能写 JSON 格式,毕竟底层是 I/O 流嘛,理论上啥都能写


  • true:启动该功能


可以预先调用(在写数据之前)这个 API 设定好模式信息即可:

JsonGenerator:
public void setSchema(FormatSchema schema) { ... }
复制代码


  • false:禁用该功能。如果底层数据格式需要所有属性的知识才能输出,那就抛出 JsonProcessingException 异常


定制 Feature

通过上一 part 知晓了控制JsonGenerator的特征值们,以及其作用是。Feature 的每个枚举值都有个默认值(括号里面),那么如果我们希望对不同的 JsonGenerator 实例应用不同的配置该怎么办呢?


自然而然的 JsonGenerator 提供了相关 API 供以我们操作:

// 开启public abstract JsonGenerator enable(Feature f);// 关闭public abstract JsonGenerator disable(Feature f);// 开启/关闭public final JsonGenerator configure(Feature f, boolean state) { ... };
public abstract boolean isEnabled(Feature f);public boolean isEnabled(StreamWriteFeature f) { ... };
复制代码


替换者:StreamWriteFeature

本类是 2.10 版本新增的,用于完全替换上面的 Feature。目的:完全独立的属性配置,不依赖于任何后端格式,因为JsonGenerator并不局限于写 JSON,因此把 Feature 放在 JsonGenerator 作为内部类是不太合适的,所以单独摘出来。


StreamWriteFeature 用在JsonFactory里,后面再讲解到它的构建器JsonFactoryBuilder时再详细探讨。


序列化 POJO 对象

上篇文章用代码演示过了如何使用writeObject(Object pojo)来把一个 POJO 一次性序列化成为一个 JSON 串,它主要依赖于 ObjectCodec 去完成:

public abstract JsonGenerator setCodec(ObjectCodec oc);
复制代码

ObjectCodec 可谓是 Jackson 里极其重要的一个基础组件,我们最熟悉的ObjectMapper它就是一个解码器,实现了序列化和反序列化、树模型等操作。这将在后面章节里重点介绍~


输出漂亮的 JSON 格式

我们知道 JSON 之所以快速流行的原因之一是得益于它的可读性好,可读性好又表现在它漂亮的(规则)的展示格式上。


默认情况下,使用JsonGenerator写 JSON 时,所有的部分都是输出在同一行里,显然这种格式对人阅读来说是不够友好的。作为最流行的 JSON 库自然考虑到了这一点,提供了格式化器来美化输出

// 自己指定漂亮格式打印器public JsonGenerator setPrettyPrinter(PrettyPrinter pp) { ... }
// 应用默认的漂亮格式打印器public abstract JsonGenerator useDefaultPrettyPrinter();
复制代码

PrettyPrinter 有如下两个实现类:

使用不同的实现类,对输出结果的影响如下:

什么都不设置:MinimalPrettyPrinter:{"zhName":"A哥","enName":"YourBatman","age":18}
DefaultPrettyPrinter:useDefaultPrettyPrinter():{ "zhName" : "A哥", "enName" : "YourBatman", "age" : 18}
复制代码

由此可见,在什么都不设置的情况下,结果会全部在一行显示(紧凑型输出)。DefaultPrettyPrinter表示带层级格式的输出(可读性好),若有此需要,建议直接调用更为快捷的useDefaultPrettyPrinter()方法,而不用自己去 new 一个实例。


总结

本文的主要内容和重点是介绍了用 Feature 去控制 JsonGenerator 的写行为,不同的特征值控制着不同的行为。在实际使用时可针对不同的需求,定制出不同的JsonGenerator实例,因地制宜和互相隔离。


相关推荐:


---------

关注 A 哥

Author | A哥(YourBatman)

-------- | -----

个人站点 | www.yourbatman.cn

E-mail | yourbatman@qq.com

微 信 | fsx641385712

公众号 | BAT的乌托邦(ID:BAT-utopia)

知识星球 | BAT的乌托邦

每日文章推荐 | 每日文章推荐



发布于: 2020 年 07 月 27 日阅读数: 215
用户头像

YourBatman

关注

分享、成长,拒绝浅尝辄止。 2018.01.21 加入

分享、成长,拒绝浅尝辄止。公众号:BAT的乌托邦

评论 (2 条评论)

发布
用户头像
再来一瓶~「手动狗头」
2020 年 07 月 27 日 16:39
回复
用户头像
真相了
2020 年 07 月 27 日 12:11
回复
没有更多了
3. 懂了这些,方敢在简历上说会用Jackson写JSON