百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

我的天,Jackson原来是这样写JSON的

ztj100 2025-02-15 18:23 15 浏览 0 评论

文章很长,文末有福利的

前言

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

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

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


命名为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吧~


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

版本约定

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

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

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

工程结构

鉴于是首次展示工程示例代码,将基本结构展示如下:



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

正文

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

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

  • 流式(Streaming):此概念和Java8中的Stream流是不同的。这里指的是IO流,因此具有最低的开销和最快的读/写操作(记得关流哦)
  • 增量模式(incremental mode):它表示每个部分一个一个地往上增加,类似于垒砖。使用此流式API读写JSON的方式使用的均是增量模式
  • JsonToken:每一部分都是一个独立的Token(有不同类型的Token),最终被“拼凑”起来就是一个JSON。这是流式API里很重要的一个抽象概念。

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



本文将看看它是如何写JSON数据的,也就是JsonGenerator。


JsonGenerator使用Demo

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

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

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

Bash

@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优雅关闭资源(这也是推荐的使用方式),代码改造如下:

Bash

@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的赛车一样,先要速度、高性能、灵活性,那必须上手动档。


JsonGenerator详细介绍

JsonGenerator是个抽象类,它的继承体系如下:


  • 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两大阶段。


写JSON Key

JsonGenerator一共提供了3个方法用于写JSON的key:



@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在写入时起到一个桥梁(适配)作用:


下面针对不同的Value类型分别作出API讲解,给出示例说明。在此之前,请先记住两个结论,会更有利于你理解示例:


  • JSON的顺序,和你write的顺序保持一致
  • 写任何类型的Value之前请记得先write写key,否则可能无效

字符串



可把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"}

数字



参考上例,不解释。


对象(JSON 对象)




@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的闭环思路。


如何向数组里写入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也很暖心的为此提供了专用方法(可以调用该方法来一次性便捷的写入单个数组):



@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提供了二合一的组合方法,一个顶两:



@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片,效率高。


其它写方法

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

writeRaw()和writeRawValue()


该方法将强制生成器不做任何修改地逐字复制输入文本(包括不进行转义,也不添加分隔符,即使上下文[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()


使用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}

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

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模块的树模型没有意义,所以这里先卖个关子,保持好奇心哈

思考题

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

总结

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

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

喜欢的可以关注加转发私信我可以赠送你们资料,都是我收集的

相关推荐

Python 操作excel的坑__真实的行和列

大佬给的建议__如何快速处理excelopenpyxl库操作excel的时候,单个表的数据量大一些处理速度还能接受,如果涉及多个表甚至多个excel文件的时候速度会很慢,还是建议用pandas来处理,...

Python os.path模块使用指南:轻松处理文件路径

前言在Python编程中,文件和目录的操作是非常重要的一部分。为了方便用户进行文件和目录的操作,Python标准库提供了os模块。其中,os.path子模块提供了一些处理文件路径的函数和方法。本文主要...

Python常用内置模块介绍——文件与系统操作详解

Python提供了多个强大的内置模块用于文件和系统操作,下面我将详细介绍最常用的几个模块及其核心功能。1.os模块-操作系统交互...

Python Flask 建站框架实操教程(flask框架网页)

下面我将带您从零开始构建一个完整的Flask网站,包含用户认证、数据库操作和前端模板等核心功能。##第一部分:基础项目搭建###1.创建项目环境```bash...

为你的python程序上锁:软件序列号生成器

序列号很多同学可能开发了非常多的程序了,并且进行了...

PO设计模式全攻略,在 UI 自动化中的实践总结(以企业微信为例)

一、什么是PO设计模式?PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成一个Page类,即一个py文件,并以页面为单位来写测试用例,实现页面对象和测试用例的...

这种小工具居然也能在某鱼卖钱?我用Python一天能写...

前两天在某鱼闲逛,本来想找个二手机械键盘,结果刷着刷着突然看到有人在卖——Word批量转PDF小工具...

python打包成exe,程序有图标,但是任务栏和窗口都没有显示图标

代码中指定图标信息#设置应用ID,确保任务栏图标正确显示ifsys.platform=="win32":importctypesapp_id=...

使用Python构建电影推荐系统(用python做推荐系统)

在日常数据挖掘工作中,除了会涉及到使用Python处理分类或预测任务,有时候还会涉及推荐系统相关任务。...

python爬取并分析淘宝商品信息(python爬取淘宝商品数据)

python爬取并分析淘宝商品信息背景介绍一、模拟登陆二、爬取商品信息1.定义相关参数2.分析并定义正则3.数据爬取三、简单数据分析1.导入库2.中文显示3.读取数据4.分析价格分布5.分析销售...

OpenCV入门学习基础教程(从小白变大神)

Opencv是用于快速处理图像处理、计算机视觉问题的工具,支持多种语言进行开发如c++、python、java等,下面这篇文章主要给大家介绍了关于openCV入门学习基础教程的相关资料,需要的朋友可以...

python图像处理-一行代码实现灰度图抠图

抠图是ps的最基本技能,利用python可以实现用一行代码实现灰度图抠图。基础算法是...

从头开始学python:如何用Matplotlib绘图表

Matplotlib是一个用于绘制图表的库。如果你有用过python处理数据,那Matplotlib可以更直观的帮你把数据展示出来。直接上代码看例子:importmatplotlib.pyplot...

Python爬取爱奇艺腾讯视频 250,000 条数据分析为什么李诞不值得了

在《Python爬取爱奇艺52432条数据分析谁才是《奇葩说》的焦点人物?》这篇文章中,我们从爱奇艺爬取了5万多条评论数据,并对一些关键数据进行了分析,由此总结出了一些明面上看不到的数据,并...

Python Matplotlib 库使用基本指南

简介Matplotlib是一个广泛使用的Python数据可视化库,它可以创建各种类型的图表、图形和可视化效果。无论是简单的折线图还是复杂的热力图,Matplotlib提供了丰富的功能来满足我们...

取消回复欢迎 发表评论: