你必须非常努力,才能看起来毫不费力。本文已被 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出现吗?这便由这些特征去控制

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

AUTO_CLOSE_TARGET(true)

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

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

自动关闭:

@Test
public void test1() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
// doSomething
}
}

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

@Test
public 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()方法,你应该能懂吧

AUTO_CLOSE_JSON_CONTENT(true)

先来看下面这段代码:

@Test
public 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做了容错,但是自己在使用时,请务必显示书写闭合

FLUSH_PASSED_TO_STREAM(true)

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

@Test
public 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消息...

QUOTE_FIELD_NAMES(true)

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

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

  • true:字段名使用""括起来 -> 遵循JSON规范
  • false:字段名使用""括起来 -> 遵循JSON规范
@Test
public 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哥"}

QUOTE_NON_NUMERIC_NUMBERS(true)

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

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

@Test
public 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%是数字),很难做到既安全又方便,因此有了此特征让你根据需要来控制。

ESCAPE_NON_ASCII(false)

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

@Test
public 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"

WRITE_NUMBERS_AS_STRINGS(false)

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

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

  • true:所有数字强制写为字符串
  • false:不做处理
@Test
public 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

WRITE_BIGDECIMAL_AS_PLAIN(false)

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

  • true:使用BigDecimal#toPlainString()方法输出
  • false: 使用默认输出方式(取决于BigDecimal是如何构造的)
@Test
public 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

STRICT_DUPLICATE_DETECTION(false)

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

  • true:检测是否有重复字段名,若有,则抛出JsonParseException异常
  • false:不检测JSON对象重复的字段名,即:相同字段名都要解析
@Test
public 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实例,因地制宜和互相隔离。

相关推荐:

