RapidJSON v1.1.0 发布简介

时隔 15.6 个月,终于发布了一个新版本 v1.1.0。
新版本除了包含了这些日子收集到的无数的小改进及 bug fixes,也有一些新功能。本文尝试从使用者的角度,简单介绍一下这些功能和沿由。
Photo by Ian Schneider
JSON Pointer
也许 RapidJSON 一直最为人垢病的地方,是它奇怪的 API 设计。例如,对 DOM 加添数据要给于 allocator 参数:
#include "rapidjson/document.h"
using namespace rapidjson;
// ...
Document d(kObjectType);
Value a(kArrayType);
for (int i = 1; i <= 4; i++)
    a.PushBack(i, d.GetAllocator());
d.AddMember("a", a, d.GetAllocator());
// { a : [1, 2, 3, 4] }
这是由于 RapidJSON 的 DOM 使用局部的分配器,以避免全局分配器的问题。而为了节省内存,每个 Value 不会存储分配器的指针,所以必须从外部提供。
此设计也导致另一种问题。我们看看一个例子,使用 DOM API 访问以下这个 JSON:
{
    "widget": {
        "window": {
            "title": "Sample Konfabulator Widget"
        }
    }
}
要访问 title,最直觉想到的应该是这样:
Document d;
d.Parse(json);
std::cout << d["widget"]["window"]["title"].GetString();
如果 widget、window 或 title 不存在呢?以标准库 std::map::operator[] 的做法来说,当找不到键,它会自动加入一个键值对,并返回该值(所以 map::operator[] 必须是 non-const 函数)。然而,RapidJSON 创建值的时候需要 allocator,所以不可能自动加入键值对。因此,RapidJSON 规定以 operator[] 访问时,必须确保键存在(找不到时直接断言失败)。若不能确保,应先用 HasMember() 判断,或更好的是使用 FindMember(),因为它可以告之键是否存在的同时,能通过迭代器取得该值。可是,使用 FindMember() 去访问多层对象,代码非常冗长:
Value::ConstMemberIterator itr1 = d.FindMember("widget");
if (itr1 != d.MemberEnd()) {
    const Value& widget = itr1->value;
    if (widget.IsObject()) {
        Value::ConstMemberIterator itr2 = widget.FindMember("window");
        if (itr2 != widget.MemberEnd()) {
            const Value& window = itr2->value;
            if (window.IsObject()) {
                Value::ConstMemberIterator itr3 = window.FindMember("title");
                if (itr3 != window.MemberEnd()) {
                    const Value& title = itr3->value;
                    if (title.IsString()) {
                        std::cout << title.GetString();
                    }
                }
            }
        }
    }
}
这坨代码也许是最快最直接的方式。但一般业务代码写成这样,可读性太低,也容易出错。
大家都可以写一些辅助函数来解决这个问题。而我选择了实现 RFC6901 ── JSON Pointer。先看看使用后的结果:
#include "rapidjson/pointer.h"
// ...
if (const Value* title = GetValueByPointer(d, "/widget/window/title")) {
    if (title->IsString()) {
        std::cout << title->GetString();
    }
}
这个版本简单得多吧,"/widget/window/title" 是一个 JSON Pointer 的字符串形式,然后 GetValueByPointer() 在 d 上解引用,如果失败就返回空指针。
在逻辑上是和上面的冗长版本是一模一样的,只是增加了一些解析 JSON Pointer 的运行时间及空间成本。对大多数人来说,应该更会接受这个版本。
有时候,业务逻辑还会是这样的:「如果键不存在,就使用缺省值。」RapidJSON 的 JSON Pointer 也提供此功能:
Value& title = GetValueByPointerWithDefault(
    d, "/widget/window/title", "untitled");
