[享学Jackson] 二、jackson-core之流式API与JsonFactory、JsonGenerator、JsonParser

做难事必有所得

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/jackson-learning

前言

jackson-core是三大核心模块之一,并且它是核心中的核心,它提供了对JSON数据的完整支持
此模块提供了最具底层的Streaming JSON解析器/生成器,这组流式API属于Low-Level API,具有非常显著的特点:

  • 开销小,损耗小,性能极高
  • 因为是Low-Level API,所以灵活度极高
  • 又因为是Low-Level API,所以易错性高,可读性差

jackson-core模块提供了两种处理JSON的方式(整个Jackson一共3种):

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

本文将重点讲解流式API的使用:它是所有的三种方式中效率上最高的,当然也是最易出错、且最难使用的方式。

说明:树模型方式,将会放在和第三种处理方式:数据绑定 中一起讲述(数据绑定在另外一个核心模块jackson-databind中)


正文

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

对于一般的读写(99.99%情况),我们使用最简单的databind方式即可,这部分在专栏对应章节会作为重中之重进行讲解


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

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

Demo示例

下面将通过一个示例先看看效果,再基于此做更深入的探讨。


JsonGenerator 写

public static void main(String[] args) throws IOException {
    JsonFactory factory = new JsonFactory();

    // 此处最终输输出到OutputStreams输出流(此处输出到文件)
    JsonGenerator jsonGenerator = factory.createGenerator(new File("java-jackson/src/main/resources/person.json"), JsonEncoding.UTF8);
    jsonGenerator.writeStartObject(); //开始写,也就是这个符号 {

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

    // 写入Dog对象(枚举对象)
    jsonGenerator.writeFieldName("dog");
    jsonGenerator.writeStartObject();
    jsonGenerator.writeStringField("name", "旺财");
    jsonGenerator.writeStringField("color", "WHITE");
    jsonGenerator.writeEndObject();

    //写入一个数组格式
    jsonGenerator.writeFieldName("hobbies"); // "hobbies" :
    jsonGenerator.writeStartArray(); // [
    jsonGenerator.writeString("篮球"); // "篮球"
    jsonGenerator.writeString("football"); // "football"
    jsonGenerator.writeEndArray(); // ]

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

    // 关闭IO流
    jsonGenerator.close();
}

提示:源码地址请参见专栏里每篇文章的文首部分

执行此程序,最终会在resources目录下生成一个名为person.json的文件,内容如下(多次执行,效果是覆盖):
在这里插入图片描述
完完全全的手动档有木有,逐字逐句的都得我们手动“书写”,格式也都得手动来控制。所以说这么做的灵活度极大,效率极高,但相反的便是编码效率低、代码可读性差,易出错…


JsonParser 读

public static void main(String[] args) throws IOException {
    JsonFactory factory = new JsonFactory();

    // 此处InputStream来自于文件
    JsonParser jsonParser = factory.createParser(new File("java-jackson/src/main/resources/person.json"));

    // 只要还没到末尾,也就是}这个符号,就一直读取
    // {"name":"YourBatman","age":18,"dog":{"name":"旺财","color":"WHITE"},"hobbies":["篮球","football"]}
    JsonToken jsonToken = null; // token类型
    while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
        String fieldname = jsonParser.getCurrentName();
        if ("name".equals(fieldname)) {
            jsonToken = jsonParser.nextToken();
            System.out.println("==============token类型是:" + jsonToken);
            System.out.println(jsonParser.getText());
        } else if ("age".equals(fieldname)) {
            jsonToken = jsonParser.nextToken();
            System.out.println("==============token类型是:" + jsonToken);
            System.out.println(jsonParser.getIntValue());
        } else if ("dog".equals(fieldname)) {
            jsonToken = jsonParser.nextToken();
            System.out.println("==============token类型是:" + jsonToken);
            while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
                String dogFieldName = jsonParser.getCurrentName();
                if ("name".equals(dogFieldName)) {
                    jsonToken = jsonParser.nextToken();
                    System.out.println("======================token类型是:" + jsonToken);
                    System.out.println(jsonParser.getText());
                } else if ("color".equals(dogFieldName)) {
                    jsonToken = jsonParser.nextToken();
                    System.out.println("======================token类型是:" + jsonToken);
                    System.out.println(jsonParser.getText());
                }
            }
        } else if ("hobbies".equals(fieldname)) {
            jsonToken = jsonParser.nextToken();
            System.out.println("==============token类型是:" + jsonToken);
            while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
                System.out.println(jsonParser.getText());
            }
        }
    }

    // 关闭IO流
    jsonParser.close();
}