3. 懂了这些,方敢在简历上说会用Jackson写JSON的更多相关文章

  1. 什么样的 GitHub 才适合放简历上?

    为什么 GitHub 可以加分? 很多招聘描述上面都会备注 GitHub 是加分项,那么为什么它是加分项呢? 停,如果看到这里你还不知道 GitHub 是什么,可以看一下 Phodal的手记:http ...

  2. 【Java8新特性】不了解Optional类,简历上别说你懂Java8!!

    写在前面 最近,很多读者出去面试都在Java8上栽了跟头,事后自己分析,确实对Java8的新特性一知半解.然而,却在简历显眼的技能部分写着:熟练掌握Java8的各种新特性,能够迅速使用Java8开发高 ...

  3. 分形树Fractal tree介绍——具体如何结合TokuDB还没有太懂,先记住其和LSM都是一样的适合写密集

    在目前的Mysql数据库中,使用最广泛的是innodb存储引擎.innodb确实是个很不错的存储引擎,就连高性能Mysql里都说了,如果不是有什么很特别的要求,innodb就是最好的选择.当然,这偏文 ...

  4. 读懂 PetaLinux:让 Linux 在 Zynq 上轻松起“跑”(转)

    对于Zynq这样一个“ARM+可编程逻辑”异构处理系统我们已经不陌生,其创新性大家也有目共睹.不过想要让更多的应用享受到这一“创新”带来的红利,让其真正“落地”则需要大量系统性的工作,去营造一个完善的 ...

  5. fastjson简单使用demo,@JSONField注解属性字段上与set、get方法上。实体类toString(),实体类转json的区别;_下划线-减号大小写智能匹配

    一.demo代码 @JSONField注解属性字段上与set.get方法上.使用@Data注解(lombok插件安装最下方),对属性“笔名”[pseudonym]手动重写setter/getter方法 ...

  6. 先搞清楚这些问题,简历上再写你熟悉Java!

    原创声明 本文作者:黄小斜 转载请务必在文章开头注明出处和作者. 系列文章介绍 本文是<五分钟学Java>系列文章的一篇 本系列文章主要围绕Java程序员必须掌握的核心技能,结合我个人三年 ...

  7. flush方法和close方法的区别和字符输出流写数据的其他方法和字符输出流的续写和换行

    flush方法和close方法的区别 flush:刷新缓冲区,流对象可以继续使用 close:先刷新缓冲区,然后通知系统释放资源.刘对象不可以再被使用了. public class demo02 { ...

  8. 【offer收割机必备】我简历上的Java项目都好low,怎么办?

    这篇文章我们来聊一聊,在系统设计和项目经验这两块,应该如何充分的准备,才能拿出有技术含量的项目经验战胜跟你同台竞技的其他工程师,征服你的面试官,收获各种心仪的offer. (1)高级工程师必备:系统设 ...

  9. 不要再说我简历上Java项目都好low!【offer收割机必备】

    获取精品学习资料私信 欢迎加入QQ群架构华山论剑:836442475(大牛聚集地)一起交流学习探讨! 目录 高级工程师必备:系统设计能力 如何让你的项目更有技术含量 这篇文章我们继续来聊一聊,在系统设 ...

随机推荐

  1. css与javascript重难点,学前端,基础不好一切白费!

    JavaScript是一种属于网络的脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果.通常JavaScript脚本是通过嵌入在HTML中来实现 ...

  2. 个人对于flask中蓝图的理解

    什么是蓝图? 蓝图可以理解为,是一种对项目中的代码进行模块化管理的工具,相当于python中的包为什么要使用蓝图? 在一个py文件中具有多个功能代码,不利于维护和管理. 如果在其他的模块中去调用视图函 ...

  3. Idea自带插件Groovy无法创建和启动

    前言 如果现在有人要开始完全重写 Java,那么 Groovy 就像是 Java 2.0.Groovy 并没有取代 Java,而是作为 Java 的补充,它提供了更简单.更灵活的语法,可以在运行时动态 ...

  4. 微软全球资深副总裁对 VS Code 黑宝书的推荐序!VS Code 月活用户已达 1200 万!

    前不久,首本 VS Code 中文书终于问世了! 在本书出版之前,我很高兴能邀请到微软全球资深副总裁 Julia Liuson 为本书写推荐序!下面,我们就来看一下 Julia 所写的推荐序的完整内容 ...

  5. 【线型DP】CF1012C Hills 小山坡

    来了来了. 题目: 给你n个数,你一次操作可以把某一个数-1(可以减为负数),你的目标是使任意的k个数严格小于它旁边的两个数(第一个数只用严格小于第二个数,第n个数只用严格小于第n-1个数),问最少需 ...

  6. SpringBoot常用数据源配置

    #mysql8.X url: jdbc:mysql://localhost:3306/yourdbname?serverTimezone=UTC&useSSL=false&allowP ...

  7. HDFS读写流程(重点)

    @ 目录 一.写数据流程 举例: 二.异常写流程 读数据流程 一.写数据流程 ①服务端启动HDFS中的NN和DN进程 ②客户端创建一个分布式文件系统客户端,由客户端向NN发送请求,请求上传文件 ③NN ...

  8. day11 本日作业+周末作业

    目录 一.今日作业 1.编写文件copy工具 2.编写登录程序,账号密码来自于文件 3.编写注册程序,账号密码来存入文件 二.周末综合作业: 1.编写用户登录接口 2.编写程序实现用户注册后,可以登录 ...

  9. 技术干货丨通过wrap malloc定位C/C++的内存泄漏问题

    摘要:用C/C++开发的程序执行效率很高,但却经常受到内存泄漏的困扰.本文提供一种通过wrap malloc查找memory leak的思路. 用C/C++开发的程序执行效率很高,但却经常受到内存泄漏 ...

  10. 数据可视化之powerBI技巧(十二)学会这几个度量值,轻松获取前N名

    数据中的明细项一般都有很多,可是我们关注的往往只是前几名,所以在报表中只展示关注的部分,就十分常用. 有了上篇(这几个示例,帮你深入理解RANKX排名)关于排名的铺垫,仅显示前N名就简单多了. 依然以 ...