JsonObject和JsonArray

  前文讲了JSON数据格式,本章主要讲解Vert.x中处理JSON的两个核心类:io.vertx.core.json.JsonObjectio.vertx.core.json.JsonArray,Vert.x内置使用了jackson的JSON库,在整个工具箱中大部分地方都用了JSON作为内部数据格式,本章除了讲解这两个类的用法,还会带着读者去阅读部分源代码,理解这两个类的内部结构。

  • io.vertx.core.json.JsonObject处理JSON对象。
  • io.vertx.core.json.JsonArray处理JSON数组。

1. JsonObject

  JSON的核心数据格式包含:键值对(Json Object,下统称JSON对象)和有序元素集合(Json Array,下统称JSON数组),Vert.x中使用io.vertx.core.json.JsonObject类处理JSON对象。

1.1. 构造

  在理解JsonObject之前先看一段代码:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;

public class JObjectInit {

    public static void main(final String[] args) {
        /*
         * 字面量初始化
         * */
        final String literal = "{\"name\":\"Lang\"}";
        final JsonObject data = new JsonObject(literal);
        System.out.println(data.encodePrettily());
    }
}

  上述代码会输出:

{
    "name" : "Lang"
}

  代码中演示了JsonObject对象的构造方式,它总共有四个构造函数:

  • JsonObject():直接构造一个空对象,输出为:{}
  • JsonObject(String):传入一个java.lang.String对象,根据字面量解析生成合法的JSON对象。
  • JsonObject(Map<String,Object>):传入一个java.util.Map对象,键类型为java.lang.String,值类型为java.lang.Object
  • JsonObject(Buffer):传入一个io.vertx.core.buffer.Buffer对象,构造一个JSON对象。

  示例代码中使用字符串字面量构造JSON对象,这种方式在实际开发过程中比较常见,而JsonObject内部使用了java.lang.LinkedHashMap<String,Object>的数据结构来存储数据,它的键值对类型描述是:String = Object的格式。理论上它是可以存储任意格式的值(因为值类型是java.lang.Object),但由于本身遵循JSON的格式规范,所以值格式和Java语言中的Object有所区别,若值的格式非法,那么将会抛出对应的异常信息。

  这里补充一段代码来说明四种构造方式:

package io.vertx.up._02.json;

import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;

import java.util.HashMap;
import java.util.Map;

public class JObjectInit {

    public static void main(final String[] args) {
        /*
         * 空的JSON对象
         */
        final JsonObject empty = new JsonObject();
        System.out.println(empty.encodePrettily());
        /*
         * 字面量初始化
         */
        final String literal = "{\"age\":18,\"name\":\"Lang\"}";
        final JsonObject strData = new JsonObject(literal);
        System.out.println(strData.encodePrettily());
        /*
         * 使用Map初始化
         */
        final Map<String, Object> map = new HashMap<String, Object>() {
            {
                this.put("email", "lang.yu@hpe.com");
                this.put("age", 18);
            }
        };
        final JsonObject mapData = new JsonObject(map);
        System.out.println(mapData.encodePrettily());
        /*
         * 使用Buffer初始化
         */
        final Buffer buffer = Buffer.buffer(literal);
        final JsonObject bufferData = new JsonObject(buffer);
        System.out.println(bufferData.encodePrettily());
    }
}

  不论使用哪种方式构造JsonObject,对象内部都会执行“解析”(第一种构造空JSON对象除外),若提供的格式非法,那么在解析时就会报错。最后一种方式对读者而言比较陌生——它使用了Vert.x中定义的io.vertx.core.buffer.Buffer类型作为参数,后边我们会对这种类型深入说明,此处就不重复。

1.2. 格式

  io.vertx.core.json.JsonObject遵循Json的格式规范,它对Json格式的支持将达到哪种程度呢?本章将带领读者从Json格式的“视角”去理解JsonObject对象,一旦理解后,在程序编写过程中,您就可以写出更加“健壮”的代码。当您使用别人定义的数据结构时,若不了解它的细节,写出来的代码往往会有不可预知的“副作用”,如JsonObject在构造时内部会执行Json格式的解析,失败则会抛出异常,而在真实场景下,我们更期望的是:解析失败得到一个空对象默认值({})。

  在此前提下,您就需要更加深入了解JsonObject这个类——至少要清楚在什么情况下会出现解析失败!回答这个问题之前,还是先看一段代码:

package io.vertx.up._02.json;

import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;

public class JObjectError {

    public static void main(final String[] args) {
        // 键不使用引号(JavaScript)常见
        final String script = "{name:\"Lang\"}";
        out(script);

        // 键使用单引号
        final String singleScript = "{'name':\"Lang\"}";
        out(singleScript);

        // 值使用单引号
        final String singleValue = "{\"name\":'name'}";
        out(singleValue);

        // 值无引号
        final String emptyValue = "{\"name\":name}";
        out(emptyValue);

        // 布尔大小写敏感
        final String boolValue = "{\"name\":TRUE}";
        out(boolValue);

        // null字面量异常
        final String nullValue = "{\"name\":Null}";
        out(nullValue);

        // 数值 Java 格式
        final String numberValue = "{\"name\":13L}";
        out(numberValue);
    }

    private static void out(final String literal) {
        try {
            final JsonObject data = new JsonObject(literal);
            System.out.println(data.encodePrettily());
        } catch (final DecodeException ex) {
            System.out.println("解析异常:" + literal);
        }
    }
}

  直接运行上述代码会得到下边的输出:

解析异常:{name:"Lang"}
解析异常:{'name':"Lang"}
解析异常:{"name":'name'}
解析异常:{"name":name}
解析异常:{"name":TRUE}
解析异常:{"name":Null}
解析异常:{"name":13L}

  如果打印解析失败的堆栈信息,会得到Vert.x中定义的专用异常:io.vertx.core.json.DecodeException,其输出如下:

Exception in thread "main" io.vertx.core.json.DecodeException: Failed to decode: Unrecognized token 'test': was expecting 'null', 'true', 'false' or NaN
 at [Source: (String)"test"; line: 1, column: 9]
    at io.vertx.core.json.Json.decodeValue(Json.java:124)
    at io.vertx.core.json.JsonObject.fromJson(JsonObject.java:956)
    at io.vertx.core.json.JsonObject.<init>(JsonObject.java:48)

  上述概念代码已经将大部分非法格式的Json数据测试了,这里就不一一列举,而是分享一些实际开发过程中的心得:

  1. 做过JavaScript开发的读者,通常会在写Json时不严谨,最常见的情况就是不带双引号或者使用单引号,从代码中的测试可知,Vert.x中的JsonObject在使用时,只支持双引号,格式中只有字符串格式支持且仅支持双引号。
  2. Java中通常会使用后缀法来写一些特殊的数值,如long型可以使用2L,而float类型可以使用1.1f,这些格式同样不符合Json数据规范,Json对数值格式的支持仅限于标准整数、浮点数和科学计数法,其他格式都是非法的。
  3. 对布尔类型的truefalse,以及空指针null,在Json格式中是大小写敏感的,Json数据格式中仅支持小写,其他所有的组合也是非法的——并且需要注意这三种格式不能添加双引号,如果使用了双引号,那么解析的值会转变成java.lang.String类型,而不是我们所期望的布尔类型或null。

  书写合法的Json数据是使用JsonObject的基础,如果开发人员不掌握这些知识,往往会因为一些小的数据格式问题,使程序变得不够健壮。是的,我们在使用JsonObject对象时,尽可能考虑到程序期望的输出结果,而不是直接使用一行:final JsonObject data = new JsonObject(literal)就完事,只有掌握了合法Json数据的写法,那么您在编写程序过程中才不会因小失大。

  本章一开始就提到了字面量(Literal),究竟什么是字面量?

  在编程语言中,字面量(literal)指的是源代码中直接表示的一个固定的值,除了我们直接用肉眼可以辨识的字面量以外,不同的语言对字面量的“表示”还会存在一定的区别。几乎所有的计算机编程语言都具有对基本值的字面量有表示,通常出现于整数、浮点数、字符串、布尔值等,字面量表示法又称为标记法(notation)。

  看看下边代码,理解Java语言中的字面量表示法:

package io.vertx.up._02.json;

public class LiteralNotation {

    public static void main(final String[] args) {
        // 布尔字面量
        final Boolean bool = true;

        // 字符串字面量
        final String str = "Hello";

        // 字符字面量(Json非法)
        final char character = 'Y';

        // 字节字面量(Json无字节类型)
        final byte bytes = 12;

        // 整数字面量(Json中只支持十进制)
        final int number10 = 10;        // 十进制
        final int number16 = 0x10;      // 十六进制
        final int number8 = 012;        // 八进制

        // 浮点数字面量(Json中不支持)
        final float float1 = 1.1f;

        // 长整(Json中不支持)
        final long long1 = 1L;

        // 短整:Java中短整和整数同字面量(隐式转换)
        final short short1 = 2;
    }
}

  上述代码中不仅枚举了Java语言里的字面量表示法,还对比备注了和Json数据格式的对应关系,相信读者看到这里,对字面量和JsonObject所支持的数据格式就有所理解了,接下来深入看看JsonObject在实际开发过程中的读写

1.3. 值读写

  JsonObject描述的是JSON对象,设置JSON对象的键值对称为操作,根据某个键去读取JSON对象中的值称为操作,它提供了一系列的put/get的方法。下边的表格列举了不同数据类型对应的put/get方法:

数据类型 put方法 get方法
null值 putNull(String)
布尔值 put(String, Boolean) getBoolean(String)
字节数组 put(String, byte[]) getBinary(String)
双精度浮点数 put(String, Double) getDouble(String)
单精度浮点数 put(String, Float) getFloat(String)
枚举值 put(String, Enum)
时间类型 put(String, Instant) getInstant(String)
整数 put(String, Integer) getInteger(String)
长整数 put(String, Long) getLong(String)
字符串 put(String, String) 或 put(String,CharSequence) getString(String)
JSON对象 put(String, JsonObject) getJsonObject(String)
JSON数组 put(String, JsonArray) getJsonArray(String)
Object对象 put(String, Object) getValue(String)

  上边表格列举了JsonObject对象中所有读写的方法,除了上边提到的方法以外,有几点需要注意:

  • 字符串的put方法有两种输入格式,除了java.lang.String以外,还支持CharSequence(接口)类型。
  • 所有的get方法除了表格中定义的单参数方法,还包含另一种重载格式:get(String, X),这里的第二个参数X表示读取不到数据时(读取的值为null)的默认值。
  • JsonObject有一个特殊的getMap方法,可以直接拿到LinkedHashMap引用,引用类型是:Map<String,Object>

  接下来看下边的代码:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;

import java.util.Date;

enum StringType {
    TEST1, TEST2
}

public class JObjectSet {
    public static void main(final String[] args) {
        /* 枚举类型 */
        final JsonObject data = new JsonObject();
        data.put("enum", StringType.TEST1);
        /* CharSequence */
        final CharSequence sequence = "Hello World";
        data.put("str1", sequence);
        System.out.println(data.encodePrettily());
        /* Error:Date类型 */
        final Date date = new Date();
        data.put("date", date);
    }
}

  运行上述代码会看到输出:

{
  "enum" : "TEST1",
  "str1" : "Hello World"
}
Exception in thread "main" java.lang.IllegalStateException: Illegal type in JsonObject: class java.util.Date
    at io.vertx.core.json.Json.checkAndCopy(Json.java:265)
    at io.vertx.core.json.JsonObject.put(JsonObject.java:684)
    at io.vertx.up._02.json.JObjectSet.main(JObjectSet.java:22)

  前两个put方法的调用很容易理解,因为JsonObject类中定义了确切类型put方法,而最后一个put会调用put(String,Object)方法,这种情况下,并不是所有的类型都是它支持的,也就是说并不是所有的java.lang.Object的子类型都是JsonObject支持的类型。如果让读者一一去尝试哪些类型是它所支持的,无疑这个过程将会是一场噩梦,最直接的方式是看内部方法checkAndCopy的源代码:

  static Object checkAndCopy(Object val, boolean copy) {
    if (val == null) {
      // OK
    } else if (val instanceof Number && !(val instanceof BigDecimal)) {
      // OK
    } else if (val instanceof Boolean) {
      // OK
    } else if (val instanceof String) {
      // OK
    } else if (val instanceof Character) {
      // OK
    } else if (val instanceof CharSequence) {
      val = val.toString();
    } else if (val instanceof JsonObject) {
      if (copy) {
        val = ((JsonObject) val).copy();
      }
    } else if (val instanceof JsonArray) {
      if (copy) {
        val = ((JsonArray) val).copy();
      }
    } else if (val instanceof Map) {
      if (copy) {
        val = (new JsonObject((Map)val)).copy();
      } else {
        val = new JsonObject((Map)val);
      }
    } else if (val instanceof List) {
      if (copy) {
        val = (new JsonArray((List)val)).copy();
      } else {
        val = new JsonArray((List)val);
      }
    } else if (val instanceof byte[]) {
      val = Base64.getEncoder().encodeToString((byte[])val);
    } else if (val instanceof Instant) {
      val = ISO_INSTANT.format((Instant) val);
    } else {
      throw new IllegalStateException("Illegal type in JsonObject: " + val.getClass());
    }
    return val;
  }

  看了源代码后,读者就可以彻底理解put(String,Object)的内部逻辑,它支持的Java对象类型主要包含:

  • null引用
  • 除开java.math.BigDecimaljava.lang.Number的子类型
  • java.lang.Boolean
  • java.lang.String
  • java.lang.Character
  • 实现了java.lang.CharSequence接口的类型
  • io.vertx.core.json.JsonObject
  • io.vertx.core.json.JsonArray
  • 实现了java.util.Map接口的类型
  • 实现了java.util.List接口的类型
  • byte[]
  • java.time.Instant

  除此之外,其他类型的Java对象就会抛出上述示例代码中的异常,也就是说自定义的Java对象不是JsonObject支持的类型,这种情况下不可以使用put(String, Object)方法来写数据。如果要在JsonObject对象中写入自定义对象数据,先把自定义对象转换成JsonObject对象,然后使用put(String, JsonObject)方法写数据。

  接下来看看JsonObject数据操作,JsonObject的读数据操作内部使用了()类型的强制转换而不是解析,并且它处理数据时使用的是Java语言中的封装类型(Wrapper/In-Box Type)而不是原生类型,如果类型之间不能执行强制转换,那么读取数据就会失败。先看下边代码:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;

public class JObjectGet {
    public static void main(final String[] args) {
        final JsonObject json = new JsonObject();
        json.put("result", "true");
        // java.lang.String cannot be cast to java.lang.Boolean
        final Boolean result = json.getBoolean("result");
    }
}

  上边代码演示了开发过程中的常见错误,运行它会得到下边输出:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Boolean
    at io.vertx.core.json.JsonObject.getBoolean(JsonObject.java:207)
    at io.vertx.up._02.json.JObjectGet.main(JObjectGet.java:9)

  您也许不小心使用了字符串类型的true来表示结果,而这个结果本身无法转换成java.lang.Boolean类型,所以当您调用getBoolean时,这个异常就会抛出来——往往这样的异常还会出现于另一种比较隐晦的情况:序列化/反序列化过程。假设代码需要从某个Json文件去读取数据:

