筋斗云框架:REST-RPC风格服务接口实例分析

无论设计原生手机App,或是前面文章曾提及过的“变脸式应用”(一种无网页刷新的多页面Web应用),都需要后端应用服务器提供业务支持。于是,如何设计后端服务接口是开发前必须考虑清楚的一件事。

谈及接口设计,我们需要从两个维度来考虑:协议(Protocol)及原型(Prototype),简称2P维度。

原型定义了一个调用的抽象形式。假定要做上门送餐业务,每个“商户”是个对象,取名为”Store”,那么一个“查询商户列表”接口,可以设计其原型为:

Store.query() -> tbl(id, name, dscr)

原型中,描述了调用名为Store.query,参数为空,返回Table类型的数据表示一张表(下文有对此类型介绍),表中的每行有id及name等几列,代表一个商户对象的属性。

接着,需要设计协议来实现它。可以这样来实现上述调用:客户端对服务端的请求基于HTTP协议,使用HTTP GET或POST方法,将调用名放在URL末尾,将参数放在URL参数中(此处没有),服务端返回数据使用JSON格式来描述。于是,客户端需要像这样发出HTTP请求:

GET /api/Store.query

这便是筋斗云框架的服务接口设计。事实上,筋斗云框架是对DACA架构的实现,DACA全称“分布式访问和控制架构”,其中就有定义客户端-服务器如何通讯,即对上例中的设计方案进一步规范化,称为BQP协议(业务查询协议);DACA还规定了客户端如何调用服务接口,称为客户端公共调用接口,如下文将介绍的callSvr调用。

BQP协议的设计风格介于RESTful和RPC之间,故称为REST-RPC风格。Leonard Richardson 和 Sam Ruby 在他们的著作 RESTful Web Services 中引入了术语 REST-RPC 混合架构(中文版译文),它不像SOAP或XML-RPC那样使用额外的信封格式来包装调用名和参数,而是直接通过HTTP传输数据,这与 REST 样式的 Web 服务是类似的;但是它不使用标准的HTTP PUT/DELETE等方法操作资源,而且在URI中存储调用名(例子中是Store.query)。

我们考察协议设计的主要原则有:

  • 清晰易懂
  • 易实现
  • 传输及处理效率高

对照这些原则,RESTful风格清晰易懂,但像HTTP PUT/DELETE等方法的兼容性并不好,不论服务端或客户端在实现上都会遇到障碍;而使用RPC风格的设计,不仅可读性差很多,而且封包解包效率较低。所以,从实用的角度,筋斗云的设计思想认为,REST-RPC是目前更好的选择。

在BQP协议中,业务接口分为函数调用型接口(如login调用)和对象调用型接口(如Store.query调用)。函数调用型接口可以自由设计原型,而对象调用型接口其实是一种特殊的函数调用,用于操作业务对象,有相对固定的原型,设计者可以对它加以裁减或扩展。两类接口在通讯协议及客户端使用上没有太大区别,其主要区别在于后端服务的实现模型不同。

本文只讨论对象调用型接口。BQP协议定义了一个对象的五种标准操作:查询列表(query),获取明细(get),添加(add),更新(set)和删除(del)。下文将详细举例说明,我们先假定有“商户”(Store)这个对象,其数据模型描述如下:

@Store: id, name, addr, tel, dscr

这表示商户表Store,有id, name等字段。注意:DACA规范建议,在设计数据模型时,应以id作为主键。

DACA规范要求客户端应提供callSvr方法来调用服务接口,在筋斗云前端中,该接口为JS函数,其原型为

callSvr(ac, param?, fn?, postParam?, userOptions?) -> XMLHttpRequest
或
callSvr(ac, fn?, postParam?, userOptions?) -> XMLHttpRequest

其中ac表示调用名(action),parampostParam分别为通过URL和POST内容传递的参数,如果没有param,可以忽略该参数(即第二种原型)。fn为回调函数,调用格式为fn(data),其中参数data为返回的JSON对象,类型参考接口原型中的返回值描述。

带问号的参数表示可缺省。

函数返回XMLHttpRequest对象,与jQuery中的$.ajax返回值相同。

以上调用为异步方式,即该函数执行后立即结束,待服务端数据返回再回调函数fn。也可以做同步调用,只要将函数名callSvr改为callSvrSync,即意味着该函数将等服务端返回数据才结束,而且,其返回值不再是XMLHttpRequest对象,而是服务接口返回的JSON对象。我们在Chrome控制台窗口测试接口时,常用同步调用以方便看到结果。

只要熟悉这几个客户端接口,就可以根据设计文档中的接口原型调用任何接口了,不必再对BQP底层协议细节有深入了解。

添加对象

BQP协议中定义对象添加操作的原型如下:

{object}.add()(POST fields...) -> id

一般在原型定义中参数部分只用一个括号,表示参数通过URL或POST内容传递都可以。而这里出现了两个括号,就表示URL参数和POST参数不可混用,两个括号依次表示URL参数和POST参数。

这样,我们添加一个商户,可以用:

var postParam = {name: "华莹小吃", addr: "银科路88号", tel: "13712345678"};
callSvr("Store.add", api_StoreAdd, postParam);

function api_StoreAdd(data) {
    // 根据原型定义中的返回值,data是id值。
    alert("id=" + data);
}

由于没有URL参数,所以callSvr的第二个参数可以省略。如果想写完整,会像这样:

callSvr("Store.add", null, api_StoreAdd, postParam);

调用成功,则会调用指定的回调函数,如果调用失败,则前端框架会接管错误处理,调用者一般不必关心。

更新对象

原型为:

{object}.set(id)(POST fields...)

其中未指定返回值,表示调用成功时无特定返回值。筋斗云后端会返回字符串”OK”。

假如要更新id=8这家商户对应的联系电话:

var param = {id: 8};
var postParam = {tel: "13812345678"};
callSvr("Store.set", param, api_StoreSet, postParam);

function api_StoreSet(data)
{
    alert("更新成功");
}

注意:要更新的字段一定要放在POST参数中。

置空一个字段

在BQP协议中,设置一个字段为空串一般是被服务端忽略的,但在set操作中,如果在postParam中设置某个字段为空串(或特定字符串"null"),则表示清空该字段。

要清空某商户的地址:

var postParam = {addr: ""};
// 或者 var postParam = {addr: "null"};
callSvr("Store.set", {id: 8}, api_StoreSet, postParam);

下次用Store.get获取该商户时,可见属性addr值为null (注意:不是字段串"null")

删除对象

原型为:

{object}.del(id)

调用很简单,假如要删除id=8对应的商户:

callSvr("Store.del", {id: 8}, function (data) {
    alert("删除成功");
});

获取对象详情

原型为:

{object}.get(id, res?) -> {fields...}

其默认返回对象对应主表中的字段,设计时也可以为返回内容增加子对象或虚拟字段(实现方法参考筋斗云后端文档)。

假定在设计“获取商户”接口时,增加一个子对象“商品列表”名为items,设计接口原型为:

Store.get(id, res?) -> {id, name, addr, tel, @items=[item]}

item:: {id, name, price}

(注意:在设计接口原型时,用的是“蚕茧表示法”层层解析和描述对象类型,不在本文讨论范围内,详见相关文章。)

根据接口,要获取一个商户的详情可以这样调用:

callSvr("Store.get", {id: 8}, api_StoreGet);
function api_StoreGet(data) { ... }

返回数据data像这样:

{
    id: 8,
    name: "华莹小吃",
    addr: "银科路88号",
    tel: "13812345678",
    items: [
        {id: 1001, name: "鲜肉小笼", price: 10.0},
        {id: 1002, name: "大肉粽", price: 8.0}
        ...
    ]
}

在URL中的可选参数res它表示”result”,即返回字段的列表,多个字段中间用逗号分隔。如果你不想返回默认的字段,可以通过该参数指定想要哪些字段。

例:获取商户详情,只返回店名和电话:

callSvr("Store.get", {id: 8, res: "name,tel"}, api_StoreGet);
function api_StoreGet(data)
{
    // data示例:{name: "华莹小吃", tel: "13812345678"}
}

查询对象列表

查询操作是标准操作中最灵活和最复杂的,它的可选参数很多,原型有两种(返回内容的格式不同):

{object}.query(res?, cond?, orderby?, distinct?=0, _pagesz?=20, _pagekey?, _fmt?) -> tbl(field1,field2,...)
{object}.query(wantArray=1, ...) -> [{field1,field2,...}]

第一种原型返回特别的Table类型(下文介绍,可以转成对象数组),好处是数据精练,而且支持分页;第二种原型多了wantArray参数的设置(其它参数用法相同),返回类型变成对象数组,支持子对象,然而它不支持分页操作,一般使用较少。

Table类型

如果未指定参数wantArray(第一种原型),则返回的内容为Table类型,这种格式不可以返回子对象(如上节get操作中的子对象商品列表items),比如取商户列表:

callSvr("Store.query", api_StoreQuery);

function api_StoreQuery(data) { ... }

回调函数api_StoreQuery中的data参数格式为:

{
    h: [ "id", "name", "addr", "tel"]
    d: [
        [ 8, "华莹小吃", "银科路88号", "13812345678"],
        [ 9, ... ]
        ...
    ]
    nextkey: 998
}

其中属性h为列名数组,d表示数据行数组,每行的值数组与列名数组中元素一一对应。

如果存在属性nextkey,则表示这只是一部分数据,要取下一页数据,可以用同样的查询,带上参数_pagekey设置为该值,如

