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

阅读数:10219 2016 年 12 月 15 日 02:34

核心要点

  • 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<Integer> 或 List<URL>。

方法可以返回任意的数字原始类型或包装类、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<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

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论