核心要点
- 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
方法可以返回任意的数字原始类型或包装类、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
// 创建服务 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
评论