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类是一个用于实现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<Integer>或List<URL>。
方法可以返回任意的数字原始类型或包装类、boolean、java.lang.Boolean或java.lang.CharSequence,也可以返回java.util.List或java.util.Map实例。
结果会映射为对应的JSON类型,如下所示:
需要注意的是,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类允许服务方法返回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类,我们能够让服务方法高效地返回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类,我们能够让服务方法高效地返回任意游标所对应的内容。这个类实现了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<Double>)方法,将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