{
    "age": "22",
    "gender": "false"
}

  试着运行下边代码会有什么输出呢?

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;
import io.vertx.up.util.Ut;

public class JObjectGet2 {
    public static void main(final String[] args) {
        /*
         * 从文件中读取内容转换成JsonObject
         * 示例中引用了: cn.vertxup:vertx-core:0.5-SNAPSHOT
         **/
        final JsonObject data = Ut.ioJObject("data/input.json");
        final Integer age = data.getInteger("age");
    }
}

  运行上边代码会遇到同样的转型异常:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
    at io.vertx.core.json.JsonObject.getInteger(JsonObject.java:131)
    at io.vertx.up._02.json.JObjectGet2.main(JObjectGet2.java:9)

  如果读者直接查看JsonObject的源代码,整个问题就一目了然了,下边是getInteger的源代码:

  public Integer getInteger(String key) {
    Objects.requireNonNull(key);
    Number number = (Number)map.get(key);
    if (number == null) {
      return null;
    } else if (number instanceof Integer) {
      return (Integer)number;  // Avoids unnecessary unbox/box
    } else {
      return number.intValue();
    }
  }

  上述两段代码都演示JsonObject在读数据过程中遇到的转型错误,其实真正开发过程第一种情况类似json.put("result","true")并不多见,除非是工程师自己犯错,否则不会轻易出现这种转型错,这种情况的输入值是工程师“人工”方式提供,数据本身是可控制的。而第二种情况在和第三方系统集成时就特别常见,这种情况下,由于第三方接口提供的数据本身类型不规范,在业务层面,工程师会按照“规范”的思维去处理数据。如第三方传入了金额的数据,格式为:"amount": "3200.56",此时,工程师会习惯性觉得金额使用的类型是浮点数,于是在读取数据时调用了getDouble(String)方法,转型错误就产生了。

「思」:真实的开发和教科书上所讲的理论内容是有一定差距的,很多工程师无法认可这种差距,很多时候这种“规范”的情绪会被带入实际项目里,心里觉得:“这个东西本应该……”,而从项目交付层面上考虑,这就是工程师需要解决的实际问题,如何在不规范的数据中去稳定运行软件,也是工程师需要修炼的一门课程。反过来讲,越是不规范的数据越能够验证程序本身的健壮性,如果您不排斥这种情况,那么您编写的程序会在这样的实际场景中越炼越成熟,到最后系统的健壮性就不言而喻了。

1.4. 合并

  项目中处理JsonObject时有一种很常见的情况,就是合并:将两个JSON对象组合到一起生成一个新的JSON对象,本章主要针对Vert.x中合并JsonObject对象单独说明,io.vertx.core.json.JsonObject类中提供了三个核心方法来合并JsonObject对象,三个方法的定义如下:

public JsonObject mergeIn(JsonObject other){}
public JsonObject mergeIn(JsonObject other, boolean deep){}
public JsonObject mergeIn(JsonObject other, int depth){}

  根据方法定义,可以将Vert.x中的JsonObject对象合并分为三类:浅合并、深合并、定深合并,定深的意思是固定深度,按照您所期望的深度(设置depth的值)执行Json数据合并。这里先给读者普及下JSON对象的深度,如有以下Json数据:

{
    "code": "Develop",
    "money": 3200,
    "report": {
        "application": 12,
        "sortware": 21,
        "web": {
            "php": "PHP",
            "java": {
                "jsp": "JSP"
            }
        }
    }
}

  实际上JSON对象本身可以构成一个树形结构,而这里的深度表示的就是树的深度,下图是这个JSON对象的结构图:

  上图标注了JSON对象中不同深度的节点,为了方便读者理解合并,这里提供另外一份数据文件:

{
    "name": "DEV",
    "report": {
        "testing": 12,
        "web": {
            "js": "JS",
            "react": "React",
            "java": {
                "jsf": "JSF"
            }
        }
    }
}

  我们使用同样的方式来绘制上述的JSON对象结构:

  有了上边的图示,读者应该很清楚深度在这里指代什么。接下来先看如何将这两个对象合并的代码:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;
import io.vertx.up.util.Ut;

public class JObjectMerge {
    public static void main(final String[] args) {
        /* 左值 */
        final JsonObject leftJson = Ut.ioJObject("data/merge/left.json");
        /* 右值 */
        final JsonObject rightJson = Ut.ioJObject("data/merge/right.json");
        /* 合并,深度为2 */
        final JsonObject result = leftJson.mergeIn(rightJson, 2);
        System.err.println(result.encodePrettily());
    }
}

  上述代码运行过后,您可以看到如下输出:

