HTTP-RPC:轻量级跨平台REST框架

2016 年 12 月 15 日

核心要点

  • HTTP-RPC 是一个开发 RESTful 应用的轻量级开源框架,同时符合 RPC 隐喻的做法;
  • 提供了服务端和客户端的 API;
  • 支持各种操作系统和设备;
  • 支持多种语言,包括 Java、Objective-C/Swift 和 JavaScript。

HTTP-RPC 是一个开源框架,致力于简化基于 REST 的应用开发。它允许开发人员创建和访问基于 HTTP 的 Web 服务,这个过程会使用便利的、类似于 RPC 隐喻的做法,同时还能保留基础的 REST 理念,比如无状态和统一资源访问。

目前,这个项目支持使用 Java 来实现 REST 服务,使用 Java、Objective-C/Swift 或 JavaScript 来消费服务。相对于基于 Java 的更大的 REST 框架,服务端组件提供了一种轻量级的替代方案,对于微服务和物联网(Internet of Things,IOT)应用来说,这是一个理想的选择。统一的跨平台客户端 API 能够使与服务的交互变得非常容易,不用关心目标设备或操作系统是什么。

概览

HTTP-RPC 服务要通过 HTTP 动作来进行访问,比如对目标资源的 GET 或 POST 请求。目标是通过路径来进行指定的,路径代表了资源的名称,通常会使用一个名词来组成 URL,比如 /calendar 或 /contacts。

参数会通过查询字符串或类似于 HTML 表单那样的请求体的方式来提供。结果通常会返回 JSON 格式,当然不返回任何值的操作也是支持的。

例如,如下的请求将会得到两个数字的和,这两个数字分别是通过 a 和 b 这两个查询参数指定的:

GET /math/sum?a=2&b=4除此之外,参数值也可以通过一个列表来指定,而不是两个固定的变量:

GET /math/sum?values=1&values=2&values=3在这两种情况下,服务都会在响应中返回 6 这个值。

POST、PUT 和 DELETE 操作的行为与之类似。

实现服务

HTTP-RPC 的服务端库是以一个 JAR 文件的形式来进行分发的,这个库只有 32KB,并没有外部的依赖。它包含了如下的包 / 类:

org.httprpc

WebService——HTTP-RPC 所提供的 RPC 服务的抽象基础类,为其添加注解就能指定“远程方法调用”或服务方法。

org.httprpc.beans

BeanAdapter——适配器类,将 Java Bean 实例的内容呈现为一个 Map,适用于序列化为 JSON 的场景。

org.httprpc.sql

ResultSetAdapter——适配器类,它代表了 JDBC 结果集的内容,将其作为一个可迭代的列表,适用于将流(streaming)转换为 JSON。

Parameters——用于简化预处理语句(prepared statement)执行的类。

org.httprpc.util

IteratorAdapter——适配器类,它以一个可迭代列表的形式展现迭代器中的内容,适用于将流(streaming)转换为 JSON。

上述的每个类都会在后文中进行更详细的讨论。

WebService 类

WebService 类是一个用于实现 HTTP-RPC Web 服务的基础抽象类。我们定义服务操作的方式就是为某个具体的服务实现添加公开方法。

@RPC 注解用来标记某个方法可以进行远程访问。这个注解会为方法关联一个 HTTP 动作和资源路径。当服务发布之后,所有带有注解的公开方法将会自动允许远程执行。

例如,如下的类可以用于实现我们前文所述的简单加法操作:

复制代码
public class MathService extends WebService {
@RPC(method="GET", path="sum")
public double getSum(double a, double b) {
return a + b;
}
@RPC(method="GET", path="sum")
public double getSum(List<Double> values) {
double total = 0;
for (double value : values) {
total += value;
}
return total;
}
}

注意,上面的两个方法都会映射到“/math/sum”路径上。具体执行哪个方法,要根据所提供参数值的名称来确定。例如,如下的请求将会调用第一个方法:

