過去的 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 應用程序
在我介紹 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
如果跟著做過這個系列以前的 Ruby on Rails 項目,就會知道典型的控制器方法的一般流程是:
用戶通過跟隨鏈接或指定 URL,通過 HTTP 發送請求。
Web 服務器根據域的配置把請求轉給 Ruby on Rails。
Rails 路由器根據 URL 模式把請求路由給控制器。默認模式是 http://主機名/控制器/動作/參數。
路由器用與動作相同的參數調用控制器上的方法。
動作參數為視圖設置實例變量,並呈現視圖。
動作方法把實例變量拷貝到視圖。
例如,請看 清單 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 語句。它的工作方式是這樣的:
respond_to 方法接受單個代碼塊,並傳遞一個實例變量(標為 wants)到代碼塊。
wants 對每個可能的類型都有一個方法。控制器可以為控制器期望的每個類型指定一個代碼塊。
如果方法名稱與 HTTP Accept 頭中的類型匹配,wants 方法執行對應的代碼塊。
如果沒有指定代碼塊(例如 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>[email protected]</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 服務的漂亮之處。您真正需要的所有東西就是超群出眾的勇氣。