{
    "code": "Develop",
    "money": 3200,
    "report": {
        "application": 12,
        "sortware": 21,
        "web": {
            "js": "JS",
            "react": "React",
            "java": {
                "jsf": "JSF"
            }
        },
        "testing": 12
    },
    "name": "DEV"
}

  在上边代码中,我们主要调用了方法mergeIn(JsonObject, int),这种合并方式我称为定深合并,由工程师指定需要合并节点的深度(如果深度参数depth传入的值是0则表示不合并)。示例中我们给了参数2,也就是说合并的终止深度发生在depth = 2的节点位置,根据输出结果,可以知道:

  • 深度为一(depth = 1)的节点被合并后有四个,分别是:code, money, report, name
  • 由于两个Json数据都拥有report节点,所以继续合并深度为二(depth = 2)的节点,第二层节点被合并后也有四个:application, software, web, testing
  • 虽然两份Json数据的report节点都包含了web节点,但web节点的深度为3,我们给的参数是2,所以web节点的数据就不执行合并。

  那么读者可能由一个困惑,既然不合并,为什么最终输出的web节点会是如下格式:

        "web": {
            "js": "JS",
            "react": "React",
            "java": {
                "jsf": "JSF"
            }
        }

  主要原因是mergeIn方法执行后,它是具有副作用的,最终它会改变调用者的内容,传入的结果并不是以“追加”的方式来修改内容,而是以“覆盖”的方式修改内容,那么上述的调用者的原始内容被传入的内容覆盖掉了,这种情况下web节点的数据内容自然以第二份数据为主。看完了上边的例子,简单总结一下三个方法的深度信息:

方法签名 深度depth
mergeIn(JsonObject) 1
mergeIn(JsonObject, depth) 由工程师指定
mergeIn(JsonObject, boolean) 如果第二参数为true那么值为最大整数Integer.MAX_VALUE,反之为1

  在理解了这个方法的逻辑后,我们似乎还遗留了几个悬而未决的问题,这个问题在开发过程中容易被工程师忽视,甚至在真实场景会产生不可预知的错误。这个问题就是:谁被改了?谁和谁在合并过后共享了JSON对象的引用?探讨这个问题的主要原因是作者最初在开发过程中经常被这个问题作弄,甚至经常踩到一些本以为正确的坑中,而这个坑就发生在合并的环节。

  接下来用简单的Json数据来说明这个问题:

data/copy/left.json

{
    "actor1": 1,
    "actor2": 2
}

data/copy/right.json

{
    "actor3": "3",
    "actor4": "4"
}

  结合上边数据看代码:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;
import io.vertx.up.util.Ut;

public class JObjectCopy {
    public static void main(final String[] args) {
        /* 左值 */
        final JsonObject leftJson = Ut.ioJObject("data/copy/left.json");
        /* 右值 */
        final JsonObject rightJson = Ut.ioJObject("data/copy/right.json");
        /* 直接合并 */
        final JsonObject result = leftJson.mergeIn(rightJson);
        System.out.println(leftJson.encode());
        System.out.println(rightJson.encode());
        System.out.println(result.encode());
        System.out.println("-------- 分割线 --------");
        /* 分别修改左右值 */
        result.put("actor1", "Lang");
        result.put("actor3", 121);
        System.out.println(leftJson.encode());
        System.out.println(rightJson.encode());
        System.out.println(result.encode());
    }
}

  上述代码最终会输出:

{"actor1":1,"actor2":2,"actor3":"3","actor4":"4"}
{"actor3":"3","actor4":"4"}
{"actor1":1,"actor2":2,"actor3":"3","actor4":"4"}
-------- 分割线 --------
{"actor1":"Lang","actor2":2,"actor3":121,"actor4":"4"}
{"actor3":"3","actor4":"4"}
{"actor1":"Lang","actor2":2,"actor3":121,"actor4":"4"}

  仔细分析上边代码,从最终的输出结果可以知道:

  1. 当某个JsonObject对象调用了mergeIn方法后,调用者会和合并过后的对象共享一个JsonObject引用,简单说,在后续流程里,如果某个引用指向的对象内容被改变(如上边的leftJsonresult),另一个也会受到影响。
  2. 而在mergeIn方法执行过后,原来传入的JsonObject对象本身不会发生变化(示例中的rightJson)。

  这样的测试结果会让工程师遇到一个比较头疼的问题:是否存在一种方式将leftJsonresult指向的对象分离开,在合并过后相互之间不影响呢?参考下边代码:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;
import io.vertx.up.util.Ut;

public class JObjectCopy1 {
    public static void main(final String[] args) {
        /* 左值 */
        final JsonObject leftJson = Ut.ioJObject("data/copy/left.json");
        /* 右值 */
        final JsonObject rightJson = Ut.ioJObject("data/copy/right.json");
        /* 直接合并 */
        final JsonObject result = leftJson
                .copy() // 拷贝方法
                .mergeIn(rightJson);
        /* 分别修改左右值 */
        result.put("actor1", "Lang");
        result.put("actor3", 121);
        System.out.println(leftJson.encode());
        System.out.println(rightJson.encode());
        System.out.println(result.encode());
    }
}

  这次的示例代码在前边例子中做了简单的修改,得到下边输出:

{"actor1":1,"actor2":2}
{"actor3":"3","actor4":"4"}
{"actor1":"Lang","actor2":2,"actor3":121,"actor4":"4"}

  从输出可以知道,在mergeIn方法执行过后,虽然调用了result的方法改变了对象内容,但是leftJson中的内容和前边的例子输出结构不同,它的内容没有改变——很多真实场景中这是我们所期望的结果。这里我们调用了一个新的方法:copy(),它表示拷贝当前JSON对象。

1.5. 拷贝

  前一章最后我们使用了copy()方法,那么JsonObject中的拷贝究竟是深拷贝还是浅拷贝?本章节深入copy()方法去看看,这个方法的源码如下:

    public JsonObject copy() {
        final Map<String, Object> copiedMap;
        if (map instanceof LinkedHashMap) {
            copiedMap = new LinkedHashMap<>(map.size());
        } else {
            copiedMap = new HashMap<>(map.size());
        }
        for (final Map.Entry<String, Object> entry : map.entrySet()) {
            Object val = entry.getValue();
            val = Json.checkAndCopy(val, true); // 实际是在递归执行
            copiedMap.put(entry.getKey(), val);
        }
        return new JsonObject(copiedMap);
    }

  上边的源码中有注释的一行就是读取节点值的内容,checkAndCopy方法在前文中已经讲过,当传入值是JsonObject类型时,它执行的取值方式如:

      if (copy) {
        val = ((JsonObject) val).copy();
      }

  这里的递归调用会执行深拷贝——拷贝过程不仅拷贝当前节点,而且所有JsonObjectJsonArray类型的子节点也会被拷贝一次!这个结果留给读者自己去验证,当做学习本章的课后习题。

2. JsonArray

  学习了JSON对象,这个章节开始我们来学习JSON数组,在Vert.x中使用io.vertx.core.json.JsonArray类来处理JSON数组,它表示一个有序的元素集合。

2.1. 构造

  JsonArray也有四种构造方式:

  • JsonArray():直接构造一个空数组,输出为:[]
  • JsonArray(String):传入一个java.lang.String对象,根据字面量解析生成合法的JSON数组。
  • JsonArray(List):传入一个java.util.List对象,生成合法的JSON数组。
  • JsonArray(Buffer):传入一个io.vertx.core.buffer.Buffer对象,构造一个JSON数组。

  虽然JsonArrayJsonObject的构造大同小异,读者还是先就一段简单的代码:

package io.vertx.up._02.json;

import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;

import java.util.ArrayList;
import java.util.List;

public class JArrayInit {

    public static void main(final String[] args) {
        /*
         * 空的JSON数组
         */
        final JsonArray empty = new JsonArray();
        System.out.println(empty.encodePrettily());
        /*
         * 字面量初始化
         */
        final String literal = "[1,true,\"Lang\",{\"name\":\"Lang\"}]";
        final JsonArray strData = new JsonArray(literal);
        System.out.println(strData.encodePrettily());
        /*
         * 使用List初始化
         */
        final List list = new ArrayList() {
            {
                this.add(Boolean.TRUE);
                this.add(Integer.MAX_VALUE);
                this.add("Hello World");
            }
        };
        final JsonArray listData = new JsonArray(list);
        System.out.println(listData.encodePrettily());
        /*
         * 使用Buffer初始化
         */
        final Buffer buffer = Buffer.buffer(literal);
        final JsonArray bufferData = new JsonArray(buffer);
        System.out.println(bufferData.encodePrettily());
    }
}

  JsonArray对象的构造和JsonObject唯一的区别是前者只能使用java.util.List构造,而后者只能用java.util.Map构造,这里的MapList在Java语言中的集合语义是“哈希表”和“有序列表”,这个语义和键值对、有序元素集合是等价的。同样JsonArray在构造时内部也会执行“解析”(第一种构造空JSON数组除外),若提供的格式非法,解析时会抛出对应的错误。

2.2. 格式

  JsonArray是一个有序元素集合,它内部使用了java.util.List<Object>结构来存储内容,每一个元素必须满足JSON数据的值格式。一般在实际开发中,使用最多的是JSON对象数组(每个元素类型都是JsonObject),这样的使用场景往往给初学的工程师造成一种错觉:JSON数组中的元素必须是JSON对象类型!——其实不然,只要符合JSON数据格式中提及的值格式,就可以用于描述JSON数组中的某个元素。

  同样看下边的非法格式代码:

package io.vertx.up._02.json;

import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonArray;

public class JArrayError {
    public static void main(final String[] args) {
        // 字符串元素使用单引号
        final String singleValue = "['Lang']";
        out(singleValue);
        // 布尔大小写敏感
        final String boolValue = "[True]";
        out(boolValue);
        // null字面量异常
        final String nullValue = "[Null]";
        out(nullValue);
        // 数值Java格式
        final String numberValue = "[1L]";
        out(numberValue);
    }

    private static void out(final String literal) {
        try {
            final JsonArray data = new JsonArray(literal);
            System.out.println(data.encodePrettily());
        } catch (final DecodeException ex) {
            System.out.println("解析异常:" + literal);
        }
    }
}

  运行上边代码会得到输出:

解析异常:['Lang']
解析异常:[True]
解析异常:[Null]
解析异常:[1L]

  看完了JSON数组的非法格式示例代码,这里做个简单的总结:

  1. 在实际开发过程中,不提倡使用不同类型元素的JSON数组,其原因是解析时会引入太多的判断语句来执行逻辑代码。
  2. 如果JSON数组中的元素类型是JSON对象,这个JSON对象也必须是合法的格式。

2.3. 集合操作

  JsonArray描述的是JSON数组,并且是一个有序元素集合,往JSON数组中添加一个元素称为操作,根据JSON数组索引读取该索引位置上的元素称为操作,它提供了一系列的add/get的方法。参考下边的表格来看JsonArray的读写方法:

数据类型 add方法 get方法
null值 addNull()
布尔值 add(Boolean) getBoolean(int)
字节数组 add(byte[]) getBinary(int)
双精度浮点数 add(Double) getDouble(int)
单精度浮点数 add(Float) getFloat(int)
枚举值 add(Enum) getEnum(int)
时间类型 add(Instant) getInstant(int)
整数 add(Integer) getInteger(int)
长整数 add(Long) getLong(int)
字符串 add(String) 或 add(CharSequence) getString(int)
JSON对象 add(JsonObject) getJsonObject(int)
JSON数组 add(JsonArray) getJsonArray(int)
Object对象 add(Object) getValue(int)

  表格中枚举了JsonArray对象针对不同类型的读写方法,因为JsonArray对象和JsonObject对象的读写方法思路是一致的,所以前文提到的点在这里不重复列举,但还是需要说明几点:

  • 所有的get方法的入参都是int类型的索引,它表示读取该索引位置上的元素信息,和JsonObject读方法不同的点在于,没有重载格式,不提供默认值。
  • JsonArray包含了一个特殊方法getList,它可以让您拿到ArrayList的引用。

  最后读者参考下边代码感受一下基本用法:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonArray;

public class JArrayGet {
    public static void main(final String[] args) {
        // 数组基本
        final JsonArray array = new JsonArray();
        array.add(Boolean.TRUE);
        array.add(12.1f);
        array.add("Whether");
        // 正确读取
        final int length = array.size();
        for (int idx = 0; idx < length; idx++) {
            final Object item = array.getValue(idx);
            if (item instanceof Boolean) {
                System.out.println("布尔值:" + (Boolean) item);
            } else if (item instanceof Float) {
                System.out.println("浮点数:" + (Float) item);
            } else if (item instanceof String) {
                System.out.println("字符串:" + item);
            }
        }
        // 错误遍历
        for (int idx = 0; idx < length; idx++) {
            final Float item = array.getFloat(idx);
        }
    }
}

  上边代码的输出为:

布尔值:true
浮点数:12.1
字符串:Whether
Exception in thread "main" java.lang.ClassCastException: java.lang.Boolean cannot be cast to java.lang.Number
    at io.vertx.core.json.JsonArray.getFloat(JsonArray.java:149)
    at io.vertx.up._02.json.JArrayGet.main(JArrayGet.java:26)

add(JsonArray) 和 addAll(JsonArray)

  单独将这两个写入方法提取出来讲,是因为开发者很容易对这两个方法产生误解,混淆不清。两个方法的基本描述如下:

  • add方法会在当前JsonArray对象中添加一个新元素。
  • addAll方法会将传入JsonArray中的每一个元素一次添加到当前JsonArray对象中。

  参考下边代码:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonArray;

public class JArrayAdd {
    public static void main(final String[] args) {
        // 原始数组
        final JsonArray original = new JsonArray();
        original.add("A").add("B").add("C");
        // add
        final JsonArray added = new JsonArray();
        added.add("D").add("E");
        final JsonArray add = original.copy().add(added);
        // addAll
        final JsonArray addedAll = new JsonArray();
        addedAll.add("D").add("E");
        final JsonArray addAll = original.copy().addAll(addedAll);

        // 打印结果
        System.out.println(add.encodePrettily());
        System.out.println("--- 分割 ---");
        System.out.println(addAll.encodePrettily());
    }
}

  上边代码的输出为:

[ "A", "B", "C", [ "D", "E" ] ]
--- 分割 ---
[ "A", "B", "C", "D", "E" ]

  从输出可以知道,add数组最终结果包含了4个元素,最后一个元素是一个JsonArray类型,而addAll数组最终结果包含了5个元素,最后追加了两个元素到原始数组中,分别是:DE。它们的执行过程图示如下:

add

addAll

  也就是说,addAll引起的实际效果是做数组“连接”,它会将两个JSON数组连接到一起,传入的JsonArray中的元素会一一追加到原始数组中,这样读者就可以区分这两个特殊方法了。

「思」:“比对法”是我们在学习概念过程的重要方法,这种方法特别适合初学者去掌握概念以及清除自己在概念理解的误区,学习概念没有捷径,并且每个人对概念的“掌握”的基本要求必须是:知其然并知其所以然。如果您不掌握牢这些基本概念,不了解异同而混用,往往在写代码时不会有问题,但调试会成为您的致命伤。