当解引用失败时,它会创建整个路径,并把预设值复制成新值,并返回该值。由于它总能返回一个值,此函数的返回类型为引用而不是指针。
在此也简单介绍一下 JSON Pointer 的语法。它以 '/' 分隔多个 token,而每个 token 可以是 JSON object 的键,也可以是 JSON array 的下标。还有一种特殊 token 是负号 -,它可以指 JSON array 最后元素的下一个元素。使用这种特性能实现 PushBack() 的效果:
Document d;
CreateValueByPointer(d, "/a").SetArray();
for (int i = 1; i <= 4; i++)
    SetValueByPointer(d, "/a/-", i);
// { a : [1, 2, 3, 4] }
使用 JSON Pointer 的另一优点在于,它本身也是一个字符串,可以放置在 JSON 或其他文本格式之中。那么,我们便有一个标准方式去引用 JSON 中的值。
希望 JSON Pointer 能减轻使用者的负担,同时也提供一种数据驱动的弹性。新功能 JSON Pointer 简单介绍至此,更多信息可参考 RapidJSON 使用手册:Pointer。
JSON Schema
上面我们也谈到一个问题,JSON 里的组织方式、类型可能和预期的不同,我们可能要写很多代码去校验一个 JSON 的格式是否乎合预期。特别是后台服务器可能接收到不正常的JSON,甚至是恶意编写的 JSON 以图攻击。
在 XML 的世界中,可使用 XML DTD 或 XML Schema 去描述 XML 的结构。在 JSON 的世界中,已经有相关草案,称为 JSON Schema。
RapidJSON 实现了 JSON Schema v4 draft,并正式纳入了 v1.1.0。先看看用法:
#include "rapidjson/schema.h"
// ...
Document sd;
if (!sd.Parse(schemaJson).HasParseError()) {
    // 此 schema 不是合法的 JSON
}
SchemaDocument schema(sd); // 把一个 Document 编译至 SchemaDocument
// 之后不再需要 sd
Document d;
if (!d.Parse(inputJson).HasParseError()) {
    // 输入不是一个合法的 JSON
}
SchemaValidator validator(schema);
if (!d.Accept(validator)) {
    // 输入的 JSON 不合乎 schema
}
以我所知,现时所有 JSON Schema 实现都是校验一个 DOM 是否合乎 Schema。RapidJSON 做了一个创新的尝试,以事件流(SAX 风格)的方式去做校验。上面的例子利用 Document::Accept() 产生事件流,然后送交 SchemaValidator 校验。也许读者会问:「这也是在校验一个 DOM 是否合乎 Schema,有什么特别吗?」
这实际意味着,RapidJSON 的 JSON Schema 校验器除了可以校验 DOM,也可以校验更底层的 SAX。例如,我们可以用 SAX 解析 JSON 时,同时进行 JSON Schema 校验。如果中途不合乎 JSON Schema,就能直接中止解析。
SchemaValidator validator(schema);
Reader reader;
if (!reader.Parse(stream, validator)) {
    if (!validator.IsValid()) {
        // 输入的 JSON 不合乎 schema
    }
}
也可以同时把事件转发至一个自定义 handler:
MyHandler handler;
GenericSchemaValidator<SchemaDocument, MyHandler> validator(schema, handler);
Reader reader;
if (!reader.Parse(stream, validator)) {
    if (!validator.IsValid()) {
        // 输入的 JSON 不合乎 schema
    }
}
由于 DOM 解析 JSON 时,底层也是使用 SAX,所以也可以同时做 Schema 校验。其实除了解析,在生成时也可以进行校验,以确保输出的 JSON 也是乎合 Schema 的。这些用法都可参考 RapidJSON 使用手册:Schema。要学习 JSON Schema 的写法,笔者推荐 Understanding JSON Schema 这个英文网站。
C++11 范围 for 循环
此版本还加入了 Array 和 Object 辅助类型(包裹类),可分别通过 Value::GetArray()、Value::GetObject() 获取。这两个辅助类型提供该 JSON 类型专门的接口,例如 Array::PushBack()、Object::AddMember() 等。更重要的是,为了令 C++11 用户使用得更顺手,它们可做范围 for 循环(range-based for loop):
// C++03
for (Value::ConstValueIterator itr = a.Begin(); itr != a.End(); ++itr)
    printf("%d ", itr->GetInt());