callSvr("Store.query", {_pagekey: 998});

这种Table结构设计有利于传输效率的提高,同时便于分页机制的设计。

筋斗云前端提供函数rs2Array,可将这个数据转换成通常用的对象数组:

var arr = rs2Array(data);

得到的arr像这样:

[
    {id: 8, name: "华莹小吃", addr: "银科路88号", tel: "13812345678"},
    {id: 9, ...}
    ...
]

查询参数

对象查询支持灵活的查询条件(通过参数cond - condition),排序方法(参数orderby),返回字段(参数res,与get操作一样)。

如果你了解SQL语句,则会发现这些参数用起来很简单。

  • 参数res指定返回字段, 多个字段以逗号分隔,例如, res=”field1,field2”.
  • 参数cond指定查询条件,其语法类似SQL语句的”WHERE”子句,例如”field1>100 AND field2=’hello’”,注意字符串值要加上单引号。
  • 参数orderby指定排序条件,语法可参照SQL语句的”ORDER BY”子句,例如:orderby=”id desc”,也可以多字段依次排序:”tm desc,status” (按时间倒排,再按状态正排)

例如,要查询所有id小于10且名字中以”华莹”开头的商户,返回结果按名字(name)排序:

var cond = "id<10 and name like ‘华莹%‘";
var param = {res: "id,name,addr", cond: cond, orderby: "name"};
callSvr("Store.query", param, api_StoreQuery);

function api_StoreQuery(data)
{
    // 先用rs2Array将table类型的数据转成对象数组
    var arr = rs2Array(data);

    // 遍历每个商户
    arr.forEach(function(store) {
        // 由于指定了res参数,store对象类型为:{id, name, addr}
    });
}

尽管这些参数值类似SQL语句,但它们有一些安全限制:

  • res, orderby只能是字段(或虚拟字段)列表,不能出现函数、子查询等。
  • cond可以由多个条件通过and或or组合而成,而每个条件的左边是字段名,右边是常量。不允许对字段运算,不允许子查询(不可以有select等关键字)。

像参数cond中出现以下情况都不允许:

left(type, 1)=‘A‘  -- 条件左边只能是字段,不允许计算或函数
type=type2  -- 字段与字段比较不允许
type in (select type from table2) -- 子表不允许

分页支持

参数_pagesz_pagekey用于支持分页。_pagesz指定每次返回多少条数据(默认一次返回20条)。

下面是一个获取所有商户的例子。第一次查询:

callSvr("Store.query")

返回数据像这样:

{nextkey: 10800910, h: [id, ...], d: [...]}

其中的nextkey表示数据未返回完,要查询下一页时需填写_pagekey字段。

第二次查询(下一页):

callSvr("Store.query", {_pagekey=10800910});

返回:

{nextkey: 10800931, h: [...], d: [...]}

仍返回nextkey字段说明还可以继续查询,再查询下一页:

callSvr("Store.query", {_pagekey=10800931});

返回:

{h: [...], d: [...]}

返回数据中不带nextkey属性,表示所有数据获取完毕。

如果想在首次查询时返回总记录数,可以设置_pagekey=0

callSvr("Store.query", {_pagekey: 0})

这样会返回

{nextkey: 10800910, total: 51, h: [id, ...], d: [...]}

多了total字段表示总记录数。由于缺省页大小为20,所以可估计总共有51/20=3页。

对象列表导出

在对象查询接口中设置参数_fmt,可以输出指定格式,一般用于将列表导出到文件。参数支持以下值:

  • csv:逗号分隔UTF8编码文本
  • txt:制表符分隔的UTF8文本
  • excel:与csv类似,但使用GB2312编码中文,以便不支持UTF8编码的MS Excel可以直接打开。

注意,由于默认会有分页,要想导出所有数据,一般指定一个很大的页大小,如_pagesz=9999

例如,要导出商户列表,可以这样写:

var url = makeUrl("Store.query", {_fmt: "excel", _pagesz: 9999});
location.href = url; // 下载Excel文件

注意:一定要使用框架提供的函数makeUrl生成URL,不要手写URL。它的用法与callSvr类似,传入调用名ac与URL参数param

时间: 2024-09-28 17:35:34

筋斗云框架:REST-RPC风格服务接口实例分析的相关文章

REST-RPC风格服务接口实例fenxi

无论设计原生手机App,或是前面文章曾提及过的"变脸式应用"(一种无网页刷新的多页面Web应用),都需要后端应用服务器提供业务支持.于是,如何设计后端服务接口是开发前必须考虑清楚的一件事. 谈及接口设计,我们需要从两个维度来考虑:协议(Protocol)及原型(Prototype),简称2P维度. 原型定义了一个调用的抽象形式.假定要做上门送餐业务,每个"商户"是个对象,取名为"Store",那么一个"查询商户列表"接口,可以