运行本程序,控制台里完美输出了各个属性的值:

==============token类型是:VALUE_STRING
YourBatman
==============token类型是:VALUE_NUMBER_INT
18
==============token类型是:START_OBJECT
======================token类型是:VALUE_STRING
旺财
======================token类型是:VALUE_STRING
WHITE
==============token类型是:START_ARRAY
篮球
football

核心API精讲

jackson-core内核模块里虽然有众多的类,但最为重要的只有如下3个:

  • JsonFactory:Jackson主要的工厂方法,用于配置和构建解析器(JsonParser)和生成器(JsonGenerator),这个工厂实例是线程安全的,所以可以重复使用
  • JsonGenerator:用来生成Json格式的内容的(序列化)
  • JsonParser:读取Json格式的内容(返序列化,必须是Json格式)

Demo案例中介绍了使用Streaming API完成最常规、最基本的读/写操作,下面针对于本案例涉及到的几个核心API进行分析和讲解


JsonFactory

Jackson的主要工厂类,用于 配置和构建 JsonGeneratorJsonParser,这个工厂实例是线程安全的,因此可以重复使用。该工厂最主要的方法截图展示如下:

创建JsonParser
在这里插入图片描述
创建JsonGenerator
在这里插入图片描述
关于本工厂的配置和构建,使用源代码 + 注释的方式展示如下:

public class JsonFactory ... {

	// 这三大Feature特征解析,会在专栏后面讲解特性配置章节中讲解
	protected final static int DEFAULT_FACTORY_FEATURE_FLAGS = JsonFactory.Feature.collectDefaults();
	protected final static int DEFAULT_PARSER_FEATURE_FLAGS = JsonParser.Feature.collectDefaults();
	protected final static int DEFAULT_GENERATOR_FEATURE_FLAGS = JsonGenerator.Feature.collectDefaults();

	...
	// 你可以对输入流、输出流进行干预
	// (这两个类均为抽象类,没有内置实现,若你有需要均需要你自己提供实现)
	protected InputDecorator _inputDecorator;
	protected OutputDecorator _outputDecorator;
	...

	// 构造器...
	// 99%情况下,使用空构造即可
	public JsonFactory() { this((ObjectCodec) null); }
	// ObjectCodec编码器并没有内置实现类。一般情况下我们不会自己去书写编码器
	// 提前透露只是:ObjectMapper便是一个(唯一一个)ObjectCodec实现类,它实现了自动绑定
	public JsonFactory(ObjectCodec oc) { ... }
	// 2.10版本新提供的基于Builder模式的构造方式,方便你进行配置
	public JsonFactory(JsonFactoryBuilder b) { ... }
	...

	// 2.10新增静态方法:builder模式的体现(JsonFactoryBuilder是2.10版本新增的类)
    public static TSFBuilder<?,?> builder() {
        return new JsonFactoryBuilder();
    }
    ...
    
    // 拷贝出一个新的实例
	public JsonFactory copy() { ... }
	
	// 默认输出的字段无序。
	// 若你是CSV或者Avro这种对字段顺序有要求的格式。请继承此类后然后把此属性改为true
    @Override
    public boolean requiresPropertyOrdering() { return false; }
    @Override
    public boolean canHandleBinaryNatively() { return false; }
    public boolean canUseCharArrays() { return true; }
    // 版本信息
    @Override
    public Version version() {
        return PackageVersion.VERSION;
    }

	... // Configuration, factory features/parser features/generator features
}

该工厂的API整体上非常简单,JsonGeneratorJsonParser并不能自己new实例,而只能是通过这个工厂去创建,因此建议掌握本工厂的相关配置方法。


SPI(ServiceLoader)方式创建实例

上面介绍,JsonFactory实例一般通过new构造函数的方式来创建一个工厂实例。作为如此优秀的Jackson库,自然考虑到了我们可能会有希望自己扩展JsonFactory的需求,因此它还提供了一种更具弹性的SPI方式来创建工厂实例:允许我们通过配置文件的形式来动态调整使用的具体工厂

说明:关于SPI是何意思,以及JDK提供的ServiceLoader机制到底怎么玩,建议还不清楚的同学可以先补补课(我的免费博文里也有详细讲述,出门左拐)

