[关闭]
@levinzhang 2016-12-14T21:23:30.000000Z 字数 8955 阅读 600

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

Posted by Greg Brown on Jul 12, 2016

摘要:

HTTP-RPC是一个开源框架,允许开发人员创建和访问跨平台多样化的RESTful Web服务,它采用了一种便利的、类似于RPC的做法,同时又保持了REST的基本原则,如无状态和统一资源访问。


核心要点

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类型,如下所示:

需要注意的是,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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注