3. 开发分享

  细心的读者会发现,在JsonArrayJsonObject两个对象的内部,经常调用io.vertx.core.json.Json类中的方法,如checkAndCopy(Object,boolean),这个类是两个对象的“辅助工具类”,由于本身代码简单,有兴趣的读者可以去阅读这个类的源代码。接下来分享一下开发过程中使用JsonArrayJsonObject的心得,回顾一下前两节的主要内容。

3.1. 防止死循环

  在书写JsonArrayJsonObject类相关代码时,有时候会遇到死循环,这个小结讨论一下死循环的出现原因,以及如何解决。先看下边的代码:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;

public class IssueOne {
    public static void main(final String[] args) {
        final JsonObject data = new JsonObject()
                .put("name", "Test");
        data.put("data", data);
        System.out.println(data.encodePrettily());
    }
}

  上边代码可以通过编译,而从逻辑上讲,这种代码是不符合逻辑的,这里只是为了演示给读者产生原因。运行上述代码,会在控制台看到如下异常:

Exception in thread "main" java.lang.StackOverflowError
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider$Impl.<init>(DefaultSerializerProvider.java:614)

  如果这段代码在主线程中,恭喜,您是幸运的,因为您可以看到这里的异常信息,也可以从输出看到引起问题的原因。但如果这段代码存在于Future的异步代码中,那么这里就很不容易重现了。当然也有人会说,你可以不用写这种代码!——对的,这种代码本身是没有意义的,但是若您的代码结构过于复杂,就像前一章那个故事:“从前有座山,山里有座庙……”,这种情况总会难免?在使用JsonObjectJsonArray的过程中,如果您在读写过程直接使用,很多时候会因为本身逻辑的复杂度(并不一定是工程师的问题)导致这种情况发生,如果要解决这个问题,就可以使用copy()方法,将中间写入代码改写成:

        data.put("data", data.copy());

  如此,您就可以在控制台看到下边的输出了:

{
    "name": "Test",
    "data": {
        "name": "Test"
    }
}

  这段代码给了我们一个提醒:在JSON数组和JSON对象相互嵌套的时,尽可能将JsonObjectJsonArray作为数据副本来传递去运算,而不是直接使用,否则就会出现本章这种引起死循环的问题。

3.2. 迭代中修改

  JsonObject和JsonArray内部都封装了Java中的集合,使用过程会出现不可避免的java.util.ConcurrentModificationException问题,如下:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;

import java.util.Objects;

public class JsonIterator {
    public static void main(final String[] args) {
        /*
         * JsonObject
         */
        final JsonObject data = new JsonObject()
                .put("name", "Lang")
                .put("email", "lang.yu@hpe.com")
                .put("age", 34);
        /*
         * 常用迭代
         */
        data.stream()
                .filter(Objects::nonNull)
                .filter(entry -> Objects.nonNull(entry.getValue()))
                .forEach(item -> {
                    if ("name".equals(item.getKey())) {
                        data.remove(item.getKey());
                    }
                });
    }
}

  运行上述代码可以得到:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:719)
    at java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:752)
    at java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:750)

  这个错误信息主要源于在迭代过程中原始集合发生了改变,如代码中调用remove方法移除其中的一个元素,而这个错误的细节这里不深究,不论是互联网还是Java本身的源代码都有详细说明,这里主要提一下解决办法。前文中已经提到JsonObject中包含了一个copy()方法,JsonArray也有这个方法,解决代码如:

        // 调用 data的 copy() 方法防止java.util.ConcurrentModificationException
        data.copy().stream()
                .filter(Objects::nonNull)
                .filter(entry -> Objects.nonNull(entry.getValue()))
                .forEach(item -> {
                    if ("name".equals(item.getKey())) {
                        data.remove(item.getKey());
                    }
                });

3.3. 和Pojo转换

  Vert.x中定义的JsonObjectJsonArray是轻量级数据结构,它没有任何业务意义,但真实项目中会使用自定义的类,那么如何让自定义类和JsonObject对象进行转换。看下边代码:

package io.vertx.up._02.json;

import io.vertx.core.json.JsonObject;

public class JavaMapJson {
    public static void main(final String[] args) {
        final JsonObject json = new JsonObject()
                .put("username", "Lang")
                .put("password", "test");
        // Json转User
        final User user = json.mapTo(User.class);
        System.out.println(user);
    }
}

class User {
    private String username;
    private String password;

    public String getUsername() {
        return this.username;
    }

    public void setUsername(final String username) {
        this.username = username;
    }

    public String getPassword() {
        return this.password;
    }

    public void setPassword(final String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + this.username + '\'' +
                ", password='" + this.password + '\'' +
                '}';
    }
}

  上边代码会输出:

User{username='Lang', password='test'}

4. 总结

  本章主要讲解了Vert.x中最基础的数据结构io.vertx.core.json.JsonArrayio.vertx.core.json.JsonObject,并且给读者演示了常用的方法,通过对部分源代码的解析,让读者对内部代码逻辑有所了解。

results matching ""

    No results matching ""