基于SPI方式,上面Demo中创建JsonFactory实例的方式可以改为:

public static void main(String[] args) throws IOException {
    // 1、直接new的方式
    // JsonFactory factory = new JsonFactory();
    // 2、更具弹性的SPI方式
    JsonFactory factory = null;
    ServiceLoader<JsonFactory> load = ServiceLoader.load(JsonFactory.class);
    Iterator<JsonFactory> it = load.iterator();
    if (it.hasNext()) { // 此处是if不是while,因为我只需要一个而已
        factory = it.next();
    }
    ...
}

这样能正常运行,是因为jackson-core这个包它内置了SPI的配置,如下截图:
在这里插入图片描述

使用自定义的JsonFactory

public class MyJsonFactory extends JsonFactory {
    public MyJsonFactory(){
        System.out.println("我是自定义的JsonFactory");
    }
}

写个ServiceLoader的配置文件:
在这里插入图片描述
运行如上程序,可以看见控制台上有输出:

我是自定义的JsonFactory
...

由此可见我自定义的MyJsonFactory最终被使用了,生效喽。
其实ServiceLoader.load()的时候把MyJsonFactory和系统内置的JsonFactory都加载到了,只是最终只实例化了我的,这是由加载配置文件的顺序决定的,而这种顺序往往是不可控的~

因此需要注意:ServiceLoader它不像SpringFactoriesLoader那样强大可以通过Order自己管理排序,so在实际使用中请务必做好相应的处理。

小建议:在实际代码书写中,若你想创建工厂实例,建议使用SPI方式,这样能让你的程序变得更富弹性


JsonToken

上面介绍了Streaming API中Token的含义,然而jackson-core里也提供了这样一个枚举类,枚举出了用于返回结果的一些token类型(也就是说当你遇上这些类型的token的时候,就可以去获取结果了)。

public enum JsonToken {

	START_OBJECT("{", JsonTokenId.ID_START_OBJECT),
	END_OBJECT("}", JsonTokenId.ID_END_OBJECT),
	START_ARRAY("[", JsonTokenId.ID_START_ARRAY),
	END_ARRAY("]", JsonTokenId.ID_END_ARRAY),
		
	...
	
	// 值的token类型
	// 嵌入式的Object,表示raw Object
	VALUE_EMBEDDED_OBJECT(null, JsonTokenId.ID_EMBEDDED_OBJECT),
	VALUE_STRING(null, JsonTokenId.ID_STRING),
	VALUE_NUMBER_INT(null, JsonTokenId.ID_NUMBER_INT),
	VALUE_NUMBER_FLOAT(null, JsonTokenId.ID_NUMBER_FLOAT),
	VALUE_TRUE("true", JsonTokenId.ID_TRUE),
	VALUE_FALSE("false", JsonTokenId.ID_FALSE),
	VALUE_NULL("null", JsonTokenId.ID_NULL);
}

JsonGenerator

它是用于writing JSON content的基类(抽象类),因为创建它的实例使用的是JsonFactory工厂,因此我们无需关心具体实现类,只需了解此基类的API即可。

public abstract class JsonGenerator... {

	// 用于漂亮格式的输出:便于人阅读。默认它是null:紧凑型输出
	protected PrettyPrinter _cfgPrettyPrinter;
	protected JsonGenerator() { }
    @Override
    public abstract Version version();
    public JsonGenerator setPrettyPrinter(PrettyPrinter pp) { ... }
    public abstract JsonGenerator useDefaultPrettyPrinter();

	... // Feature configuration特性相关配置,在专栏特性文章里会详细介绍

	// 获取当前值。它只用于高级数据绑定功能
	public Object getCurrentValue() { ... }
	public void setCurrentValue(Object v) { ... }
}

以上是此生成器的配置部分的源码说明,接下来便是它提供的写能力的核心API了(截图方式展示,更加的直观):

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这些便是它最为重要的基础性API,使用起来没有难度,Demo示例中有相关代码展示。

约定:为了不显得文章过于臃肿,出现本末倒置现象而重点不突出,本文包括后续文章像这种基础性API的使用就不会给出相关示例,有任何疑问的可以留言~


write方法说明

从API中可以看出,这种Low-Level的API是只能写基本类型的:如int、long、BigInteger…对于对象类型如Date、Person等,它都是不能直接写的。

虽然JsonGenerator有个writeObject(Object pojo)方法,但默认情况下仅是调用了toString()方法而已,意义并不大。若想要良好的格式输出,需要自定义ObjectCodec的实现~