webservciescxf框架之客户端与服务端实例详解

webservciescxf框架之客户端与服务端实例详解 可以关注我之前发的文章,那是采用jdk发布服务并且使用wsimpor来生成客户端的. 但本文采用的是soap1.2协议,而wsimport仅对soap1.1协议有效,所以,本文采用的是 cxf框架提供的wsdl2java 来生成客户端,如下: wsdl2java -d . http://127.0.0.1/framework?wsdl 另外,需要强调的是wsdl2java工具(axis好像也提供了)既支持soap1.1协议,也支持soap

《连载 | 物联网框架ServerSuperIO教程》- 12.服务接口的开发,以及与云端双向交互

1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架ServerSuperIO教程>2.服务实例的配置参数说明 <连载 | 物联网框架ServerSuperIO教程>- 3.设备驱动介绍 <连载 | 物联网框架ServerSuperIO教程>-4.如开发一套设备驱动,同时支持串口和网络通讯. <连载 | 物联网框架ServerSupe

3-JEESZ分布式框架--REST服务接口文档

1. REST介绍 REpresentational State Transfer (REST) 是一种架构原则,其中将 web 服务视为资源,可以由其 URL 唯一标识. RESTful Web 服务的关键特点是明确使用 HTTP 方法来表示不同的操作的调用. REST 的基本设计原则对典型 CRUD 操作使用 HTTP 协议方法: POST - 创建资源 GET - 检索资源 PUT – 更新资源 DELETE - 删除资源 REST 服务的主要优势在于: 它们是跨平台 (Java..net

当当网开源Dubbox,扩展Dubbo服务框架支持REST风格远程调用

当当网近日开源了Dubbox项目,可为Dubbo服务框架提供多项扩展功能,包括REST风格远程调用.Kryo/FST序列化等等. 当当网架构部和技术委员会架构师沈理向InfoQ中文站介绍了Dubbox项目,开发背景和主要特点描述如下: Dubbo是一个被国内很多互联网公司广泛使用的开源分布式服务框架,即使从国际视野来看应该也是一个非常全面的SOA基础框架.作为一个重要的技术研究课题,在当当网我们根据自身的需求,为Dubbo实现了一些新的功能,并将其命名为Dubbox(即Dubbo eXtensi

Spring Boot 2 整合 Dubbo 框架 ,实现 RPC 服务远程调用

一.Dubbo框架简介 1.框架依赖 图例说明: 1)图中小方块 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor 代表层或模块,蓝色的表示与业务有交互,绿色的表示只对 Dubbo 内部交互. 2)图中背景方块 Consumer, Provider, Registry, Monitor 代表部署逻辑拓扑节点. 3)图中蓝色虚线为初始化时调用,红色虚线为运行时异步调用,红色实线为运行时同步调用. 4)图中只包含 RPC

【SpringBoot】使用IDEA创建一个SpringBoot服务,并创建三个restful风格的接口

项目创建: 选择创建一个springboot项目: 输入一下项目信息,一般就是maven的信息填一下: 选择spring web starter: 然后finish就OK了. 编码: 演示的功能就是提供一个计数器功能,可以初始化计数器,修改计数器,查询计数器当前值.没有使用数据库,直接用一个单例类来模拟了,项目结构如下: Count: 1 package com.me.redis.resouce.bean; 2 3 public class Count { 4 private int count

【DDD】领域驱动设计实践 —— 架构风格及架构实例

概述 DDD为复杂软件的设计提供了指导思想,其将易发生变化的业务核心域放置在限定上下文中,在确保核心域一致性和内聚性的基础上,DDD可以被多种语言和多种技术框架实现,具体的框架实现需要根据实际的业务场景和需求来制定. 核心的指导思路归纳为: 关注点放在domain上,将业务领域限定在同一上下文中 降低上下文之间的依赖,通过‘开发主机服务’(REST服务是其中的一种).‘消息模式’.‘事件驱动’等架构风格实现 遵循分层架构模式 架构风格 针对DDD的架构设计,<实现领域驱动设计>提到了几种架构风

.Net 与 Java 的服务接口相互调用

本文介绍.Net 与 Java 相互调用的例子.下面的介绍主要包括三方面:一是通过常用Web服务进行相互调用,二是使用TCP/IP套接字进行相互调用,三是使用Remote实现远程对象相互调用. 首先说一下Web服务的来源,Web服务是一种新的Web应用程序分支,可以执行从简单的请求到复杂商务处理等任何功能.一旦部署以后,其他Web服务应用程序可以发现并调用它部署的服务. Web Service是一种应用程序,它可以使用标准的互联网协议,像超文件传输协议(HTTP).简单对象访问协议(SOAP).