背景
dapeng的原生协议是thrift, 包括二进制、压缩二进制两种模式. 原生协议的优势是有丰富的数据类型支持能力(优于JSON), 传输格式紧凑(远比JSON更小, 尤其是压缩二进制格式), 序列化开销小(几乎没有解析成本和数据转换成本), 这些优点是dapeng选择thrift的基本原因.
虽然有很多的优势, 原生的thrift协议最大的缺点就是对“人”不友好, 而JSON则是human readable的最优选择, 目前已是互联网应用的事实标准. 它相比XML更为简洁, 当然也更为紧凑, 便于开发者阅读, 非常方便在WEB上应用. 这使得它取代xml成为了数据交换事实上的行业标准. 尽管语言之争是个永恒的话题, 但在数据交换格式上, JSON是毫无争议的王者.
如何为dapeng服务提供对JSON的支持, 可以使用restful的形式来调用dapeng服务, 从而方便WEB前端、PHP或者命令行工具来调用服务呢?dapeng提供了API Gateway, 将JSON请求转换成为thrift请求, 对外提供了一个restful + JSON的API接入服务. API Gateway包括了鉴权、流控、路由等多个功能, 但其核心的功能是完成JSON到thrift的双向数据转换工作, 本文介绍了dapeng-json的设计特色, 包括dapeng是如何高效率的完成数据转化、如何处理可变压缩二进制格式等挑战的.
元数据(metadata)是dapeng的一大特色, 所有的服务都自带有丰富的、完整的IDL metadata, 基于metadata, 我们可以动态的实现任意协议格式转换成为原生thrift格式的能力. 未来, 我们还将计划在dapeng中支持 flatbuffer() 等数据格式, 适应某些极速应用的场合.
dapeng-json的设计目标
dapeng-json的最终目的就是实现JSON字符串与基于thrift协议格式的二进制流之间的高效互换.具体有如下三个指标:
在介绍dapeng-JSON的设计思路之前, 我们先看看dapeng对JSON的支持架构:
右上角的两个模块(dapeng-json, ServiceMetaData), 再暴露一个http端口, 可以构成一个典型的 HttpServiceAgent.
dapeng-json 的实现思路
让我们重温一下dapeng-json的使命:
dapeng-json的最终目的就是实现 JSON 字符串与 thrift 格式的二进制流之间的高效互换
常规方式:string -> JSONObject -> POJO -> Binary
将JSON 字符串转换为 JSON 对象模型(JSONObject)
然后通过三方JSON-POJO映射框架(如Google的Gson, 阿里的FastJSON等), 将JSONObject转换成为POJO,
通过dapeng生成的POJO序列化代码完成从POJO到thrfit的转换过程.
部分框架可以结合1和2, 直接从JSON字符串转换成为 POJO.
不过, 这个设计不满足我们的设计要求:
基于流的转换:JSON bytes -> Binary
这是dapeng-json所采用的方式, 显而易见, 这个过程只经历了一次协议转换, 即直接将从网络上接收到的一个JSON字节流直接转换成为thrift数据包.
那么dapeng-JSON是如何做到的呢?
JSON结构简述
首先我们来看看JSON的结构. 一个典型的JSON结构体如下:
{
"createOrderRequest": {
"memberId": 1024,
"payCode": "tidf3325aaeny",
"storeIds: [28, 35, 64],
"items": [
{
"skuId": 24,
"amount": 4.5
}, {
"skuId": 106,
"amount": 20.0
}
]
}
}
相比XML, JSON数据类型比较简单, 由Object/Array/Value/String/Boolean/Number等元素组成, 每种元素都由特定的字符开始/结束. 例如Object以'{‘以及’}’这两个字符标志开始以及结束, 而Array是'[‘以及’]’. 简单的结构使得JSON比较容易组装以及解析, 这也是JSON如此流行的重要原因.
好了, 介绍完JSON的基本知识, 我们先上一张dapeng-JSON的类结构图, 详细分析见下文:
JSONCallback
从上节我们可以得知, JSON的组成元素类型不多, 且每种元素都有自己的开始/结束字符, 我们暂且称这些字符为关键字符.
为了高效的处理JSON字符串, 我们采用了Streaming的处理方式:逐个字符读入JSON字符串,碰到关键字符就触发一定的处理动作. 为此, 我们首先创建一个接口JSONCallback, 并定义了一组回调方法用以处理每种JSON元素:
package com.github.dapeng.json;
import com.github.dapeng.org.apache.thrift.TException;
/**
* @author zxwang
*/
public interface JSONCallback {
/**
* Called at start of JSON object, typical handle the '{'
*
* @throws TException
*/
void onStartObject() throws TException;
/**
* Called at end of JSON object, typical handle the '}'
*
* @throws TException
*/
void onEndObject() throws TException;
/**
* Called at start of JSON array, typical handle the '['
*
* @throws TException
*/
void onStartArray() throws TException;
/**
* Called at end of JSON array, typical handle the ']'
*
* @throws TException
*/
void onEndArray() throws TException;
/**
* Called at start of JSON field, such as: "orderId":130
*
* @param name name of the filed, as for the example above, that is "orderId"
* @throws TException
*/
void onStartField(String name) throws TException;
/**
* Called at end of JSON field
*
* @throws TException
*/
void onEndField() throws TException;
/**
* Called when a boolean value is met,
* as to given field:"expired":false* First onStartField("expired") is called, followed by a call onBoolean(false) and a call onEndField()
*
* @param value
* @throws TException
*/
void onBoolean(boolean value) throws TException;
/**
* Called when a double value is met.
*
* @param value
* @throws TException
*/
void onNumber(double value) throws TException;
/**
* Called when a long/int value is met.
*
* @param value
* @throws TException
*/
void onNumber(long value) throws TException;
/**
* Called when a null value is met.
* Such as: "subItemId":null
*
* @throws TException
*/
void onNull() throws TException;
/**
* Called when a String value is met.
* Such as: "name": "Walt"
*
* @param value
* @throws TException
*/
void onString(String value) throws TException;
}
这个接口的设计借鉴了XML SAX的设计思想, 既可用于序列化(生成JSON), 也可以反序列化(解析JSON字符串).
上面的JSON数组转json,经过JSONParser后数组转json,就会产生如下的事件流:
- onStartObject
- onStartFiled createOrderRequest
- onStartObject
- onStartField memberId
- onNumber 1024
- onEndField
- onStartField payCode
- onString tidf3325aaeny
- onEndField
- onStartField storeIds
- onStartArray
- onNumber 28
- onNumber 35
- onNumber 64
- onEndArray
- onEndField
- onStartField items
- onStartArray
- onStartObject
- onStartField skuId
- onNumber 24
- onEndField
- onStartField amount
- onNumber 4.5
- onEndField
- onEndObject
- onStartObject
- onStartField skuId
- onNumber 106
- onEndField
- onStartField amount
- onNumber 20.0
- onEndField
- onEndObject
- onEndArray
- onEndField
- onEndObject
- onEndField
- onEndObject此外, dapeng核心包里定义了一个通用的编解码接口:
package com.github.dapeng.core;
import com.github.dapeng.org.apache.thrift.TException;
import com.github.dapeng.org.apache.thrift.protocol.TProtocol;
/**
* 通用编解码器接口
* @author ever
* @date 2017/7/17
*/
public interface BeanSerializer {
/**
* 反序列化方法, 从thrift协议格式中转换回PoJo
* @param iproto
* @return
* @throws TException
*/
T read(TProtocol iproto) throws TException;
/**
* 序列化方法, 把PoJo转换为thrift协议格式
* @param bean
* @param oproto
* @throws TException
*/
void write(T bean, TProtocol oproto) throws TException;
/**
* PoJo校验方法
* @param bean
* @throws TException
*/
void validate(T bean) throws TException;
/**
* 输出对人友好的信息
* @param bean
* @return
*/
String toString(T bean);
}dapeng-json通过JSONSerializer实现了这个编解码器.
在深入具体实现之前, 我们先了解一下dapeng的服务元信息, 因为服务元信息是dapeng很多模块的关键.
dapeng 元信息
前文提到, dapeng服务有丰富的元信息, 非常类似于Java Class的反射信息, 通过反射, 我们动态的访问一个对象的各个字段、方法, 而无需依赖于编译时期的类型信息. 同样, 有了服务元信息, 我们也可以在运行期访问到IDL中定义的所有信息.
可以通过元数据信息自动生成java/scala等客户端代码.
可以通过元数据信息在客户端序列化/反序列接口参数或者返回信息的时候, 校验每一个字段是否有效.
这里是一个服务元信息的示例:
<request name="getAddress_args">
<field tag="1" name="request" optional="false" privacy="false">
STRUCT
com.today.api.dictionary.request.GetAddressRequest
查询请求
<struct namespace="com.today.api.dictionary.request" name="GetAddressRequest">
新增/编辑 字典 的请求实体
<field tag="1" name="provinceCode" optional="true" privacy="false">
STRING
省级地址编码
<field tag="2" name="cityCode" optional="true" privacy="false">
STRING
市级地址编码
<field tag="3" name="districtCode" optional="true" privacy="false">
STRING
区级地址编码
<field tag="4" name="townCode" optional="true" privacy="false">
STRING
乡镇级地址编码
<field tag="5" name="streetCode" optional="true" privacy="false">
STRING
街道级地址编码
JSONSerializer.write: 序列化(JSONString->Binarry):自顶向下解析JSON
@Override
public void write(String input, TProtocol oproto) throws TException {
JsonReader jsonReader = new JsonReader(service, method, version, struct, requestByteBuf, oproto);
new JsonParser(input, jsonReader).parseJsValue();
}我们通过JsonParser实现对JSON字符串的解析, 在流式解析JSON的过程中, 我们会产生JsonCallback事件, JsonReader就是负责将相应的事件转化为thrift的操作.
TProtocol根据不同的协议格式有不同的序列化方式, 例如普通二进制或者压缩二进制.
map以及struct在JSON中都表现为object.
2.1.1中, 如果object的类型是map, 根据 thrift 的协议规范, 集合字段需要写入集合的大小, 这里我们先往ByteBuf中写入0.
2.1.3中, 如果object是一个map(可通过dapeng的服务元数据信息来判断到底是map还是struct), 那么还需要改写我们在2.1.1中写到ByteBuf中的集合长度.同理, 在2.2.3中, 也需要改写集合长度.
2.3.2在处理字符串的过程中, 为了更好的兼容前端, 我们允许int/double/boolean/enum/bigdecimal等类型也带上双引号. 这时候元数据的强大威力又显现了. 我们可以根据当前字段的数据类型, 给字符串做适当的转义再写入到ByteBuf中去.
遇到某个属性对应的值为null的情况下, 实际操作中还需要重置ByteBuf的writerIndex, 把null对应的属性名从二进制流中清除.
考虑如下的JSON, 对于没有信仰的Jack来说, 由于是流式处理, 我们在处理null的时候,faith已经写入到ByteBuf中了, 我们要忽略这个字段的话, 需要重置ByteBuf的writerIndex.
{
"name": "Jack",
"faith" : null
"age" : 12
}JSONSerializer.read:反序列化(Binary->JSONString)
这个过程是write的一个逆向过程.
针对JSON的各种数据类型通过dapeng强大的服务元数据,我们可以知道某个服务的某个方法的某个入参的元数据信息,例如有多少field,每个field的类型,是否必填等:
在处理JSON的过程中, 我们可以通过元数据来实现如下功能:
对于接口请求参数中没有的字段, 可直接忽略
对每个字段做必要的类型校验, 同时
对于接口请求参数中要求必填的参数字段,在处理完整个JSON字符串之后,做一个对必填字段的校验. 如果存在必填字段缺失的情况,那么直接返回提示给调用方,防止无效请求发到服务端.
基于string-stream的流式处理机制,尽可能少的分配内存以及创建字符串,支持fast-failed. 流式处理不需要在内存中构建好整个JSON对象再做处理, 它对内存的消耗取决于JSON结构的深度而不是长度. 例如某个JSON结构体包含一个数组元素, 那么流式处理机制所需要的最大内存等于单个数组元素的大小, 而数组的长度对于内存消耗来说没有影响. 这个特性很重要, 就算要处理几MB或者几十MB甚至几百MB的JSON数据, dapeng-JSON可能也只是需要几KB的内存消耗而已.
限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。