GET /math/sum?a=2&b=4如下的请求将会调用第二个方法:

GET /math/sum?values=1&values=2&values=3### 方法参数与返回类型

方法参数可以是任意的数字原始类型或包装类、boolean、java.lang.Boolean 或 java.lang.String。参数也可以是 java.net.URL 或 java.util.List 实例。URL 参数代表了二进制内容,比如 JPEG 或 PNG 图片。List 参数则代表了多个值的参数,List 中的元素可以是任意支持的简单类型,比如 List或 List

方法可以返回任意的数字原始类型或包装类、boolean、java.lang.Boolean 或 java.lang.CharSequence,也可以返回 java.util.List 或 java.util.Map 实例。

结果会映射为对应的 JSON 类型,如下所示:

  • java.lang.Number 或数字原始类型:number
  • java.lang.Boolean 或 boolean 原始类型:true/false
  • java.lang.CharSequence:string
  • java.util.List:array
  • java.util.Map:object

需要注意的是,List 和 Map 类型并不需要支持随机存取(random access),只需要支持迭代就可以。另外,实现了 java.lang.AutoCloseable 的 List 和 Map 类型在它们的值写入到输出流之后,将会自动关闭。这样的话,服务的实现就能够以流的方式来响应数据,而不是在写入之前预先将其缓冲在内存中。

例如,org.httprpc.sql.ResultSetAdapter 类包装了一个 java.sql.ResultSet 实例,将它的内容暴露为可向前移动( forward-scrolling)、自动关闭的 map 值的列表。关闭这个列表将会自动关闭底层的结果集,从而确保数据库资源不会泄露。

ResultSetAdapter 稍后还会详细讨论。

请求元数据

WebService 提供了如下的方法,允许它的扩展类获取当前请求的附加信息:

getLocale()——返回当前请求相关的地域信息;

getUserName()——返回当前请求相关的用户名,如果请求没有认证过的话,会返回 null;

getUserRoles()——返回一个集合,代表了用户所属的角色,如果用户没有认证过的话,会返回 null。

这些方法所返回的值都是由受保护的 setter 方法注入的,对于每个服务请求,这些 setter 方法只会调用一次。这些 setter 方法的本意是不希望由应用程序的代码调用的,但是它们有助于对服务实现进行单元测试。

BeanAdapter 类

BeanAdapter 类允许服务方法返回 Java Bean 对象,并对其内容进行转换。这个类实现了 Map 接口,并将 Bean 中的属性暴露为 Map 中的条目,允许自定义的数据类型序列化为 JSON。

例如,如下的 Bean 类可能会用来代表一组值的基本统计数据:

复制代码
public class Statistics {
private int count = 0;
private double sum = 0;
private double average = 0;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public double getSum() {
return sum;
}
public void setSum(double sum) {
this.sum = sum;
}
public double getAverage() {
return average;
}
public void setAverage(double average) {
this.average = average;
}
}

使用这个类的 getStatistics() 方法,可能会如下所示:

复制代码
@RPC(method="GET", path="statistics")
public Map<String, ?> getStatistics(List<Double> values) {
Statistics statistics = new Statistics();
int n = values.size();
statistics.setCount(n);
for (int i = 0; i < n; i++) {
statistics.setSum(statistics.getSum() + values.get(i));
}
statistics.setAverage(statistics.getSum() / n);
return new BeanAdapter(statistics);
}

尽管值实际上存储在强类型的 Statistics 对象中,但是 adapter 能让数据看起来像 map 一样,这样的话,就能以 JSON 对象的形式将数据返回给调用者。

需要注意的是,如果某个属性返回的是嵌套的 Bean 类型,那么该属性的值将会自动包装为一个 BeanAdapter 实例。除此之外,如果属性返回的是 List 或 Map 类型,那么这个值将会包装到对应类型的 adapter 之中,自动化地包装其子元素。这样的话,就允许服务方法返回递归的结构,比如树形结构的数据。

