过去的 年间一个趋势主导了商业软件工具的开发用复杂性对抗复杂性这一趋势在任何地方都没有比在分布式计算领域更明显C 和 Java; 社区已经看到一些惊人复杂的框架被构建出来支持分布式通信分布式计算环境(DCE)支持用 C 语言编写的应用程序之间的远程过程调用公共对象请求代理架构(CORBA)标准支持面向对象应用程序之间的通信企业 JavaBean(EJB)规范提供安全性持久性事务消息和远程的服务对各个框架的宣传甚嚣尘上但是这些框架都没有满足预期有些甚至因为它们的复杂性而成为灾难在这些框架中只有 EJB 属于大力简化的结果有潜力在分布式应用程序上成功市场可能给也可能不给这个面临强敌的框架另一个空间但 EJB 仍然需要交付使用 最新的大型分布式框架是 Web 服务Web 服务技术让应用程序可以用平台独立或编程语言独立的方式相互通信Web 服务标准也受到复杂性恶魔的威胁但是称作 REST 的替代策略承诺了更简单的方式本文介绍了如何在 Ruby on Rails 中添加 REST 风格的 Web 服务并从 Ruby 和 Java 代码调用服务 关于本系列在 跨越边界 系列中作者 Bruce Tate 推动了这样一个概念今天的 Java 程序员通过学习其他方法和语言可以受益编程阵营中 Java 技术是所有开发项目最佳选择的情况已经变了其他框架正在改变 Java 框架构建的方式从其他语言学到的概念有助于 Java 编程编写的 Python(或 RubySmalltalk 或 ……)代码可以改变 Java 编码的方式 本系列介绍了与 Java 开发有根本不同但是却直接适用的编程概念和技术在某些情况下需要集成这些技术以利用它在其他情况下将可以直接应用这些概念单独的工具不如其他语言和框架中影响 Java 社区的开发人员框架甚至基本方法的思想重要 Web 服务领域 就像 EJBCORBA 和 DCE 一样Web 服务的核心抽象也是远程过程调用Web 服务利用叫做 SOAP(最初SOAP 代表简单对象存取协议但是这个术语现在降级了)的协议用 XML 表示消息的结构这里有一个技巧如果协议用代表简单的 S 开始那它就不简单Web 服务定义语言(WSDL)提供了服务的标准规范像 SOAP 一样WSDL 也是一个棘手而复杂的 API而 SOAP 和 WSDL 仅仅涉及到了构成 Web 服务这个大怪物的众多 API 的表面Web 服务需要一次大修感谢 Roy Fielding 的一份有影响的博士论文Web 服务得到了大修 Fielding 的论文描述了 REST 应用程序联网策略REST 与全堆栈 Web 服务根本不同主要原因有三个 REST 的核心抽象是远程资源而不是远程过程调用 REST 没有发明一个详尽的标准列表而是采用现有的 Internet 标准包括 HTTPXML 和 TCP/IP REST 没有覆盖每个可能场景而是覆盖了最常见的问题 请把 REST 想像成浏览REST 客户使用与浏览器相同的 HTTP 命令访问资源当 REST 客户访问到资源的表示时客户转换到一个状态使用不同的 HTTP 命令REST 客户可以创建读取更新或删除资源的记录 例如以典型的博客为例通过输入 URL例如 得到贴子的列表然后如果想编辑博客条目可以在 URL 中输入 HTTP 参数(例如 /edit?article=)然后显示编辑表单由于每个博客条目都有自己的 URL所以点击链接或直接输入 URL就可以用 HTTP 命令读取修改或删除内容 简而言之REST 可以 用 TCP/IP 命名标准命名 Web 上的资源 用 HTTP 查询和操纵这些资源 使用基于文本的标准消息格式(例如 XML 或 HTML)来构造数据 Ruby on Rails 用 REST 对 Web 服务提供了优秀的支持 Action Web Services 概述 Rails 用叫做 Action Web Services 的模块实现 Web 服务许多开发框架鼓励视图和 Web 服务使用独立的控制器这个策略可以维护控制器之间的风格一致问题是针对所服务的每种内容都需要一个新控制器例如Ajax 用户界面要求从控制器取得到 JavaScript 的远程 XML 调用 不必为 Web 服务专门分配一个控制器使用 Rails可以通用地用同一个控制器向基于 HTML 的视图基于 XML 的 Web 服务和基于 XML 的 JavaScript 组件提供内容理解 Action Web Services 的最好方式就是在工作应用程序的环境下查看它的实际作用 请用自己选择的数据库管理器创建一个叫做 service_development 的数据库接下来用以下命令创建 Rails 项目和模型 > rails service > script/generate model Person
在生成模型之后就有了一个叫做 db/migrate/_create_peoplerb 的迁移请把这个迁移编辑成像清单 一样 清单 people 表的迁移 class CreatePeople < ActiveRecord::Migration def selfup create_table :people do |t| lumn :first_name :string :limit => lumn :last_name :string :limit => lumn :email :string :limit => lumn :phone :string :limit => end end def selfdown drop_table :people endend
把 config/databaseyml 中的数据库配置修改成与自己的数据库配置匹配并输入 rake migrate 最后输入 script/generate scaffold Person People 为 Person 模型和 People 控制器生成工作台现在可以用 script/server 启动服务器了请把浏览器指向 localhost:/people以看到针对 Person 的经典的 Rails 脚手架图 显示了带有标准 Rails 脚手架的应用程序 图 简单的 Rails 应用程序 在我介绍 Rails 的 Web 服务之前请查看控制器代码编辑 app/controllers/people_controllerrb使之与清单 的代码匹配 清单 PeopleController 的控制器代码 class PeopleController < ApplicationController def index list render :action => list end # GETs should be safe (see ) verify :method => :post :only => [ :destroy :create :update ] :redirect_to => { :action => :list } def list @person_pages @people = paginate :people :per_page => end def show @person = Personfind(params[:id]) end def new @person = Personnew end def create @person = Personnew(params[:person]) if @personsave flash[:notice] = Person was successfully created redirect_to :action => list else render :action => new end end def edit @person = Personfind(params[:id]) end def update @person = Personfind(params[:id]) if @personupdate_attributes(params[:person]) flash[:notice] = Person was successfully updated redirect_to :action => show :id => @person else render :action => edit end end def destroy Personfind(params[:id])destroy redirect_to :action => list endend
如果跟着做过这个系列以前的 Ruby on Rails 项目就会知道典型的控制器方法的一般流程是 用户通过跟随链接或指定 URL通过 HTTP 发送请求 Web 服务器根据域的配置把请求转给 Ruby on Rails Rails 路由器根据 URL 模式把请求路由给控制器默认模式是//主机名/控制器/动作/参数 路由器用与动作相同的参数调用控制器上的方法 动作参数为视图设置实例变量并呈现视图 动作方法把实例变量拷贝到视图
例如请看 清单 中的 show 方法控制器设置视图使用的 @person 实例变量因为方法没有指定视图的名称所以 Rails 用与控制器动作相同的名称调用视图 —— 在这个示例中视图位于 app/views/people/showrhtml 再来看 list 方法如果想让这个方法呈现 XML需要 删除分页 把 people 实例变量转换成 XML 呈现 XML 而不是 HTML Rails 使得处理 Web 服务和呈现来自同一 Web 服务的视图成为可能实际上也不需要分页为了把 Web 服务的 list 方法简化一些可以把控制器中的 list 方法变成像清单 一样清除分页还需要删除靠近 app/views/people/listrhtml 代码底部的 Next Page 和 Previous Page 链接 清单 简化 list
def list @people = Personfind_allend
由于删除了分页也就删除了让用户界面更健壮的一个特性但是又得到了一些回报可以用相同的代码来驱动 Web 服务和视图如果日后发现需要分页可以编写一些定制的助手 现在基本应用程序出来了可以添加一些 Web 服务了 向 Rails 控制器添加 Web 服务 如果我想说大话我可以说 现在已经有了一个 Web 服务记得我对 REST 说过什么?这种风格的 Web 服务使用指定的资源我的 Rails 应用程序也具有指定的资源host_name/people/list 调用我的 list 服务REST 风格的 Web 服务也使用 TCP/IP 和 HTTP我的 Rails 应用程序就是这么做的而且格式良好的 HTML 就是 XML 的子集也满足最后一条 REST 要求只需在 localhost:/people/list 上调用 HTTP get 并解析结果就可以得到人员列表这就是关键REST 的工作方式与 Internet 的工作方式一样但这并不是真正基于 REST 的 Web 服务理想情况下应当提供反映 Person 含义的 XML 文档而不是用户界面的结构 真正的服务应当产生纯数据的表示一个专门针对服务的预期客户而构建的表示但是示例应用程序有两个客户终端用户和 REST 客户要为两个目的重用相同的代码需要给 Rails 提供更多信息Rails 的设计者可能决定使用额外的 URL 参数但是处理 URL 可是一项费劲的工作Rails 不应当用这些细节增加用户负担相反HTTP 提供了指定更多信息的工具HTTP 头 要理解 Web 服务的 REST 模型了解一点 HTTP 是有帮助的curl (请把它想像成 查看 URL)命令允许用一个命令查询 URL并查看响应基于 Unix 的操作系统默认包含 curl 可以为其他操作系统下载免费的 curl 工具通过输入 //someurl 可以将请求限制成只输出默认的响应体(浏览器呈现的 HTML)输入 curl //someurl 可以得到更多信息这个命令返回 HTTP 头如清单 所示可以看到头配置由表示每个请求的配置的键值对组成 清单 用 curl 调用 HTTP 请求 > curl i AliveDate: Tue Jun :: GMTContentType: text/html; charset=UTFServer: WEBrick/ (Ruby//)ContentLength: SetCookie: _session_id=defbcdd; path=/
后面将频繁地看到 HTTP get put post 和 delete 命令REST 利用达些命令执行经典的 CRUD(CRUD 是create readupdate 和 delete 的共同缩写)HTTP 命令到 CRUD 的映射是这样的 Create(创建)HTTP put Read(读取)HTTP get Update(更新)HTTP post Delete(删除)HTTP delete 浏览器利用 HTTP 头通过相同的服务器端代码来满足不同类型的请求行为良好的应用程序提供正确处理文档的充足信息其中一条信息叫做 HTTP Accept 头只要多花一点力气控制器就能利用一些助手用 Accept 头决定如何响应进入的请求然后控制器可以呈现适当的响应请把 PeopleController 中的 list 方法改成像清单 一样 清单 扩展 list方法以呈现 XML def list # wants is determined by the http Accept header in the request @people = Personfind_all respond_to do |wants| l wantsxml { render :xml => @peopleto_xml } end end
在清单 中可以看到完整的基于 REST 的 Web 服务生成的代码是 Rails 中小型的特定于域的语句的优美示例它扩展 Ruby 以构造一种 switch 语句它的工作方式是这样的 respond_to 方法接受单个代码块并传递一个实例变量(标为 wants )到代码块
wants 对每个可能的类型都有一个方法控制器可以为控制器期望的每个类型指定一个代码块
如果方法名称与 HTTP Accept 头中的类型匹配wants 方法执行对应的代码块 如果没有指定代码块(例如 l )Rails 就执行默认动作(在这个示例中呈现 app/views/people/listrhtml)
这个策略允许在所有预期的客户之间共享相同的设置代码如果需要添加期望 HTML 的 JavaScript 客户以便让应用程序支持 Ajax只需要添加 wantsjs如清单 所示 清单 为 JavaScript 客户呈现 HTML def list # wants is determined by the http Accept header in the request @people = Personfind_all respond_to do |wants| l wantsjs wantsxml { render :xml => @peopleto_xml } end end
现在已经看到了如何向只读的方法中添加 REST Web 服务show 方法也类似如清单 所示 清单 实现 show
def show @person = Personfind(params[:id]) respond_to do |wants| l wantsxml { render :xml => @personto_xml } end end
您可能已经注意到通过 REST 看到的只有只读服务原因是让应用程序处理提交和删除所需要的工作比较少删除不需要额外的支持因为当前的代码已经用 URL 指定了要删除的人的 IDRails 自动转换 post 请求中进入的 XML所以不需要构建任何服务器端支持实际上应用程序不用变就能删除更新和创建可以修补每个方法呈现的 HTTP 响应但是客户代码实际就在 HTTP 返回码之后 现在是调用 Web 服务的时候了 调用 Web 服务 使用现有 HTTP 协议这一策略使得调用变得简单清单 显示了 Ruby 版本请注意 HTTP Accept 头记住控制器根据这个头决定内容的类型 清单 从 Ruby 调用服务 require net/httpNet::HTTPstart(localhost ) do |http| response = httpget(/people/list Accept => text/xml) #Do something with the response puts Code: #{de} puts Message: #{ssage} puts Body:\n #{responsebody}end
清单 中的 Web 服务调用在//localhost:/people/list 上调用 HTTP get 方法并输出响应Ruby 有很好的库可以处理生成的 XML但是它们超出了本文的范围不需要用 Ruby 调用这个服务只需要 HTTP 的库清单 显示这个服务的 Java 调用 清单 用 Java 代码调用服务 package comrapidredws;import *;import javaio*;public class SimpleGet { void get() { try { URL url = new URL(//localhost:/people/list); URLConnection urlConnection = urlopenConnection(); urlConnectionsetRequestProperty(accept text/xml); BufferedReader in = new BufferedReader(new InputStreamReader(urlConnectiongetInputStream())); String str; while ((str = inreadLine()) != null) { Systemoutprintln(str); } inclose(); } catch (Exception e) { Systemoutprintln(e); } }
像其 Ruby 等价物一样这个代码打开一个 URL 连接把 Accept 头设置成 text/xml 发出 get 并输出结果Java 代码有许多 XML 框架但是我在这个示例中硬编码了 XML以保持示例简单 post 的调用也相似清单 显示了简单的 post
清单 用 Java 代码调用 HTTP post
void post() {try { String xmlText = <person> + <firstname>Maggie</firstname> + <lastname>Maggie</lastname> + <email></email> + </person>; URL url = new URL(//localhost:/people/create); HttpURLConnection conn = (HttpURLConnection)urlopenConnection(); connsetDoOutput(true); connsetRequestMethod(POST); connsetRequestProperty(ContentType text/xml); OutputStreamWriter wr = new OutputStreamWriter(conngetOutputStream()); wrwrite(xmlText); wrflush(); BufferedReader rd = new BufferedReader(new InputStreamReader(conngetInputStream())); String line; while ((line = rdreadLine()) != null) { Systemoutprintln(line); } wrclose(); rdclose(); } catch (Exception e) { Systemoutprintln(Error + e); }}
这个 HTTP post 通过在//localhost:/people/create 上调用 post 并在 HTTP 文档体中传递一个 XML 文档创建了一个新 Person (通常应当用 Java XML 库构建 XML 文档这次我还是硬编码了 XML 文档以保持示例简单)Rails 支持自动把进入的 XML 转换成 Person 属性的 Ruby 散列表 结束语 在本文中已经看到只用少量代码就使控制器支持基于 REST 的 Web 服务动态类型化的 Internet 语句例如 Ruby大量地利用 REST 代替基于 SOAP 的 Web 服务一些简单的调用包括漂亮的 responds_to 语法和对进入提交的自动 XML 转换使得可以容易地利用同一控制器处理 Web 服务远程 JavaScript 请求或 HTML Java 语言对 REST 也有非常好的支持毕竟servlet 实际上是服务器端基于 REST 的 Web 服务可以在 Java 端使用 servlet在 Ruby 端使用 Rails 控制器把利用两个平台优势的应用程序组合在一起这就是 Web 服务的漂亮之处您真正需要的所有东西就是超群出众的勇气 |