// C++11
for (auto& v : a.GetArray())
    printf("%d ", v.GetInt());
// C++03
for (Value::ConstMemberIterator itr = document.MemberBegin();
    itr != document.MemberEnd(); ++itr)
{
    printf("Type of member %s is %s\n",
        itr->name.GetString(), kTypeNames[itr->value.GetType()]);
}
// C++11
for (auto& m : document.GetObject())
    printf("Type of member %s is %s\n",
        m.name.GetString(), kTypeNames[m.value.GetType()]);
其他相关详情可参阅 RapidJSON 使用手册:教程。
结语
这个 RapidJSON 版本对我而言是一个挑战。
JSON Schema 实际上也需要 JSON Pointer,所以 JSON Pointer 可算是一举两得的新功能。但实现 JSON Schema 时有两个难点。一个是 JSON Schema 需要正则引擎,在 C++11 下能直接使用 std::regex;而为了 C++03,我还实现了一个 500 行代码的 Thompson NFA 正则引擎。另一个难点在于,事件流的校验不容易实现 allOf、anyOf、oneOf、not 等关键字,需要多个校验器同时检验事件流。
新功能 JSON Schema 和 JSON Pointer 都是附加功能,完全不影响 v1.0.x 的 API。
除新功能外,此版本含有一个重要的内存优化。在 x86-64 架构下,64 位指针只使用到 48 位,我重新设计了 Value 的排布,使每个值的内存开销从 24 字节缩减至 16 字节。虽然存储指针时会有时间开销,但因大量缩减内存,更好的缓存一致性应该可以厘补损失,甚至能进一步提升整体性能。
屈指一算,RapidJSON 已快近 5 个年头了,最近一年我转部门后,更少机会在工作上使用 RapidJSON,所以我可能较少机会发现问题和新需求。虽然是这样,我仍然会继续维护这个项目,也要靠大家去发现问题和新需求,希望能得到大家的意见。
P.S. 可能大家会关心性能,我会尽快更新 nativejson-benchmark。
RapidJSON v1.1.0 发布简介的更多相关文章
- FineUIMvc v1.4.0 发布了(ASP.NET MVC控件库)!
		
FineUIMvc v1.4.0 已经于 2017-06-30 发布,FineUIMvc 是基于 jQuery 的专业 ASP.NET MVC 控件库,是我们的新产品.由于和 FineUI(专业版)共 ...
 - Jsonnet-PHP v1.3.0 发布,支持 PHP 7 使用 Jsonnet
		
JsonNet-PHP 是 Google Jsonnet 对 PHP的支持扩展. pecl: http://pecl.php.net/package/jsonnet github: https://g ...
 - Yearning v1.3.0 发布,Web 端 SQL 审核平台
		
企业级MYSQL web端 SQL审核平台. Website 官网 www.yearning.io Feature 功能 数据库字典自动生成 SQL查询 查询工单 导出 自动补全,智能提示 查询语句审 ...
 - spring-boot-plus V1.4.0发布 集成用户角色权限部门管理
		
RBAC用户角色权限 用户角色权限部门管理核心接口介绍 Shiro权限配置
 - Gitea v1.17.0 正式发布 | 集成软件包管理器、容器镜像仓库
		
我们自豪地宣布 Gitea v1.17.0 发布了.本次发布带来了诸多新特性和累积的更新,我们强烈建议用户在更新到最新版本之前仔细阅读发行注记. 在 1.17.0 版本的开发中我们一共合并了 645 ...
 - 亿能测试白盒安全测试模板V1.0发布
		
亿能测试白盒安全测试模板V1.0发布http://automationqa.com/forum.php?mod=viewthread&tid=2911&fromuid=21
 - RDIFramework.NET平台代码生成器V1.0发布(提供下载)
		