BeanAdapter 能够非常便利地将 JPA 查询的结果转换为 JSON。该地址的样例展现了如何组合使用BeanAdapter 与Hibernate。

ResultSetAdapter 和 Parameters 类

借助 ResultSetAdapter 类,我们能够让服务方法高效地返回 SQL 查询的结果。这个类实现了 List 接口,让 JDBC 结果集中的每一行都以 Map 实例的形式进行展现,这样的话,数据非常适于序列化为 JSON 格式。它还实现了 AutoCloseable 接口,能够保证底层的结果集可以正常关闭,避免数据库资源的泄露。

ResultSetAdapter 只能向前移动,它的内容无法通过 get() 和 size() 方法来获取。这样的话,结果集内容可以直接返回给调用者,不需要任何的中间缓冲。调用者只需简单地执行 JDBC 查询,将得到的结果集传递给 ResultSetAdapter 的构造器,并返回该 adapter 实例即可:

复制代码
@RPC(method="GET", path="data")
public ResultSetAdapter getData() throws SQLException {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select * from some_table");
return new ResultSetAdapter(resultSet);
}

Parameters 类提供了一种执行预处理语句的方式,这个过程中会使用命名的参数值(named parameter value)而不是使用参数的索引。与在 JPQL 中类似,参数名称会通过“:”字符来指定,样例如下:

复制代码
SELECT * FROM some_table
WHERE column_a = :a OR column_b = :b OR column_c = COALESCE(:c, 4.0)

借助 parse() 方法,我们可以根据 SQL 语句来创建 Parameters 实例。这个方法会接受一个 java.io.Reader 类型的参数,其中包含了 SQL 的文本,样例如下:

Parameters parameters = Parameters.parse(new StringReader(sql));通过 Parameters 类的 getSQL() 方法,能够返回根据标准 JDBC 语法所解析的 SQL:

复制代码
SELECT * FROM some_table
WHERE column_a = ? OR column_b = ? OR column_c = COALESCE(?, 4.0)

这个值用来创建实际的预处理语句:

PreparedStatement statement = DriverManager.getConnection(url).prepareStatement(parameters.getSQL());参数值会通过 apply() 方法应用到 SQL 语句之中。这个方法的第一个参数就是预处理语句,第二个参数是一个 map,包含了语句中的变量:

复制代码
HashMap arguments = new HashMap();
arguments.put("a", "hello");
arguments.put("b", 3);
parameters.apply(statement, arguments);

显式的创建和注入参数 Map 看上去会很繁琐,因此 WebService 类提供了如下的静态便利方法来简化 Map 的创建过程:

复制代码
public static <K> Map<K, ?> mapOf(Map.Entry<K, ?>... entries) { ... }
public static <K> Map.Entry<K, ?> entry(K key, Object value) { ... }

通过使用这些便利方法,填充参数值的代码可以简化为:

parameters.apply(statement, mapOf(entry("a", "hello"), entry("b", 3)));在参数填充完成之后,语句就可以执行了:

return new ResultSetAdapter(statement.executeQuery());该地址中的样例展现了关于如何通过ResultSetAdapter 和Parameters 类访问MySQL 数据库。

IteratorAdapter 类

借助 IteratorAdapter 类,我们能够让服务方法高效地返回任意游标所对应的内容。这个类实现了 List 接口,能够将迭代器生成的每个元素序列化为 JSON,包括嵌套的 List 和 Map 结构。与 ResultSetAdapter 类似,IteratorAdapter 实现了 AutoCloseable 接口。如果底层的迭代器类型也实现了 AutoCloseable 接口的话,IteratorAdapter 会确保底层的游标会关闭,这样的话,资源不会产生泄露。

与 ResultSetAdapter 相同,IteratorAdapter 只能向前移动,所以它的内容无法通过 get() 和 size() 方法进行访问。这样就允许将游标的内容直接返回给调用者,无需任何的中间缓冲。

