级别: 初级 赵 雄伟 (zhaoxw@cn.ibm.com), 软件工程师, IBM 左 超 (zuochao@cn.ibm.com), 软件工程师, IBM
2009 年 6 月 18 日 Project Zero 是一个利用动态脚本语言来开发 Web2.0 应用的敏捷开发环境。它是一个开放的平台,可以很方便地对它进行扩展,最新的版本支持 Groovy 和 PHP 两种动态脚本语言。Ruby 脚本语言是目前主流的脚本语言之一,有广大的用户群。本文通过对 Project Zero 进行扩展,使它不仅能够支持 Ruby 脚本,而且能够利用 Ruby 脚本在 Project Zero 上提供 RESTful 服务。本文还阐述了 Project Zero 支持基础的 RESTful 服务的内部机制。
背景知识
本文针对的是有 Project Zero 和 Ruby 开发经验的人员,并对 RESTFul 服务有一定的了解。本文假设您在阅读之前已经了解 Project Zero 的基本情况,RESTful 服务的概念以及了解 Ruby 脚本的语法。您可以从“Project Zero 简介,第一部分:为 Web 应用程序构建 RESTful 服务 ”这篇文章中获得关于 Project Zero 和 RESTful 服务的基本概念。
RESTful 服务的特征及优点
在 Web 服务中,有两个最基本的问题,一是客户端如何把自己的意图传递给服务器;服务器如何知道该请求是要获得数据,删除数据,还是改写数据呢?服务器为什么要做这个操作,而不是那个操作呢?这个问题可以称为方法信息。二是客户端如何告诉服务器对哪些数据进行操作,假如服务器已经知道客户端要删除数据了,但它怎么知道客户端要删除的是哪些数据呢?为什么服务器要对这些数据,而不是那些数据进行操作呢?这个问题称为作用域信息。
根据对这两个问题处理方式的不同,可以区分一个 Web 服务是 RESTful 服务,还是基于 SOAP 的 Web 服务。
RESTful 服务是这样来处理上述的两个问题的:
1.方法信息放在 HTTP 方法中,常见的 HTTP 方法有 GET,PUT,POST,DELETE,Head
2.作用域信息放在 HTTP 域中,如放在 URI 路径里。
基于 SOAP 的 Web 服务提供了另一种解决方案:方法信息和作用域信息封装在 SOAP 中,用 XML 来描述方法名,参数,返回值等信息。这种方式在 Web 上把 HTTP 协议作为传输协议,SOAP 在 HTTP 协议之上传递。
基于 SOAP 的 Web 服务都采用自己的词汇(定义的函数名称都不一样)。而 RESTful Web 服务则是共用一套标准词汇,即 HTTP 方法,RESTful 服务里的每个对象都具有统一的基本接口。
RESTful 服务基于 HTTP 协议,充分利用了 HTTP 方法 ,URI,Cache 等 HTTP 协议作为应用协议所具备的能力,相比于基于 SOAP 的 Web 服务,减少了一层封装,更好得挖掘了 HTTP 协议的潜力,是一种轻量级的 Web 服务的实现方式。
RESTful 服务的优点有:
-
可以利用缓存 Cache 来提高响应速度
-
通讯本身的无状态性可以让不同的服务器的处理一系列请求中的不同请求,提高服务器的扩展性
-
浏览器即可作为客户端,简化软件需求
-
相对与其他叠加在 HTTP 协议 之上的机制,REST 的软件依赖性更小
-
不需要额外的资源发现机制
-
在软件技术演进中的长期的兼容性更好
Ruby 脚本语言的特点
Ruby 是一个完全面向对象的动态脚本语言,可以采用虚拟机实现跨平台应用。它的语法简洁,灵活,便于扩展,拥有功能强大和完善的标准类库。
JRuby 是面向 Ruby、基于 Java 虚拟机 (JVM) 的一种解释程序,它结合了 Ruby 语言的简易性和功能强大的 JVM 的执行机制,包括与 Java 库全面集成,可以直接调用 Java 和 Ruby 的资源。本文将采用 JRuby 作为 Ruby 脚本的实现方式。
Ruby 与 Project Zero 结合的优势
Ruby 语言是目前最流行的动态脚本语言之一,有大量的开发人员在基于 Ruby 进行 Web 2.0 和相关应用的开发。Project Zero 作为 Web2.0 的敏捷开发环境,提供了诸如安全性、数据库访问接口、视图层渲染接口等大量的平台底层支持。把 Ruby 语言引入到 Project Zero 中,可以无缝地利用 Project Zero 平台提供的底层支持,将 Ruby 语言动态、高效的能力和 Project Zero 敏捷的特性很好地结合在一起。
Project Zero 对 RESTful 服务有很好的支持,默认采用的实现技术是 Groovy 和 PHP。它对基于资源的 URI 映射和对 HTTP 协议的事件机制提供了内部支持。Ruby 语言能够很方便地利用 Project Zero 的这些内建机制,同时结合 Ruby 强大的动态特性,从而快速敏捷地在 Project Zero 上构建起 RESTFul 服务。如图 1 所示客户端、外部应用、Ruby、Project Zero 以及底层数据库之间的关系。
图 1. 系统结构图
在 Project Zero 中用 Ruby 实现 RESTful 服务
在 Project Zero 中用 Ruby 实现基本的 RESTful 服务有以下步骤:
-
将 resource 的 HTTP URL 映射到 Ruby 脚本
-
为 Ruby 脚本配置 Interpreter
-
将 Project Zero 的事件映射到 Ruby 脚本中的事件响应函数
-
在 Ruby 脚本中利用 Zero 提供的持久化机制对数据库进行操作
-
将执行结果渲染到客户端
将 resource 的 HTTP URL 映射到 Ruby 脚本
这个过程需要两个配置:
为 resource 的 HTTP URL 配置事件响应的 handler
将处理资源的 script 的文件名后缀配置为“.rb”
为 resource 的 HTTP URL 配置事件响应的 handler
在 Zero 中,默认的 RESTful 的 URL 是用“/resources/”做前缀。Zero 会将“/resources/”为前缀的 URL 映射到 Zero 工程目录下的“/app/resources”,在这个目录中存放着对资源处理的脚本。脚本的文件名和资源名相同。
在 Zero 中合法的 RESTful 的 URL 有如下规则:
/resources/<collection name>[/<member identifier>[/<path info>]]
“collection name”指定了处理该 URL 的的 handler,“member identifier”和“path info”是可以选的,作为参数保存在 GlobalContext 的 request 域中。
注:URL 不能以“/”结尾,否则会抛出 URL 不合法的异常
Project Zero 用一种松耦合的事件机制来处理 HTTP 请求的事件和内部触发的事件。Zero 默认的处理 RESTful HTTP URL 的 Handler 是 zero.core.resource.ResourceHandler 类。在 zero.config 里可以配置事件的处理流程,将 HTTP URL 请求触发的事件映射到 ResourceHandler。
清单 1:zero.config 中对 ResourceHandler 的配置
# Resource handling
/config/handlers += {
"events" : ["GET", "POST", "PUT", "DELETE"],
"handler" : "zero.core.resource.ResourceHandler.class",
"conditions" : ["/request/path matches /resources/.+"]
}
|
拿这个配置项说明:“events”配置了要处理的事件,“handler”指定了处理上述事件的 Handler,“conditions”通过正则表达式指定了要对哪些模式的 URLs 进行拦截处理。
Zero 中将 RESTful Resource 分为 Collection 和 Member 两种,对这两种类型的资源,Zero 根据 HTTP Events 分别给它们定义了 Zero 内部调用的 Event 类型,见下表:
|
Resource
|
Get
|
Put
|
Post
|
Delete
|
|---|
|
Collection
|
list
|
putCollection
|
create
|
deleteCollection
| |
Member
|
retrieve
|
update
|
postMember
|
delete
|
ResourceHandler 负责将 HTTP Event 转化为 Zero 内部定义的 Event,并调用 EventEngine.fire()方法,将事件传递给后端的 EventDispatcher 来处理。
将处理资源的 script 的文件名后缀配置为“.rb”
Zero 默认的对 resource 处理的脚本语言是 Groovy,可以在 zero.config 中配置。只需要做如下配置,就可以将 RESTful HTTP URL 映射到 Ruby 脚本中:
清单 2:zero.config 中对 RESTful 服务采用的默认脚本文件后缀名的配置
/config/resources/defaultExtensions = [".rb"]
|
Zero Core API 中的 zero.core.resource.ResourceResolver 类负责从配置文件中读取“/config/resources/defaultExtension”配置项,然后结合资源名称拼出一个脚本文件名,Zero 会检测这个脚本文件是否存在,如果不存在,将抛出“No resource handler found”异常。如果存在这个目标脚本文件,就将脚本文件的物理路径保存到 HandlerInfo 中。
zero.core.events.HandlerInfo 封装了将要执行的目标脚本的文件路径以及 HTTP request 中保存的数据和事件触发后设置的一些数据。它的数据将被传递到脚本解释器中。
为 Ruby 脚本配置 Interpreter
Zero 提供了一个动态脚本语言扩展的接口:zero.core.Interpreter。这个接口只提供了一个方法:
清单 3:Zero Core API 中 Interpreter 接口的方法
public void invoke(HandlerInfo handlerInfo);
|
Zero 默认提供的 Interpreter 实现类 GroovyInterpreter 支持对 Groovy 脚本的处理。我们要想在 Zero 中使用 Ruby 脚本,就要提供处理 Ruby 脚本的 Interpreter,这里我们提供一个 JRubyRestfulInterpreter 类来实现 Interpreter 接口,这个类将对“.rb”后缀的脚本文件进行处理。需要在 zero.config 中做如下配置将 JRubyRestfulInterpreter 集成到 Zero 中来。
清单 4:zero.config 中针对 Ruby 脚本的 Interpreter 实现类 JRubyRestfulInterpreter 的配置
/config/interpreters/.rb = "zero.extend.ruby.JRubyRestfulInterpreter"
|
zero.core.interprerter.InterpreterFactory 负责从 zero.config 配置文件中读取这段配置,并实例化 JRubyRestfulInterpreter 对象。InterpreterFactory 是在 zero.core.events.dispatcher.EventDispatcher 中被调用的。
Zero 在处理 RESTful URL 时内部的基本调用流程如下:
图 2.Zero 处理 RESTful URL 时内部的基本调用流程
注:Zero 读取配置文件时准循“First one win”的原则,会先读取当前工程的 zero.config 配置项信息,再读取所依赖工程的 zero.config 配置项信息,以最先读到的配置为准。
将 Project Zero 的事件映射到 Ruby 脚本中的事件响应函数
JRuby 能够在 Ruby 脚本中调用 Java API,从而可以获得 Zero 的全局上下文,因此本文采用 JRury 来编写 Ruby 脚本。关于 JRuby 的更多信息请查看文章末尾的参考资料。
Ruby 语言有很强大的反射机制,因此本文在处理“将 Zero 的事件映射到 Ruby 脚本中的事件响应函数”时采用如下策略:
1.创建一个 JRubyRestfulDispatcher.rb 脚本,使用 JRuby 类来作为映射控制器,可以充分利用 Ruby 的反射机制,动态地实现对 Ruby 函数的调用 , 同时又能访问到 Zero 的 GlobalContext 中的数据,从而可以很简洁地将 Zero 定义的事件映射到 Ruby 的事件响应函数中。
2.在 JRubyRestfulInterpreter 解释器类中执行 JRubyRestfulDispatcher.rb 脚本。用 Java 调用 JRuby 脚本有多种方式,本文采用 Apache 提供的开源的脚本引擎:Apache BSF (Bean Script Framework)来执行 JRuby 脚本。关于 BSF 的更多信息请查看文章末尾的参考资料。
JRubyRestfulInterpreter 解释器完成的任务如下:
-
读取 JrubyRestfulDispatcher.rb 脚本文件的内容
-
使用 BSF 的脚本引擎执行 JRuby 脚本
清单 5: JRubyRestfulInterpreter.java
public class JRubyRestfulInterpreter implements Interpreter {
private static final String JRUBYRESTFULDISPATHER = "jrubyRestfulDispatcher.rb";
public void invoke(HandlerInfo handlerInfo) {
restfulInvoke(handlerInfo);
}
public void restfulInvoke(HandlerInfo handlerInfo) {
//load "jrubyRestfulDispatcher.rb" script
BufferedReader br = new BufferedReader(new InputStreamReader(getClass()
.getResourceAsStream(JRUBYRESTFULDISPATHER)));
StringBuffer sb = new StringBuffer();
try {
String str;
while ((str = br.readLine()) != null) {
sb.append(str).append("\n");
}
BSFManager.registerScriptingEngine("ruby",
"org.jruby.javasupport.bsf.JRubyEngine",
new String[] { "rb" });
BSFManager manager = new BSFManager();
//use BSFManager execute "jrubyRestfulDispatcher.rb" script
manager.exec("ruby", "(java)", 1, 1, sb.toString());
} catch (IOException e) {
e.printStackTrace();
} catch (BSFException ex) {
ex.printStackTrace();
}
GlobalContext.zput("/request/status", 200);
}
}
|
JrubyRestfulDispatcher.rb 脚本完成的任务主要有:
-
从 Zero 的 GlobalContext 中取得要执行的目标脚本的文件名
-
通过 Ruby 的反射机制,创建目标脚本的 Ruby 类实例
-
从 Zero 的 GlobalContext 中取得要响应的事件,利用 Ruby 的反射机制,将 Event 映射到目标 Ruby 类实例的事件响应方法上。
-
进行异常处理,如果发生异常,就调用 Zero 的 ViewEngine 将异常渲染到客户端
清单 6:JrubyRestfulDispatcher.rb 脚本
require "java"
include_class "zero.core.context.GlobalContext"
include_class "zero.core.views.ViewEngine"
class JrubyRestfulDispatcher
# convert a string to a class
def class_for_name(class_name)
return eval(class_name.capitalize).new
end
def get_method_name(event_name)
return "on" + event_name.capitalize
end
# get the target script name from GlobalContext
def get_target_script_name
path = GlobalContext.zget('/request/path') + '/'
resource_path = path[11,path.length]
return resource_path[0,resource_path.index('/')]
end
# dispatch the request to a resource handler
def do_dispatch
require get_target_script_name
object = class_for_name(get_target_script_name)
target_method = get_method_name(GlobalContext.zget('/event/_name'))
if(object.respond_to?(target_method))
object.send(target_method)
else
GlobalContext.zput("/request/view","error")
GlobalContext.zput("/request/status","404")
GlobalContext.zput("/request/error/message","No Method Found")
ViewEngine.render()
end
end
end
|
这里我们按照惯例将 Ruby 的事件响应函数名默认为: on + 事件名,针对与 Zero 的 Resources Event,有 8 个默认的事件响应函数:
针对 Collection 资源的事件响应函数有:onList,onPutCollection,onCreate,onDeleteCollection
针对 Member 资源的事件响应函数有: onRetrieve,onUpdate,onPostMember,onDelete
在 Ruby 脚本中利用 Zero 提供的持久化机制对数据库进行操作
Zero 的 Core API 中提供了 zero.data.Manager 类封装了对数据库的访问操作,如对数据的增删改查的方法。要想在 Ruby 脚本中使用 Zero 平台提供的数据库访问能力,需要做一些改进。因为 JRuby 脚本编译后,脚本中的字符串类型会转化成 RubyString 类型,而 zero.data.Manager 并没有提供对 RubyString 的支持,所以需要提供一个代理类,将 RubyString 转化为 Java 的 String 类型。本文提供了一个 zero.data.ruby.Manager 类,部分代码如下:
清单 7:zero.data.ruby.Manager.java 代码片段
public class Manager {
private zero.data.Manager manager;
public static zero.data.ruby.Manager create(String dbKey) {
Manager manager = new Manager();
manager.manager = zero.data.Manager.create(dbKey);
return manager;
}
…………
public Map<String, Object> queryFirst(RubyString sql) {
String newSql = sql.toString();
Map<String, Object> result = manager.queryFirst(newSql, null);
return result;
}
public List<Map<String, Object>> queryList(RubyString sql) {
String newSql = sql.toString();
List<Map<String, Object>> result = manager.queryList(newSql, null);
return result;
}
………………
}
|
将执行结果渲染到客户端
Zero 平台提供了两种方式来将执行结果渲染到客户端。一种是直接采用 OutputStream 或 PrintWriter 将结果写到 HTTP response 中,比如:
清单 8:Zero 采用 OutputStream 向客户端发送 HTTP response
PrintWriter writer = (PrintWriter) zget("/request/writer");
writer.println("Hello World.");
|
另外一种就是利用 Zero 提供的 ViewEngine API 来创建不同形式的 Response。目前提供了 View,Error,JSON,XML 四种 renders。
调用一个 render 需要以下步骤:
-
指定相应的 render,这个值在 Global Context 的“/request/view”项中设置
-
把和 render 相关的数据设置到 Global Context 中
-
调用 ViewEngine.render()方法
本文将采用 JSON render 将结果集以 JSON 的方式渲染到客户端。
比如如下代码就是 ViewEngine 的基本调用方式。
清单 9:Zero 采用 ViewEngine 向客户端发送 HTTP response
GlobalContext.zput("/request/view","JSON")
GlobalContext.zput("/request/json/output",result)
ViewEngine.render()
|
至此我们已经可以将 RESTful HTTP URL 映射到相应的 Ruby 脚本中,根据请求的事件映射到 Ruby 脚本的事件响应函数中,在 Ruby 的方法中使用 Data Manager 来访问数据库,使用 ViewEngine 将结果用多种形式返回给客户端。这样就完成了一个基本的 RESTful Web 服务的流程。
示例场景:
接下来我们通过一个简单的示例来展示上述对 Zero 的扩展如何应用到实际的工程中。示例的内容主要有:
创建一个 Zero 应用程序,定义一个 Customer 资源,然后利用 Ruby 脚本为这个资源提供 RESTful 服务。
创建一个示例应用
实例具体的步骤如下:
在数据库中创建 Customer 表,提供测试数据
本示例中采用 Zero 自带的 Derby 数据库,数据库名为 PRO_DB。示例的 Customer 表的 SQL 脚本如下:
清单 10:create.sql
CREATE TABLE CUSTOMER (
CUSTOMERID INTEGER NOT NULL GENERATED ALWAYS AS
IDENTITY(START WITH 1, INCREMENT BY 1),
FIRSTNAME VARCHAR(50) NOT NULL,
LASTNAME VARCHAR(50) NOT NULL,
COUNTRY VARCHAR(250),
DESCRIPTION VARCHAR(250)
)
INSERT INTO CUSTOMER(FIRSTNAME,LASTNAME,COUNTRY,DESCRIPTION)
VALUES ('Simpson','Homer','USA','He is a Ruby Programmer');
INSERT INTO CUSTOMER(FIRSTNAME,LASTNAME,COUNTRY,DESCRIPTION)
VALUES ('Simpson','Maggie','USA','She is a Groovy Programmer');
INSERT INTO CUSTOMER(FIRSTNAME,LASTNAME,COUNTRY,DESCRIPTION)
VALUES ('Simpson','Bart','USA','He is a Java Programmer');
INSERT INTO CUSTOMER(FIRSTNAME,LASTNAME,COUNTRY,DESCRIPTION)
VALUES ('Simpson','Lisa','USA','She is a C++ Programmer');
|
配置 Zero 工程的 Java Build Path
本示例采用了 JRuby 作为 Ruby 的实现语言,利用 Apache BSF 脚本引擎在 Java 中调用 JRuby 脚本,所以需要在 classpath 中引入这两个 jar 文件。同时需要引入我们对 Zero 扩展的 jar 文件。本文对 Project Zero 所做的扩展已经打包成 jar 文件,可以从文后的附件中获得。Java Build Path 配置如下:
图 3. 示例程序的 Java Build Path 配置
配置 zero.config
在工程的 config/zero.config 文件中添加使 Zero 支持 Ruby 脚本的配置
清单 11:示例程序的 config/zero.config 配置文件
# HTTP port (default is 8080)
/config/http/port = 8080
/config/db/PRO_DB = {
"class": "org.apache.derby.jdbc.EmbeddedDataSource",
"databaseName": "PRO_DB",
"user": "APP",
"password": "APP"
}
/config/resources/defaultExtensions = [".rb"]
/config/interpreters/.rb = "zero.extend.ruby.JRubyRestfulInterpreter"
|
实现 customer.rb 脚本
customer.rb 需要写事件响应函数,来处理 Zero 内部定义的 Resource 事件。本例展示了如何获得 Collection 和 Member 资源。
onList 方法来处理如下的 HTTP URL :http://localhost:8080/resources/customer,响应 GET customer collection 的事件。
onRetrieve 方法来处理如下的 HTTP URL :http://localhost:8080/resources/customer/1,响应 GET customer member 的事件。
清单 12:customer.rb 脚本
require "java"
include_class "zero.data.ruby.Manager"
include_class "zero.core.context.GlobalContext"
include_class "zero.core.views.ViewEngine"
include_class "zero.json.Json"
class Customer
def onRetrieve
data = Manager.create('PRO_DB');
id = GlobalContext.zget("/request/params/customerId")
result = data.queryFirst("select c.customerId,c.firstname,c.lastname,
c.description
from customer as c where c.customerId = #{id}");
if(result)
GlobalContext.zput("/request/view","JSON")
GlobalContext.zput("/request/json/output",result)
ViewEngine.render()
else
GlobalContext.zput("/request/view","error")
GlobalContext.zput("/request/status","404")
GlobalContext.zput("/request/error/message","No Result Found")
ViewEngine.render()
end
end
def onList
data = Manager.create('PRO_DB')
result = data.queryList("select * from customer")
if(result)
GlobalContext.zput("/request/view","JSON")
GlobalContext.zput("/request/json/output",result)
ViewEngine.render()
else
GlobalContext.zput("/request/view","error")
GlobalContext.zput("/request/status","404")
GlobalContext.zput("/request/error/message","No Result Found")
ViewEngine.render()
end
end
………………
end
|
在 customer.rb 中可以从 GlobalContext 中获得 request 的参数,可以调用 zero.extend.data.Manager 来访问数据库,可以调用 Zero 的 ViewEengine 将 Json 对象返回到客户端。
执行结果
采用 Firefox 的 Poster 插件,可以模拟 HTTP 的访问,本文的测试基于 Poster 插件,更多关于 Poster 的内容可以参考文后的资料。
访问 Customer Collection 资源:http://localhost:8080/resources/customer
图 4. 用 Poster 插件向服务器发送 http://localhost:8080/resources/customer 请求
返回结果:
图 5. 服务器针对 http://localhost:8080/resources/customer 请求返回的 response 信息,返回的是一个 JSON 对象的数组
访问一个 Customer member 资源:http://localhost:8080/resources/customer/1
图 6. 用 Poster 插件向服务器发送 http://localhost:8080/resources/customer/1 请求
返回结果:
图 7. 服务器针对 http://localhost:8080/resources/customer/1 请求返回的 response 信息,返回的是一个 JSON 对象
结束语:
本文介绍了 RESTful 服务的基本概念,Project Zero 提供 RESTful 服务时内部的事件流,以及如何在 Project Zero 进行扩展使其支持用 Ruby 脚本编写的 RESTful 服务,并结合一个简单的示例展示了如何利用我们提供的 Zero 扩展用 Ruby 脚本开发 Zero 的 RESTful 服务。您还了解到了如何利用 BSF 脚本引擎在 Java 中调用 Ruby 脚本。
参考资料 学习
获得产品和技术
作者简介  | |  | 赵雄伟是一位 IBM 软件工程师,工作在 IBM 中国软件开发实验室企业应用开发部门,现在正从事企业电子商务应用的开发和支持工作,在 Java开发和Web开发方面有丰富的经验,特别对 Web 应用的架构及 SOA、SCA 等有浓厚的兴趣,您可以通过 zhaoxw@cn.ibm.com与他联系。 |
 | |  | 左超是一位 IBM 软件工程师,工作在 IBM 中国软件开发实验室企业应用开发部门,目前从事企业电子商务应用的开发,您可以通过 zuochao@cn.ibm.com 与他联系。 |
对本文的评价
|