[译文]实现类Web应用灵活性的RESTful核心——第1部分
原文:A RESTful Core for Web-like Application Flexibility - Part 1
作者:Randy Kahle and Tom Hicks
引言
以绑定(binding)这样一个低层面问题来开始这一关于REST系统和RESTful设计方法的文章系列可能看起来很奇怪,毕竟,REST是源自于对为何万维网运作得如此良好的事后分析的一种“高层面”想法[1]。不过,如果你允许我们以这一层面为起点开始讨论的话,那么我想你已经了解了绑定是RESTful系统的一个重要方面。
什么是绑定?维基百科[2]定义为“值与标识符的关联”,并接着说明“绑定与辖域密切相关,因为作用域决定了绑定何时发生”,换句话说,绑定是代码中的实体与某个逻辑“名称”的关联,例如某个变量的内存位置或者某个方法的切入点。
作为开发者,我们是如此习惯于绑定这一概念以至于我们可能都没有非常认真地考虑过这个问题,我们写出如下的代码
for (i = 0; i < SIZE; i++) {...}
但不会想到用于存放for循环的递增值的物理内存位置,谁会关心它是位于地址B65A435中还是地址CD55D43D中呢?只要编程语言的抽象世界一日存在,我们就总是能够通过名称“i”来访问到这一位置。
在本文中,我们将简要地探讨一些绑定实现的方式,以及绑定方法的选择对系统灵活性和性能的影响。
Java中的绑定
Java虚拟机(Java Virtual Machine,JVM)的主要职能之一是查找并仅加载那些运行程序真正要用到的类,JVM在一个被称作动态链接的过程的执行期间完成这一功能。在编译Java源代码文件时,Java编译器创建的Java类文件包含了到彼此的符号引用以及到Java运行时类库的类文件的符号引用。在动态链接过程期间,JVM必须要解析类文件中的符号引用,并用到内存中的数据结构的直接引用来代替他们,这些数据结构代表了被加载的类。
虽然这一过程何时发生可以有一些灵活性,不过每一个JVM类加载器都会经历一个解析期(Resolution Phase),在这一期间其确定了每个符号引用(逻辑地址)与直接引用(物理地址)之间的映射。在这一问题解决之后,符号引用就会被直接引用代替,在使用引用的代码和内存位置之间创建一个直接的和永久的链接。注意到这样的一点很重要,即在Java中,符号引用的解析只发生一次,在这之后到符号的引用不再会被重新解析;他们只使用已经建立起来的直接引用。
举个例子来说,在下面的代码中,Date类的一个新实例的实例化过程可能会导致类加载器查找该类并把它加载到内存中(如果是第一次用到Date类的话),在堆的某处创建该类的一个实例,并解析代表了变量date到内存中适当位置的符号引用:
Date date = new Date();
不过随后对date的引用就不会再次启动类加载器的另一个解析过程了:
long time = date.getTime()
在开始考虑系统的可塑性问题之前,一切都很好,现在从客户和服务的角度来考虑这一代码,已经完成的程序假定了客户的角色,并且Date类实例就是该服务,这可能看起来没有什么问题,直到你提出这样的一个疑问:“当系统正在运行的时候,我如何使用更新了的实现来替换Date服务呢?”如果你需要附带地做这样的工作的话(比如说,你正在管理一个系统,你需要用更新了的代码来替换掉系统的某个部分),问题就出现了。对于传统的Java应用来说,这样做很困难,因为如我们前面的例子展示的那样,类已经被加载到内存中,且所有的符号引用已经被替换成直接引用。提供这种灵活性是有可能的,但是以增加了程序的复杂性(例如类加载和管理的代码)为代价的。
万维网中的绑定
作为比较,让我们来看一下一个有着令人难以置信的灵活性的系统中的绑定,这是至今为止创建的最大的也是最可靠的信息系统:万维网(World Wide Web)。
Web的成功部分归因于一条基本的经济学原理:人们能够以较低的“边际成本”来增加收益[3]。整个互联网的网站每天都在增加,已有的网站会更改他们的内容,以及一些网站会消失——所有这些情况的发生都不会中断互联网作为一个整体的存在,并且其所花费的每单位成本都很低。与在Java应用中所发现的相比较来看,这是一个根本与之无相同之处的计算命题,在Java应用中,类的改变可能会导致大量的工作(在代码库中到处传播API的改变、集成测试、部署、重新启动应用等)。
举例来说,如果Web像Java那样工作的话,那么人们可以随时添加web站点(就像Java程序按需引导新类的加载那样),然而,站点的任何修改或者更新都会要求Web的重启。当然,这有点夸张——不过有望提出这样的疑问:“为什么应用软件就不能够具有像Web那样的理想的特性呢?”
从另外一个角度来看,万维网可以被看作是不断响应着正在持续变化着的请求。像Web一样,最先进的软件系统也需要应对频繁变化的需求,那么,什么是所需要的呢?什么可以使得应用软件在这方面像Web那样灵活?
应用中的类Web绑定
既然Web符合吸引人的经济学原理以及有着极大的灵活性,有人可能会想知道我们是否能够通过把应用变得更加的“类似Web(web-like)”来把同样的这些原理纳入到我们的应用中,在应用软件的内部使用类web的绑定是否可能呢?如果是的话,哪些技术可以做到这一点?
在探讨这些问题之前,让我们先回顾一下Web是如何工作的。用户向客户端程序(大多数情况下是浏览器)指定了一个逻辑地址,浏览器接着向位于该地址上的资源表述(resource representation)发出请求,当资源表述被返回给浏览器时,浏览器“解释”这些表述,并且(通常)会为用户创建可见的界面显示。例如,输入逻辑地址“http://www.theserverside.com”就会引发浏览器请求该地址上的资源表述,返回的表述是一个嵌有JavaScript和到其他资源的链接的XHTML文档,浏览器还需要根据内嵌的资源链接发出请求,然后再把返回的表述组装成最终可见的界面表示。
从用户的角度来看,他们已经输入了一个全球范围内有效的地址,用户不知道在他们的浏览器和web服务器之间产生了一个临时的绑定,实际上,用户不知道,也不用理会,资源表述真正来自于哪里。
从技术上来说,所发生的情况如下:
1. 浏览器向域名服务器(Domain Name Service,DNS)提交一个针对“www.theserverside.com”的查询,然后取回作为该域地址的端点(endpoint)注册的服务器的IP地址。
2. 浏览器使用这一地址信息来建立一个到该地址的80端口的TCP/IP连接。
3. 浏览器使用该连接来发送一个HTTP协议请求以“取得(GET)”相对于“www.theserverside.com”的地址“/”上的资源。
4. 服务端接收该HTTP请求,并进行任何其认为需要的处理来获得或者创建某个资源表述,然后为指定的逻辑地址“/”返回该资源表述。
5. 浏览器接收该表述,处理后为用户创建一个可见的界面显示。
6. 浏览器丢弃TCP/IP连接,从服务端“解绑”[5]。
回到我们的问题上来,我们如何能够把这一行之有效的架构的这些理想特性纳入到应用软件中呢?在研究了Web的架构之后,我们注意到几个关键的概念:
客户端代码与服务端分离。
地址解析机制为给定的逻辑资源地址查找服务端。
服务端返回资源表述。
客户端与服务端之间的绑定是临时的,只在请求期间有效。
服务端了解他们正在处理的地址。
在应用软件中实现这些特性的一种方法是引入中介(Intermediary)这一概念,然后客户端代码可以发送请求给中介,中介能够解析到服务端的逻辑地址,把请求发送给服务端,接受返回的资源表述并把它传回给客户端代码,在这一周期结束之时,客户端和服务端的绑定被终止。
我们还需要客户端能够用来请求资源的逻辑地址,通用资源标识符(Universal Resource Identifiers,URI)[6]对Web来说运作良好,他们一样可以为我们所用。URI规范支持创建新的方案(scheme),因为现有的方案,例如http:、ftp:、等,都有相关的协议,并且已被用在应用之外以及应用之间,所有我们应该定义自己的方案。让我们稍随意地挑选一个方案,比如说“resource:”,有了这一URI方案,如果某个客户端想要所有顾客的表述的话,那么它可以创建并发生这样的请求给中介
resource:/customers/
接下来是中介要做的事情:
1. 解析到服务端的逻辑地址。
2. 发送请求到服务端。
3. 接收服务端返回的表述并把它发送回给发出请求的客户端。
4. 终止其在客户端和服务端建立的所有连接。
如果客户端和服务器端都是使用Java编写的话,那么他们看起来可能会是什么样子呢?客户端需要访问中介,因此,就目前来说,我们先认为中介是可以通过变量名“context”来引用的,那么客户端代码看起来有可能是这样的:
req = context.createRequest("resource:/customers/");
rep = context.issueRequest(req);
以及服务端的代码看起来有可能是这样的:
public void processRequest(Context context) throws Exception
{
Request request = context.getRequest();
...
Representation rep = // some code to create the representation
context.setResponse(rep);
}
很有意思,但是这是否能像Web一样有效地工作?似乎:
客户端发出了一个逻辑的资源请求,其并不知道将要处理请求的服务端的位置。
客户端和被解析的服务端之间的绑定只在请求处理期间存在。
服务端给客户端返回资源表述。
可以随时添加额外的服务——例如,相应于地址“resource:/accounts/…”的服务端,可以随时更新服务端,只要中介在进行更新的时候能够暂停转发请求到服务端。
这是一个好的开始……不过Web请求还包括一个指明了服务端正被请求的操作的类型的“动词”,例如HTTP协议,定义了一套为人熟知的“方法”(也就是那些动词),比如GET、POST、PUT等等。就我们的系统来说,让我们来引入一组稍加修改后的动词:SOURCE、SINK、NEW、EXISTS以及DELETE。
我们还应该就返回的资源表述的性质考虑得更加的深入一些,大多数的浏览器的目的是接收和处理现实中各种不同的表述类型(HTML、XHTML、PNG、GIFF、CSS、JavaScript等),不过假定应用软件客户端能够处理各种类型可能没有什么意义,为了解决强类型绑定这一问题,我们会允许我们的系统在处理资源表述方面有更大的自由度。
关键的想法是,客户端和服务端不需要商定返回的表述的类型,如果客户端不需要指定返回类型的话,那么服务端就可以自由的返回其选择的任何类型。如果客户端确实指定了返回类型而服务端却不能返回该类型的话,它依然会返回一些表述,在这种情况下,中介可以自由决定其是否把表述从提供的类型转换成请求的类型(例如,把XML片段转换成JSON表述)。最后,我们允许请求的结果是一个失败的返回,如果请求的类型无效或者不能创建自返回的表述的话。
让我们看看这些附加的因素是如何在客户端和服务端的代码中展现出来的:
Request req;
Representation rep;
req = context.createRequest("resource:/customers/");
req.setVerb(Request.SOURCE);
req.setType(List.class);
rep = context.issueRequest(req);
那么服务端的代码看起来可能是这个样子的:
public void processRequest(Context context) throws Exception
{
Request request = context.getRequest();
...
List customers = new LinkedList();
// 把顾客添加到列表中
context.setResponse(customers);
}
虽然这可能看起来像传统的Java应用中的消费者和提供者之间的联系,不过中介的转换资源表述的能力使得客户与服务器之间的绑定概念更为松散,例如,如果最后一个例子中的客户端代码如下所示的话:
req = context.createRequest("resource:/customers/");
req.setVerb(Request.SOURCE);
req.setType(DOM.class);
rep = context.issueRequest(req);
那么由服务返回的List类型便不符合所请求的DOM XML格式,在这种情况下,中介会查找能够把List类型表述转换成DOM类型表述的服务,并自动且透明地为客户端和服务端请求这一转换服务。这一表述形式的转换或称作“转换表述(transrepresentation)”提高了我们的系统在客户端和服务端之间的解耦程度,并增进了应用间相互组合的容易程度。
同时要注意的是,中介在客户端和服务端之间提供了完全的物理隔离——客户端永远不会访问服务端所在的物理位置,这是在典型的、受物理地址约束的Java应用中不会出现的情形。这种隔离性是通过逻辑绑定来实现的,是我们能够在应用软件中提供类Web特性的方法之一。
接下来的目标
虽然中介为我们的架构提供了其所具有的强大功能,但到目前为止,其还是相当的简单,正如你可能已经猜到的那样,可以增强中介自身来提供附加的功能,例如控制执行线程、转换表述类型、加载和管理Java类、支持其他编程语言,以及管理被用来解析地址的逻辑地址命名空间的规范和映射等。在接下来的文章中,我们将讨论其中的一些增强改进及其所带来的额外功能。
我们已经开始展示了如何构建一个开发应用软件的系统,而这些应用软件能够赢取到万维网所具有的那种经济效益。我们从一个较低的层面——绑定出发,讨论了一种使用了URI逻辑地址和解析到服务端地址的中介的方法,然后探讨了这种做法中的客户端和服务端的Java代码原型,最后我们讨论了服务端返回的表述类型与客户端请求的表述类型之间的匹配问题。
正如REST和RESTful设计方面的兴趣正在增长那样,我们会看到旨在支持这一方式的创新技术的出现,其中有些只是外表徒具RESTful风格,那么他们将会错过把REST应用到新应用的核心中所会带来的全部好处。在随后的文章中,我们将探索把REST全方位推向新应用的核心的方法,以及在实际中如何实现所追求的经济效益。
参考资料
[1] Roy Fielding,加州大学欧文分校博士论文,http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
[2] http://en.wikipedia.org/wiki/Name_binding
[3] 在经济学中,术语“边际成本”是指每增加一单位产量的成本,就Web这一情况来说,单位产量是指网站上的一个新网页或者甚至是整个新的网站。
[4] 浏览器会得到位于被请求地址上的信息的一个不可变副本。
[5] 一种可选的情况是,浏览器可能会保持连接的打开,以便有效的从同一服务端请求其他资源,这仅是一个性能上的增强,并不会改变这一逻辑模型。(这就类似于浏览器在短时间内“缓存”了到服务端的解析一样)
[6] http://www.ietf.org/rfc/rfc2396.txt
关于作者
Tom Hicks是位于亚利桑那州图森的Tohono Consulting公司的一位软件顾问,他的专长是企业架构以及一些使互联网“管道”系统功能更强大及更易于使用的技术。Tom拥有计算机科学和认知科学的研究生学位,对通过语言学汇合这些领域感兴趣。
Randy Kahle是1060 Research, Ltd的主管,这是一家致力于研究和实现RESTful系统效益的公司,在这之前,他曾在惠普、微软、MageLang研究所和Variantia工作过,他拥有Rice大学的CS和EE学士学位,以及达特茅斯塔克商学院的MBA学位。