IteratorAdapter 通常会用来序列化 NoSQL 数据库所产生的结果数据,比如 MongoDB 所产生的数据。该地址的样例展现了组合使用IteratorAdapter 和Mongo 的例子。

消费服务

HTTP-RPC 客户端库提供了一致的接口,能够实现跨多平台的服务操作调用。例如,如下的代码片段展现了 Java 客户端的 WebServiceProxy 类,它可以用来访问之前所讨论的数学计算服务方法。在代码中,我们首先创建了一个 WebServiceProxy 实例,并通过一个线程池对其进行配置,这个池中包含了 10 个用来执行请求的线程。然后,它会调用服务的 getSum(double, double) 方法,并为参数“a”传递 2,为参数“b”传递 4。最后,它执行了 getSum(List) 方法,将 1,2,3 作为参数传递了进来。与前面章节讨论的 WebService 类相似,WebServiceProxy 提供了静态的工具方法,帮助我们简化参数映射的创建过程:

复制代码
// 创建服务
URL serverURL = new URL("https://localhost:8443");
ExecutorService executorService = Executors.newFixedThreadPool(10);
WebServiceProxy serviceProxy = new WebServiceProxy(serverURL, executorService);
// 得到“a”和“b”的和
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), new ResultHandler<Number>() {
@Override public void execute(Number result, Exception exception) {
// 结果是 6
}
});
// 得到所有值的和
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("values", listOf(1, 2, 3))), new ResultHandler<Number>() {
@Override public void execute(Number result, Exception exception) {
// 结果是 6
}
});

结果处理器(result handler)是一个回调,在请求完成的时候就会调用它。在 Java 7 中,通常会使用匿名内部类来实现结果处理器。在 Java 8 之后,可以使用 lambda 表达式来替代,从而将调用代码缩减成如下所示:

复制代码
// 得到“a”和“b”的和
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), (result, exception) -> {
// 结果是 6
});
// 得到所有值的和
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("values", listOf(1, 2, 3))), (result, exception) -> {
// 结果是 6
});

如下的样例阐述了如何通过 Swift 代码来访问数学计算服务。这里会有一个 WSWebServiceProxy 实例,默认的 URL 会话会为其提供支撑功能,还有一个代理队列(delegate queue)支持 10 个并发操作,我们通过它们来执行远程方法调用。结果处理器是通过闭包实现的:

复制代码
// 配置会话
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.requestCachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData
let delegateQueue = NSOperationQueue() delegateQueue.maxConcurrentOperationCount = 10
let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)
// 初始化服务代理并调用方法
let serverURL = NSURL(string: "https://localhost:8443")
let serviceProxy = WSWebServiceProxy(session: session, serverURL: serverURL!)
// 得到“a”和“b”的和
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["a": 2, "b": 4]) {(result, error) in
// 结果是 6
}
// 得到所有值的和
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["values": [1, 2, 3]]) {(result, error) in
// 结果是 6
}

最后,这个样例阐述了如何通过 JavaScript 客户端来访问服务。我们使用 WebServiceProxy 实例来调用方法,并使用闭包来实现结果处理器:

复制代码
// 创建服务代理
var serviceProxy = new WebServiceProxy();
// 得到“a”和“b”的和
serviceProxy.invoke("GET", "/math/sum", {a:4, b:2}, function(result, error) {
// 结果是 6
});
// 得到所有值的和
serviceProxy.invoke("GET", "/math/sum", {values:[1, 2, 3, 4]}, function(result, error) {
// 结果是 6
});

更多信息

本文介绍了 HTTP-RPC 框架并提供了一些样例,展示了如何通过它来便利地创建 RESTful Web 服务 ,并通过 Java、Objective-C/Swift 和 JavaScript 消费 Web 服务。这个项目目前在 GitHub 上开发,并且非常活跃,将来还会提供对其他平台的支持。我们鼓励读者的反馈,也欢迎为其贡献功能。

关于它的更多信息,请参见项目的 README 页面或通过 gk_brown@verizon.net 联系作者。