所以说,针对于平时常说的Date/LocalDate类型是写为时间戳还是指定格式,这都是针对于高层API也就是ObjectMapper而言的,毕竟它自己就是个ObjectCodec嘛,所以实现了编码/解码过程。
而Streaming API只提供最为底层的、最为原子的方法,只有这样才能有最大的灵活性以及保证极致的性能

JsonParser

定义了一组API用于reading JSON content。它的实例也只能由工厂创建,所以也只需关心此基类的相关API即可:

public abstract class JsonParser ... {

	private final static int MIN_BYTE_I = (int) Byte.MIN_VALUE; //-2的7次方 = -128
	// 注意Byte.MAX_VALUE的最大值是127.而根据[JACKSON-804]规定把它扩大到了255(含)
	private final static int MAX_BYTE_I = (int) 255;
    private final static int MIN_SHORT_I = (int) Short.MIN_VALUE; // -2的15次方 = 32768
    private final static int MAX_SHORT_I = (int) Short.MAX_VALUE; // 2的15次方-1 = 32767

	// 内建支持的数字类型
    public enum NumberType {
        INT, LONG, BIG_INTEGER, FLOAT, DOUBLE, BIG_DECIMAL
    };

	...
	public Object getCurrentValue() { ... }
	public void setCurrentValue(Object v) { ... }
	@Override
    public abstract Version version();

	// 2.9提供了NIO的支持。默认是没有开启的(向下兼容)
	public boolean canParseAsync() { return false; }
    public NonBlockingInputFeeder getNonBlockingInputFeeder() {
        return null;
    }

	// 获取解析的当前上下文
	public abstract JsonStreamContext getParsingContext();
	// 关于token的操作(重要)
    public JsonToken currentToken() { return getCurrentToken(); }
    public int currentTokenId() { return getCurrentTokenId(); }
    public abstract JsonToken getCurrentToken();
    public abstract int getCurrentTokenId();
    public abstract boolean hasTokenId(int id);
    public abstract boolean hasToken(JsonToken t);
    public boolean isExpectedStartArrayToken() { return currentToken() == JsonToken.START_ARRAY; }
    public boolean isExpectedStartObjectToken() { return currentToken() == JsonToken.START_OBJECT; }
    public abstract void clearCurrentToken();
    public abstract JsonToken getLastClearedToken();
    public abstract String getCurrentName() throws IOException;
    public String currentName() throws IOException { return getCurrentName(); }

	// 输入的第一个字符的位置
	public abstract JsonLocation getTokenLocation();
	// 该方法返回最后处理的字符的位置(一般用于error的时候打印日志)
	public abstract JsonLocation getCurrentLocation();
	// 这是最主要的迭代方法。它将推进流来确定下一个令牌的类型(如果有的话),若没有下一个了就返回null
	public abstract JsonToken nextToken() throws IOException;
	// 迭代方法,获取下一个值类型
	public abstract JsonToken nextValue() throws IOException;
	// 简答的说就是调用nextToken,然后判断这个token是否是JsonToken.FIELD_NAME类型;
	// 并且getCurrentName()名称还和你指定的名称一样才会返回true
	public boolean nextFieldName(SerializableString str) throws IOException { ... }
	// 不解释
    public String nextFieldName() throws IOException {
        return (nextToken() == JsonToken.FIELD_NAME) ? getCurrentName() : null;
    }

	// nextToken + 获取值的 组合方法系列
    public String nextTextValue() throws IOException {
        return (nextToken() == JsonToken.VALUE_STRING) ? getText() : null;
    }
    public int nextIntValue(int defaultValue) throws IOException {
        return (nextToken() == JsonToken.VALUE_NUMBER_INT) ? getIntValue() : defaultValue;
    }
   	... // 省略Long、Bool类型的组合方法

	// 该方法将跳过数组或的所有子标记当前指的对象
	public abstract JsonParser skipChildren() throws IOException;
	public void finishToken() throws IOException { ... }
    public boolean isNaN() throws IOException {
        return false;
    }
}

相较于JsonGenerator而言,它的API相对繁多。这是很容易理解的,毕竟反序列化一般都是比序列化麻烦很多的。
同样的,为了更直观的分类展示出核心API,下面还是以图示的方式列出:
在这里插入图片描述
在这里插入图片描述
这组数据读取API中,稍微不好理解的几个方法为:readValueAsXXX系列方法。为了扫清困惑,下面专门针对它们附加一个示例以辅助理解


