跨越邊界 系列中以前的文章說(shuō) Ruby on Rails 是一個(gè)突然流行起來(lái)的框架,充當著(zhù) Ruby 編程語(yǔ)言的催化劑。隨著(zhù) Ruby 的經(jīng)驗不斷成功,開(kāi)發(fā)人員開(kāi)始尋求把他們的 Ruby 應用程序與用其他語(yǔ)言編寫(xiě)的應用程序集成。Rails 對 Web 服務(wù)提供了優(yōu)秀的支持。本文介紹 Rails 中的 Web 服務(wù),重點(diǎn)放在一個(gè)名為 Representational State Transfer (REST) 的策略上。
過(guò)去的 20 年間,一個(gè)趨勢主導了商業(yè)軟件工具的開(kāi)發(fā):用復雜性對抗復雜性。這一趨勢在任何地方都沒(méi)有比在分布式計算領(lǐng)域更明顯。C 和 Java™ 社區已經(jīng)看到一些驚人復雜的框架被構建出來(lái)支持分布式通信。分布式計算環(huán)境(DCE)支持用 C 語(yǔ)言編寫(xiě)的應用程序之間的遠程過(guò)程調用。公共對象請求代理架構(CORBA)標準支持面向對象應用程序之間的通信。企業(yè) JavaBean(EJB)規范提供安全性、持久性、事務(wù)、消息和遠程的服務(wù)。對各個(gè)框架的宣傳甚囂塵上,但是這些框架都沒(méi)有滿(mǎn)足預期,有些甚至因為它們的復雜性而成為災難。在這些框架中,只有 EJB 3.0 屬于大力簡(jiǎn)化的結果,有潛力在分布式應用程序上成功。市場(chǎng)可能給、也可能不給這個(gè)面臨強敵的框架另一個(gè)空間,但 EJB 仍然需要交付使用。
最新的大型分布式框架是 Web 服務(wù)。Web 服務(wù)技術(shù)讓?xiě)贸绦蚩梢杂闷脚_獨立或編程語(yǔ)言獨立的方式相互通信(請參閱 參考資料)。Web 服務(wù)標準也受到復雜性惡魔的威脅,但是稱(chēng)作 REST 的替代策略承諾了更簡(jiǎn)單的方式。本文介紹了如何在 Ruby on Rails 中添加 REST 風(fēng)格的 Web 服務(wù),并從 Ruby 和 Java 代碼調用服務(wù)。
![]() |
|
就像 EJB、CORBA 和 DCE 一樣,Web 服務(wù)的核心抽象也是遠程過(guò)程調用。Web 服務(wù)利用叫做 SOAP(最初,SOAP 代表簡(jiǎn)單對象存取協(xié)議,但是這個(gè)術(shù)語(yǔ)現在降級了)的協(xié)議,用 XML 表示消息的結構。這里有一個(gè)技巧:如果協(xié)議用代表簡(jiǎn)單的 S 開(kāi)始,那它就不簡(jiǎn)單。Web 服務(wù)定義語(yǔ)言(WSDL)提供了服務(wù)的標準規范。像 SOAP 一樣,WSDL 也是一個(gè)棘手而復雜的 API,而 SOAP 和 WSDL 僅僅涉及到了構成 Web 服務(wù)這個(gè)大怪物的眾多 API 的表面(請參閱 參考資料)。Web 服務(wù)需要一次大修,感謝 Roy Fielding 的一份有影響的博士論文,Web 服務(wù)得到了大修(請參閱 參考資料)。
Fielding 的論文描述了 REST 應用程序聯(lián)網(wǎng)策略。REST 與全堆棧 Web 服務(wù)根本不同,主要原因有三個(gè):
請把 REST 想像成瀏覽。REST 客戶(hù)使用與瀏覽器相同的 HTTP 命令訪(fǎng)問(wèn)資源。當 REST 客戶(hù)訪(fǎng)問(wèn)到資源的表示時(shí),客戶(hù)轉換到一個(gè)狀態(tài)。使用不同的 HTTP 命令,REST 客戶(hù)可以創(chuàng )建、讀取、更新或刪除資源的記錄。
例如,以典型的博客為例。通過(guò)輸入 URL,例如 blog.rapidred.com,得到貼子的列表。然后,如果想編輯博客條目,可以在 URL 中輸入 HTTP 參數(例如 blog.rapidred.com/edit?article=12345),然后顯示編輯表單。由于每個(gè)博客條目都有自己的 URL,所以點(diǎn)擊鏈接或直接輸入 URL,就可以用 HTTP 命令讀取、修改或刪除內容。
簡(jiǎn)而言之,REST 可以:
Ruby on Rails 用 REST 對 Web 服務(wù)提供了優(yōu)秀的支持。
![]() ![]() |
![]()
|
Rails 用叫做 Action Web Services 的模塊實(shí)現 Web 服務(wù)。許多開(kāi)發(fā)框架鼓勵視圖和 Web 服務(wù)使用獨立的控制器。這個(gè)策略可以維護控制器之間的風(fēng)格一致。問(wèn)題是針對所服務(wù)的每種內容,都需要一個(gè)新控制器。例如,Ajax 用戶(hù)界面要求從控制器取得到 JavaScript 的遠程 XML 調用。
不必為 Web 服務(wù)專(zhuān)門(mén)分配一個(gè)控制器,使用 Rails,可以通用地用同一個(gè)控制器向基于 HTML 的視圖、基于 XML 的 Web 服務(wù)和基于 XML 的 JavaScript 組件提供內容。理解 Action Web Services 的最好方式就是在工作應用程序的環(huán)境下查看它的實(shí)際作用。
請用自己選擇的數據庫管理器創(chuàng )建一個(gè)叫做 service_development 的數據庫。接下來(lái),用以下命令創(chuàng )建 Rails 項目和模型:
> rails service > script/generate model Person |
在生成模型之后,就有了一個(gè)叫做 db/migrate/001_create_people.rb 的遷移。請把這個(gè)遷移編輯成像清單 1 一樣:
class CreatePeople < ActiveRecord::Migration def self.up create_table :people do |t| t.column :first_name, :string, :limit => 40 t.column :last_name, :string, :limit => 40 t.column :email, :string, :limit => 40 t.column :phone, :string, :limit => 15 end end def self.down drop_table :people end end |
把 config/database.yml 中的數據庫配置修改成與自己的數據庫配置匹配,并輸入 rake migrate。最后,輸入 script/generate scaffold Person People,為 Person 模型和 People 控制器生成工作臺?,F在可以用 script/server 啟動(dòng)服務(wù)器了。請把瀏覽器指向 localhost:3000/people,以看到針對 Person 的經(jīng)典的 Rails 腳手架。圖 1 顯示了帶有標準 Rails 腳手架的應用程序:

在我介紹 Rails 的 Web 服務(wù)之前,請查看控制器代碼。編輯 app/controllers/people_controller.rb,使之與清單 2 的代碼匹配:
class PeopleController < ApplicationController def index list render :action => ‘list‘ end # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) verify :method => :post, :only => [ :destroy, :create, :update ], :redirect_to => { :action => :list } def list @person_pages, @people = paginate :people, :per_page => 10 end def show @person = Person.find(params[:id]) end def new @person = Person.new end def create @person = Person.new(params[:person]) if @person.save flash[:notice] = ‘Person was successfully created.‘ redirect_to :action => ‘list‘ else render :action => ‘new‘ end end def edit @person = Person.find(params[:id]) end def update @person = Person.find(params[:id]) if @person.update_attributes(params[:person]) flash[:notice] = ‘Person was successfully updated.‘ redirect_to :action => ‘show‘, :id => @person else render :action => ‘edit‘ end end def destroy Person.find(params[:id])。destroy redirect_to :action => ‘list‘ end end |
如果跟著(zhù)做過(guò)這個(gè)系列以前的 Ruby on Rails 項目,就會(huì )知道典型的控制器方法的一般流程是:
例如,請看 清單 2 中的 show 方法??刂破髟O置視圖使用的 @person 實(shí)例變量。因為方法沒(méi)有指定視圖的名稱(chēng),所以 Rails 用與控制器動(dòng)作相同的名稱(chēng)調用視圖 —— 在這個(gè)示例中,視圖位于 app/views/people/show.rhtml。
再來(lái)看 list 方法。如果想讓這個(gè)方法呈現 XML,需要:
people 實(shí)例變量轉換成 XML Rails 使得處理 Web 服務(wù)和呈現來(lái)自同一 Web 服務(wù)的視圖成為可能。實(shí)際上也不需要分頁(yè)。為了把 Web 服務(wù)的 list 方法簡(jiǎn)化一些,可以把控制器中的 list 方法變成像清單 3 一樣,清除分頁(yè)。還需要刪除靠近 app/views/people/list.rhtml 代碼底部的 “Next Page” 和 “Previous Page” 鏈接。
def list @people = Person.find_all end |
由于刪除了分頁(yè),也就刪除了讓用戶(hù)界面更健壯的一個(gè)特性,但是又得到了一些回報??梢杂孟嗤拇a來(lái)驅動(dòng) Web 服務(wù)和視圖。如果日后發(fā)現需要分頁(yè),可以編寫(xiě)一些定制的助手。
現在基本應用程序出來(lái)了,可以添加一些 Web 服務(wù)了。
![]() ![]() |
![]()
|
如果我想說(shuō)大話(huà),我可以說(shuō) “現在已經(jīng)有了一個(gè) Web 服務(wù)”。記得我對 REST 說(shuō)過(guò)什么?這種風(fēng)格的 Web 服務(wù)使用指定的資源。我的 Rails 應用程序也具有指定的資源:host_name/people/list 調用我的 list 服務(wù)。REST 風(fēng)格的 Web 服務(wù)也使用 TCP/IP 和 HTTP。我的 Rails 應用程序就是這么做的。而且格式良好的 HTML 就是 XML 的子集,也滿(mǎn)足最后一條 REST 要求。只需在 localhost:3000/people/list 上調用 HTTP get,并解析結果,就可以得到人員列表。這就是關(guān)鍵。REST 的工作方式與 Internet 的工作方式一樣。但這并不是真正基于 REST 的 Web 服務(wù)。理想情況下,應當提供反映 Person 含義的 XML 文檔而不是用戶(hù)界面的結構。
真正的服務(wù)應當產(chǎn)生純數據的表示,一個(gè)專(zhuān)門(mén)針對服務(wù)的預期客戶(hù)而構建的表示。但是示例應用程序有兩個(gè)客戶(hù):終端用戶(hù)和 REST 客戶(hù)。要為兩個(gè)目的重用相同的代碼,需要給 Rails 提供更多信息。Rails 的設計者可能決定使用額外的 URL 參數,但是處理 URL 可是一項費勁的工作。Rails 不應當用這些細節增加用戶(hù)負擔。相反,HTTP 提供了指定更多信息的工具:HTTP 頭。
要理解 Web 服務(wù)的 REST 模型,了解一點(diǎn) HTTP 是有幫助的。curl(請把它想像成 查看 URL)命令允許用一個(gè)命令查詢(xún) URL,并查看響應?;?Unix 的操作系統默認包含 curl,可以為其他操作系統下載免費的 curl 工具。通過(guò)輸入 curl http://some-url,可以將請求限制成只輸出默認的響應體(瀏覽器呈現的 HTML)。輸入 curl -i http://some-url 可以得到更多信息。這個(gè)命令返回 HTTP 頭,如清單 4 所示??梢钥吹筋^配置由表示每個(gè)請求的配置的鍵-值對組成。
> curl -i http://localhost:3000/people/list HTTP/1.1 200 OK Cache-Control: no-cache Connection: Keep-Alive Date: Tue, 27 Jun 2006 14:54:49 GMT Content-Type: text/html; charset=UTF-8 Server: WEBrick/1.3.1 (Ruby/1.8.4/2005-12-24) Content-Length: 854 Set-Cookie: _session_id=216912045de52786f032b22755c903dd; path=/ |
后面將頻繁地看到 HTTP get、put、post 和 delete 命令。REST 利用達些命令執行經(jīng)典的 CRUD(CRUD 是create, read、update 和 delete 的共同縮寫(xiě))。HTTP 命令到 CRUD 的映射是這樣的:
put get post delete 瀏覽器利用 HTTP 頭,通過(guò)相同的服務(wù)器端代碼來(lái)滿(mǎn)足不同類(lèi)型的請求。行為良好的應用程序提供正確處理文檔的充足信息。其中一條信息叫做 HTTP Accept 頭。只要多花一點(diǎn)力氣,控制器就能利用一些助手,用 Accept 頭決定如何響應進(jìn)入的請求。然后,控制器可以呈現適當的響應。請把 PeopleController 中的 list 方法改成像清單 5 一樣:
def list # wants is determined by the http Accept header in the request @people = Person.find_all respond_to do |wants| wants.html wants.xml { render :xml => @people.to_xml } end end |
在清單 5 中,可以看到完整的基于 REST 的 Web 服務(wù)。生成的代碼是 Rails 中小型的特定于域的語(yǔ)句的優(yōu)美示例,它擴展 Ruby 以構造一種 switch 語(yǔ)句。它的工作方式是這樣的:
respond_to 方法接受單個(gè)代碼塊,并傳遞一個(gè)實(shí)例變量(標為 wants)到代碼塊。 wants 對每個(gè)可能的類(lèi)型都有一個(gè)方法??刂破骺梢詾榭刂破髌谕拿總€(gè)類(lèi)型指定一個(gè)代碼塊。 Accept 頭中的類(lèi)型匹配,wants 方法執行對應的代碼塊。 wants.html),Rails 就執行默認動(dòng)作(在這個(gè)示例中,呈現 app/views/people/list.rhtml)。 這個(gè)策略允許在所有預期的客戶(hù)之間共享相同的設置代碼。如果需要添加期望 HTML 的 JavaScript 客戶(hù),以便讓?xiě)贸绦蛑С?Ajax,只需要添加 wants.js,如清單 6 所示:
def list # wants is determined by the http Accept header in the request @people = Person.find_all respond_to do |wants| wants.html wants.js wants.xml { render :xml => @people.to_xml } end end |
現在已經(jīng)看到了如何向只讀的方法中添加 REST Web 服務(wù)。show 方法也類(lèi)似,如清單 7 所示:
def show @person = Person.find(params[:id]) respond_to do |wants| wants.html wants.xml { render :xml => @person.to_xml } end end |
您可能已經(jīng)注意到,通過(guò) REST 看到的只有只讀服務(wù)。原因是:讓?xiě)贸绦蛱幚硖峤缓蛣h除所需要的工作比較少。刪除不需要額外的支持,因為當前的代碼已經(jīng)用 URL 指定了要刪除的人的 ID。Rails 自動(dòng)轉換 post 請求中進(jìn)入的 XML,所以不需要構建任何服務(wù)器端支持。實(shí)際上,應用程序不用變就能刪除、更新和創(chuàng )建??梢孕扪a每個(gè)方法呈現的 HTTP 響應,但是客戶(hù)代碼實(shí)際就在 HTTP 返回碼之后。
現在是調用 Web 服務(wù)的時(shí)候了。
![]() ![]() |
![]()
|
使用現有 HTTP 協(xié)議這一策略使得調用變得簡(jiǎn)單。清單 8 顯示了 Ruby 版本。請注意 HTTP Accept 頭。記住,控制器根據這個(gè)頭決定內容的類(lèi)型。
require ‘net/http‘ Net::HTTP.start(‘localhost‘, 3000) do |http| response = http.get(‘/people/list‘, ‘Accept‘ => ‘text/xml‘) #Do something with the response. puts "Code: #{response.code}" puts "Message: #{response.message}" puts "Body:\n #{response.body}" end |
清單 8 中的 Web 服務(wù)調用,在 http://localhost:3000/people/list 上調用 HTTP get 方法,并輸出響應。Ruby 有很好的庫可以處理生成的 XML,但是它們超出了本文的范圍。不需要用 Ruby 調用這個(gè)服務(wù)。只需要 HTTP 的庫。清單 9 顯示這個(gè)服務(wù)的 Java 調用:
package com.rapidred.ws; import java.net.*; import java.io.*; public class SimpleGet { void get() { try { URL url = new URL("http://localhost:3000/people/list"); URLConnection urlConnection = url.openConnection(); urlConnection.setRequestProperty("accept", "text/xml"); BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); String str; while ((str = in.readLine()) != null) { System.out.println(str); } in.close(); } catch (Exception e) { System.out.println(e); } } |
像其 Ruby 等價(jià)物一樣,這個(gè)代碼打開(kāi)一個(gè) URL 連接,把 Accept 頭設置成 text/xml,發(fā)出 get,并輸出結果。Java 代碼有許多 XML 框架(請參閱 參考資料),但是我在這個(gè)示例中硬編碼了 XML,以保持示例簡(jiǎn)單。
post 的調用也相似。清單 10 顯示了簡(jiǎn)單的 post:
void post() { try { String xmlText = "<person> " + "<first-name>Maggie</first-name>" + "<last-name>Maggie</last-name>" + "<email>maggie@tate.com</email>" + "</person>"; URL url = new URL("http://localhost:3000/people/create"); HttpURLConnection conn = (HttpURLConnection)url.openConnection(); conn.setDoOutput(true); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "text/xml"); OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); wr.write(xmlText); wr.flush(); BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line; while ((line = rd.readLine()) != null) { System.out.println(line); } wr.close(); rd.close(); } catch (Exception e) { System.out.println("Error" + e); } } |
這個(gè) HTTP post 通過(guò)在 http://localhost:3000/people/create 上調用 post,并在 HTTP 文檔體中傳遞一個(gè) XML 文檔,創(chuàng )建了一個(gè)新 Person。(通常應當用 Java XML 庫構建 XML 文檔。這次我還是硬編碼了 XML 文檔,以保持示例簡(jiǎn)單。)Rails 支持自動(dòng)把進(jìn)入的 XML 轉換成 Person 屬性的 Ruby 散列表。
![]() ![]() |
![]()
|
在本文中,已經(jīng)看到只用少量代碼,就使控制器支持基于 REST 的 Web 服務(wù)。動(dòng)態(tài)類(lèi)型化的 Internet 語(yǔ)句,例如 Ruby,大量地利用 REST 代替基于 SOAP 的 Web 服務(wù)。一些簡(jiǎn)單的調用,包括漂亮的 responds_to 語(yǔ)法和對進(jìn)入提交的自動(dòng) XML 轉換,使得可以容易地利用同一控制器處理 Web 服務(wù)、遠程 JavaScript 請求或 HTML。
Java 語(yǔ)言對 REST 也有非常好的支持。畢竟,servlet 實(shí)際上是服務(wù)器端基于 REST 的 Web 服務(wù)??梢栽?Java 端使用 servlet,在 Ruby 端使用 Rails 控制器,把利用兩個(gè)平臺優(yōu)勢的應用程序組合在一起。這就是 Web 服務(wù)的漂亮之處。您真正需要的所有東西就是超群出眾的勇氣。
聯(lián)系客服