关于作者

Greg Brown是一名软件工程师,在咨询、产品以及开源开发方面有着 20 年以上的经验。他目前的关注点在于移动应用和 REST 服务。

查看英文原文: HTTP-RPC: A Lightweight Cross-Platform REST Framework

2016 年 12 月 15 日 02:3410566

评论

发布
暂无评论
发现更多内容

技术实践丨PostgreSQL开启Huge Page场景分析

华为云开发者社区

数据库 管理 内存

JAVA稳定底层,快速开发首选,XJR智能化客户关系管理

Marilyn

敏捷开发 快速开发 软件架构 客户关系管理

问题篇:附源码询问Pageable实现分页无法使用原生sql

小Q

Java 学习 架构 面试 springboot

《Linux学习笔记》从常用命令、常用操作到网络管理、性能优化,无论是Java开发或是运维都可以学习!

Java架构之路

Java 程序员 架构 面试 编程语言

区块链是连接传统经济和数字经济的桥梁

CECBC区块链专委会

区块链 数字经济

LeetCode题解:78. 子集,迭代+位运算,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

USDT支付系统源码,承兑支付系统平台开发搭建

135深圳3055源中瑞8032

区块链钱包应用开发,数字货币钱包系统

135深圳3055源中瑞8032

【高并发】导致并发编程频繁出问题的“幕后黑手”

冰河

并发编程 多线程 高并发 高性能 异步

天呐!价值2980元Java成神面试题竟在Github开源了

996小迁

Java 学习 架构 面试

深度对比Apache CarbonData、Hudi和Open Delta三大开源数据湖方案

华为云开发者社区

hadoop 开源 数据处理

如何获取变量token的值

测试人生路

软件测试 接口测试

十八般武艺玩转GaussDB(DWS)性能调优:总体调优策略

华为云开发者社区

数据库 性能 调试

合约自动跟单软件开发,API合约跟单系统

135深圳3055源中瑞8032

Netty源码解析 -- 零拷贝机制与ByteBuf

binecy

Netty 源码剖析

DeFi流动性挖矿系统开发技术方案

薇電13242772558

区块链 defi

刚从蚂蚁金服Java研发岗面试回来(三轮游),我总结的面试经历(附面试题+答案)

Java架构追梦

Java 架构 面试 蚂蚁金服 面试题总结

Java程序员必须人手一本的《码出高效:Java 开发手册》,免费分享PDF文档

Java架构之路

Java 程序员 架构 面试 编程语言

区块链将构建数字社会高效的全球网络

CECBC区块链专委会

数字经济 数字时代

与其思考公司该为员工提供什么福利,不如思考有哪些 “福利” 不应该提供!

非著名程序员

个人成长 管理 福利

数字货币交易所源码,币币交易系统搭建

135深圳3055源中瑞8032

Vidyo独特的互联网适应性

dwqcmo

音视频会议 集成架构 解决方案 智能硬件

一个有趣的问题——孙庞猜数

小七

Python 数学

真香!天天996进不去阿里?看5年苦逼程序猿怎么逆袭阿里P7

小Q

Java 学习 架构 面试 程序猿

架构师训练营 - 第二周课后练习

joshuamai

测试悄然扩围 千万元红包搅活数字货币江湖

CECBC区块链专委会

数字人民币

架构师第一期作业(第 6 周)

Cheer

面试官问我:看过sharding-jdbc的源码吗?我吧啦吧啦说了一通!!

冰河

分布式事务 微服务 分布式数据库 系统架构 中间件

企业级RPC框架zRPC

Kevin Wan

go RPC microser

在阿里内部,做Java到金字塔顶端的人平时都如何学习源码?

小Q

Java 学习 架构 面试 程序猿

解惑“高深”的Kafka时间轮原理,原来也就这么回事!

华为云开发者社区

中间件 消息队列

HTTP-RPC:轻量级跨平台REST框架-InfoQ