readValueAs()系列方法Demo示例

该方法将JSON内容反序列化为非容器类型(但可以是数组类型),通常是一个bean,一个数组或包装器类型(如Boolean)。Note:该系列方法依赖于ObjectCodec对象编码/解码器

自定义实现一个ObjectCodec:

// 由于ObjectCodec需要实现的方法过多。本处只以一个实现为基准,各位举一反三即可
public class MyObjectCodec extends ObjectCodec {

    @Override
    public <T> T readValue(JsonParser jsonParser, Class<T> valueType) throws IOException {
        // {"name":"YourBatman","age":18,"dog":{"name":"旺财","color":"WHITE"},"hobbies":["篮球","football"]}
        // 使用JsonParser读取此json串,并且封装到valueType类型的JavaBean里
        // 重点说明:次数实例理应通过valueType来构造,且赋值方面大量用到反射。
        // 但本文仅想说明本质,因此不相关的步骤不在此处列出,各位知道便可
        Person person = new Person();

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("name".equals(fieldname)) {
                person.setName(jsonParser.nextTextValue());
            } else if ("age".equals(fieldname)) {
                person.setAge(jsonParser.nextIntValue(0));
            } else if ("dog".equals(fieldname)) {
                jsonParser.nextToken();
                // 构造一个dog实例(同样的,实际场景是利用反射构造的哦)
                Dog dog = new Dog();
                person.setDog(dog);

                while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
                    String dogFieldName = jsonParser.getCurrentName();
                    if ("name".equals(dogFieldName)) {
                        dog.setName(jsonParser.nextTextValue());
                    } else if ("color".equals(dogFieldName)) {
                        dog.setColor(Color.valueOf(jsonParser.nextTextValue()));
                    }
                }
            } else if ("hobbies".equals(fieldname)) {
                jsonParser.nextToken();

                List<String> hobbies = new ArrayList<>();
                person.setHobbies(hobbies);
                while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
                    // hobbies.add(jsonParser.nextTextValue()); // 请注意:此处不能用next哦~
                    hobbies.add(jsonParser.getText());
                }
            }
        }
        return (T) person;
    }

    @Override
    public Version version() {
        return PackageVersion.VERSION;
    }

    // ... 省略其它方法
}

测试用例如下,它最关键的一句代码为jsonParser.setCodec(new MyObjectCodec()),从而写的操作便委托给了对象编码/解码器去实现喽:

public static void main(String[] args) throws IOException {
    JsonFactory factory = new JsonFactory();

    // 此处InputStream来自于文件
    JsonParser jsonParser = factory.createParser(new File("java-jackson/src/main/resources/person.json"));
    jsonParser.setCodec(new MyObjectCodec()); // 若使用readValueAs系列方法,必须指定解码器

    Person person = jsonParser.readValueAs(Person.class);
    System.out.println(person);

    // 关闭IO流
    jsonParser.close();
}

运行程序,控制台完美输出:

Person(name=YourBatman, age=18, hobbies=[篮球, football], dog=Dog(name=旺财, color=WHITE))

注意:如果返回类型是List或者Map时,请不要使用readValueAs(Class<T> valueType)方法,原因是集合类型泛型类型会被擦除从而无法自省,推荐使用readValueAs(TypeReference<?> valueTypeRef)方法来处理

说明:专栏后面重点介绍的ObjectMapper,它就是ObjectCodec的(唯一)实现类,它的基础原理便来源于此

总结

本文介绍了jackson-core模块的流式API的使用,它作为JSON处理的基石,虽然极力不推荐直接使用,但这并不影响它的重要程度和地位。

对于流式API,虽然它在性能上有所特长,但是通过上面的Demo示例也可以知道:每一个 token都得自己增量处理(全手动档),换句话说,coder必须要非常小心地显示的处理每个token,这是很要命的,因为很可能会因为粗心导致丢掉/写错一些字符。而且这种方式书写的代码简洁性很差,可读性也不好,而且还得自己close流。因此,在不到需要考虑极致性能的时候,一定一定不要使用这种方式去操作JSON哦。

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

分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了377 篇原创文章 · 获赞 572 · 访问量 51万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie

分享到微信朋友圈

×

扫一扫,手机浏览

应支付19.90元
点击重新获取
扫码支付

支付成功即可阅读