RDIFramework.NET平台代码生成器V1.0发布(提供下载) RDIFramework.NET(.NET快速开发整合框架)框架做为信息化系统快速开发.整合的框架,其目的一至是给用户和开发 ...
 - 浏览器端类EXCEL表格插件 版本更新 - 智表ZCELL产品V1.1.0.1版本发布
		
智表(ZCELL),浏览器下纯JS表格控件,为您提供EXCEL般的智能体验! 纯国产化.高性价比的可靠解决方案. 更新说明 让大家久等了.因为最近忙其他项目,发布时间稍有延迟. 下次版本更新 ...
 - 启明星手机版安卓android会议室预定系统 V1.0发布
		
启明星手机版会议室预定系统 V1.0发布 在手机里输入 http://www.dotnetcms.org/e4.apk 或者扫描二维码下载 用户打开系统,可以实时查看所有会议室状态 点击会议室名称,可 ...
 
随机推荐
- 原生JS:delete、in、typeof、instanceof、void详解
			
delete.in.typeof.instanceof.void详解 本文参考MDN做的详细整理,方便大家参考[MDN](https://developer.mozilla.org/zh-CN/doc ...
 - hibernate(2) —— 主键策略
			
框架提供了三种主键生成方式,一种是由用户自己维护,一种是由hibernate框架维护,另一种是由数据库维护. 自己维护就是在插入数据的时候,一定要指定主键的值,否则会出错,如果由框架维护和由数据库维护 ...
 - 1-7 basket.js  localstorage.js缓存css、js
			
basket.js 源码分析 api 使用文档: http://t3n.de/news/basketjs-performance-localstorage-515119/ 一.前言 b ...
 - 渗透测试-奇技淫巧(一)--源IP地址隐藏
			
切记,切记.本文只作为技术交流,提醒各位注意网络安全,请勿用于其它用途,否则后果自付. 在很多时候,某某不希望不愿意有人溯源他的地址.他们是如何隐藏IP的? 今天来浅析下,如何隐藏源地址. 用到的工具 ...
 - SharePoint 2013 工作流平台的选项不可用
			
问题描述 当我想创建一个SharePoint 2013 工作流的时候,打开SharePoint 2013 Designer(一下简称SPD),发现没有SharePoint 2013 工作流的选项.原来 ...
 - ReactiveCocoa代码实践之-RAC网络请求重构
			
前言 RAC相比以往的开发模式主要有以下优点:提供了统一的消息传递机制:提供了多种奇妙且高效的信号操作方法:配合MVVM设计模式和RAC宏绑定减少多端依赖. RAC的理论知识非常深厚,包含有FRP,高 ...
 - SCRIPT5011:不能执行已释放Script的代码
			
环境:win7 64位 IE9 错误:SCRIPT5011:不能执行已释放Script的代码. 现象:在父窗体的close()中调用嵌套的iframe页面的js方法返回一个对象时抛此异常. 原因:在一 ...
 - iPhone被盗后续更新二:被换机!已取机!没扣住新机!怎么找新机呢?事发半年后跟进...
			
先说下情况 MEID/IMEI:3544 2706 9380 456 我的序列号:F17NL088G5MY 新的IMEI:3569 7606 5956 097 新的序列号:DNPNV69ZG5MY 我 ...
 - MySQL更改数据库数据存储目录
			
MySQL数据库默认的数据库文件位于/var/lib/mysql下,有时候由于存储规划等原因,需要更改MySQL数据库的数据存储目录.下文总结整理了实践过程的操作步骤. 1:确认MySQL数据库存储目 ...
 - 0028 Java学习笔记-面向对象-Lambda表达式
			
匿名内部类与Lambda表达式示例 下面代码来源于:0027 Java学习笔记-面向对象-(非静态.静态.局部.匿名)内部类 package testpack; public class Test1{ ...