Ruby on Rails 是一個突然流行起來的架構,充當著 Ruby 程式設計語言的催化劑。隨著 Ruby 的經驗不斷成功,開發人員開始尋求把他們的 Ruby 應用程式與用其他語言編寫的應用程式整合。Rails 對 Web 服務提供了優秀的支援。本文介紹 Rails 中的 Web 服務,重點放在一個名為 Representational State Transfer (REST) 的策略上。
過去的 20 年間,一個趨勢主導了商業軟體工具的開發:用複雜性對抗複雜性。這一趨勢在任何地方都沒有比在分散式運算領域更明顯。C 和 Java 社區已經看到一些驚人複雜的架構被構建出來支援分布式通訊。分散式運算環境(DCE)支援用 C 語言編寫的應用程式之間的遠端程序呼叫。公用對象請求代理架構(CORBA)標準支援物件導向應用程式之間的通訊。企業 JavaBean(EJB)規範提供安全性、持久性、事務、訊息和遠端服務。對各個架構的宣傳甚囂塵上,但是這些架構都沒有滿足預期,有些甚至因為它們的複雜性而成為災難。在這些架構中,只有 EJB 3.0 屬於大力簡化的結果,有潛力在分布式應用程式上成功。市場可能給、也可能不給這個面臨強敵的架構另一個空間,但 EJB 仍然需要交付使用。
最新的大型分布式架構是 Web 服務。Web 服務技術讓應用程式可以用平台獨立或程式設計語言獨立的方式相互連信。Web 服務標準也受到複雜性惡魔的威脅,但是稱作 REST 的替代策略承諾了更簡單的方式。本文介紹了如何在 Ruby on Rails 中添加 REST 風格的 Web 服務,並從 Ruby 和 Java 代碼調用服務。
Web 服務領域
就像 EJB、CORBA 和 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 標準,包括 HTTP、XML 和 TCP/IP。
- REST 沒有覆蓋每個可能情境,而是覆蓋了最常見的問題。
請把 REST 想像成瀏覽。REST 客戶使用與瀏覽器相同的 HTTP 命令訪問資源。當 REST 客戶訪問到資源的表示時,客戶轉換到一個狀態。使用不同的 HTTP 命令,REST 客戶可以建立、讀取、更新或刪除資源的記錄。
例如,以典型的部落格為例。通過輸入 URL,例如 blog.rapidred.com,得到貼子的列表。然後,如果想編輯部落格條目,可以在 URL 中輸入 HTTP 參數(例如 blog.rapidred.com/edit?article=12345),然後顯示編輯表單。由於每個部落格條目都有自己的 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/001_create_people.rb 的遷移。請把這個遷移編輯成像清單 1 一樣:
清單 1. people 表的遷移
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 控制器產生工作台。現在可以用 script/server 啟動伺服器了。請把瀏覽器指向 localhost:3000/people,以看到針對 Person 的經典的 Rails 腳手架。圖 1 顯示了帶有標準 Rails 腳手架的應用程式:
圖 1. 簡單的 Rails 應用程式
如果跟著做過這個系列以前的 Ruby on Rails 項目,就會知道典型的控制器方法的一般流程是:
- 1. 使用者通過跟隨連結或指定 URL,通過 HTTP 發送請求。
- 2. Web 服務器根據域的配置把請求轉給 Ruby on Rails。
- 3. Rails 路由器根據 URL 模式把請求路由給控制器。預設模式是 http://主機名稱/控制器/動作/參數。
- 4. 路由器用與動作相同的參數調用控制器上的方法。
- 5. 動作參數為視圖設定執行個體變數,並呈現視圖。
- 6. 動作方法把執行個體變數拷貝到視圖。
例如,請看 清單 2 中的 show 方法。控制器設定視圖使用的 @person 執行個體變數。因為方法沒有指定視圖的名稱,所以 Rails 用與控制器動作相同的名稱調用視圖 —— 在這個樣本中,視圖位於 app/views/people/show.rhtml。
再來看 list 方法。如果想讓這個方法呈現 XML,需要:
- 刪除分頁
- 把 people 執行個體變數轉換成 XML
- 呈現 XML 而不是 HTML
Rails 使得處理 Web 服務和呈現來自同一 Web 服務的視圖成為可能。實際上也不需要分頁。為了把 Web 服務的 list 方法簡化一些,可以把控制器中的 list 方法變成像清單 3 一樣,清除分頁。還需要刪除靠近 app/views/people/list.rhtml 代碼底部的 “Next Page” 和 “Previous Page” 連結。
清單 3. 簡化 list
def list @people = Person.find_all end |
由於刪除了分頁,也就刪除了讓使用者介面更健壯的一個特性,但是又得到了一些回報。可以用相同的代碼來驅動 Web 服務和視圖。如果日後發現需要分頁,可以編寫一些定製的助手。
現在基本應用程式出來了,可以添加一些 Web 服務了。
向 Rails 控制器添加 Web 服務
如果我想說大話,我可以說 “現在已經有了一個 Web 服務”。記得我對 REST 說過什麼?這種風格的 Web 服務使用指定的資源。我的 Rails 應用程式也具有指定的資源:host_name/people/list 調用我的 list 服務。REST 風格的 Web 服務也使用 TCP/IP 和 HTTP。我的 Rails 應用程式就是這麼做的。而且格式良好的 HTML 就是 XML 的子集,也滿足最後一條 REST 要求。只需在 localhost:3000/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 工具。通過輸入 curl http://some-url,可以將要求節流成只輸出預設的響應體(瀏覽器呈現的 HTML)。輸入 curl -i http://some-url 可以得到更多資訊。這個命令返回 HTTP 頭,如清單 4 所示。可以看到頭配置由表示每個請求的配置的鍵-值對組成。
清單 4. 用 curl 調用 HTTP 要求
> 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 利用達些命令執行經典的 CRUD(CRUD 是create, read、update 和 delete 的共同縮寫)。HTTP 命令到 CRUD 的映射是這樣的:
- Create(建立):HTTP put
- Read(讀取):HTTP get
- Update(更新):HTTP post
- Delete(刪除):HTTP delete
瀏覽器利用 HTTP 頭,通過相同的伺服器端代碼來滿足不同類型的請求。行為良好的應用程式提供正確處理文檔的充足資訊。其中一條資訊叫做 HTTP Accept 頭。只要多花一點力氣,控制器就能利用一些助手,用 Accept 頭決定如何響應進入的請求。然後,控制器可以呈現適當的響應。請把 PeopleController 中的 list 方法改成像清單 5 一樣:
清單 5. 擴充 list方法以呈現 XML
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 服務。產生的程式碼是 Rails 中小型的特定於域的語句的優美樣本,它擴充 Ruby 以構造一種 switch 語句。它的工作方式是這樣的:
- 1. respond_to 方法接受單個代碼塊,並傳遞一個執行個體變數(標為 wants)到代碼塊。
- 2. wants 對每個可能的類型都有一個方法。控制器可以為控制器期望的每個類型指定一個代碼塊。
- 3. 如果方法名稱與 HTTP Accept 頭中的類型匹配,wants 方法執行對應的代碼塊。
- 4. 如果沒有指定代碼塊(例如 wants.html),Rails 就執行預設動作(在這個樣本中,呈現 app/views/people/list.rhtml)。
這個策略允許在所有預期的客戶之間共用相同的設定代碼。如果需要添加期望 HTML 的 JavaScript 客戶,以便讓應用程式支援 Ajax,只需要添加 wants.js,如清單 6 所示:
清單 6. 為 JavaScript 客戶呈現 HTML
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 |
現在已經看到了如何向唯讀方法中添加 REST Web 服務。show 方法也類似,如清單 7 所示:
清單 7. 實現 show
def show @person = Person.find(params[:id]) respond_to do |wants| wants.html wants.xml { render :xml => @person.to_xml } end end |
您可能已經注意到,通過 REST 看到的只有唯讀服務。原因是:讓應用程式處理提交和刪除所需要的工作比較少。刪除不需要額外的支援,因為當前的代碼已經用 URL 指定了要刪除的人的 ID。Rails 自動轉換 post 請求中進入的 XML,所以不需要構建任何伺服器端支援。實際上,應用程式不用變就能刪除、更新和建立。可以修補每個方法呈現的 HTTP 響應,但是客戶代碼實際就在 HTTP 返回碼之後。
現在是調用 Web 服務的時候了。
調用 Web 服務
使用現有 HTTP 協議這一策略使得調用變得簡單。清單 8 顯示了 Ruby 版本。請注意 HTTP Accept 頭。記住,控制器根據這個頭決定內容的類型。
清單 8. 從 Ruby 調用服務
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 服務調用,在 http://localhost:3000/people/list 上調用 HTTP get 方法,並輸出響應。Ruby 有很好的庫可以處理產生的 XML,但是它們超出了本文的範圍。不需要用 Ruby 調用這個服務。只需要 HTTP 的庫。清單 9 顯示這個服務的 Java 調用:
清單 9. 用 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 等價物一樣,這個代碼開啟一個 URL 串連,把 Accept 頭設定成 text/xml,發出 get,並輸出結果。Java 代碼有許多 XML 架構,但是我在這個樣本中寫入程式碼了 XML,以保持樣本簡單。
post 的調用也相似。清單 10 顯示了簡單的 post:
清單 10. 用 Java 代碼調用 HTTP 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); } } |
這個 HTTP post 通過在 http://localhost:3000/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 服務的漂亮之處。您真正需要的所有東西就是超群出眾的勇氣。
查看原文
在我介紹 Rails 的 Web 服務之前,請查看控制器代碼。編輯 app/controllers/people_controller.rb,使之與清單 2 的代碼匹配:
清單 2. PeopleController 的控制器代碼
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 |