跨越邊界 系列中以前的文章說 Ruby on Rails 是一個突然流行起來的框架,充當(dāng)著 Ruby 編程語言的催化劑,。隨著 Ruby 的經(jīng)驗不斷成功,,開發(fā)人員開始尋求把他們的 Ruby 應(yīng)用程序與用其他語言編寫的應(yīng)用程序集成,。Rails 對 Web 服務(wù)提供了優(yōu)秀的支持,。本文介紹 Rails 中的 Web 服務(wù),重點放在一個名為 Representational State Transfer (REST) 的策略上,。
過去的 20 年間,,一個趨勢主導(dǎo)了商業(yè)軟件工具的開發(fā):用復(fù)雜性對抗復(fù)雜性。這一趨勢在任何地方都沒有比在分布式計算領(lǐng)域更明顯,。C 和 Java™ 社區(qū)已經(jīng)看到一些驚人復(fù)雜的框架被構(gòu)建出來支持分布式通信,。分布式計算環(huán)境(DCE)支持用 C 語言編寫的應(yīng)用程序之間的遠程過程調(diào)用。公共對象請求代理架構(gòu)(CORBA)標(biāo)準(zhǔn)支持面向?qū)ο髴?yīng)用程序之間的通信,。企業(yè) JavaBean(EJB)規(guī)范提供安全性,、持久性、事務(wù),、消息和遠程的服務(wù),。對各個框架的宣傳甚囂塵上,但是這些框架都沒有滿足預(yù)期,有些甚至因為它們的復(fù)雜性而成為災(zāi)難,。在這些框架中,,只有 EJB 3.0 屬于大力簡化的結(jié)果,有潛力在分布式應(yīng)用程序上成功,。市場可能給,、也可能不給這個面臨強敵的框架另一個空間,但 EJB 仍然需要交付使用,。
最新的大型分布式框架是 Web 服務(wù),。Web 服務(wù)技術(shù)讓應(yīng)用程序可以用平臺獨立或編程語言獨立的方式相互通信(請參閱 參考資料)。Web 服務(wù)標(biāo)準(zhǔn)也受到復(fù)雜性惡魔的威脅,,但是稱作 REST 的替代策略承諾了更簡單的方式,。本文介紹了如何在 Ruby on Rails 中添加 REST 風(fēng)格的 Web 服務(wù),并從 Ruby 和 Java 代碼調(diào)用服務(wù),。
|
關(guān)于本系列
在 跨越邊界 系列中,,作者 Bruce Tate 推動了這樣一個概念:今天的 Java 程序員通過學(xué)習(xí)其他方法和語言,可以受益,。編程陣營中 Java 技術(shù)是所有開發(fā)項目最佳選擇的情況已經(jīng)變了,。其他框架正在改變 Java 框架構(gòu)建的方式,從其他語言學(xué)到的概念有助于 Java 編程,。編寫的 Python(或 Ruby,、Smalltalk 或 ……)代碼可以改變 Java 編碼的方式。
本系列介紹了與 Java 開發(fā)有根本不同,,但是卻直接適用的編程概念和技術(shù),。在某些情況下,需要集成這些技術(shù)以利用它,。在其他情況下,,將可以直接應(yīng)用這些概念。單獨的工具不如其他語言和框架中影響 Java 社區(qū)的開發(fā)人員,、框架甚至基本方法的思想重要,。
|
|
Web 服務(wù)領(lǐng)域
就像 EJB、CORBA 和 DCE 一樣,,Web 服務(wù)的核心抽象也是遠程過程調(diào)用,。Web 服務(wù)利用叫做 SOAP(最初,SOAP 代表簡單對象存取協(xié)議,,但是這個術(shù)語現(xiàn)在降級了)的協(xié)議,,用 XML 表示消息的結(jié)構(gòu)。這里有一個技巧:如果協(xié)議用代表簡單的 S 開始,,那它就不簡單,。Web 服務(wù)定義語言(WSDL)提供了服務(wù)的標(biāo)準(zhǔn)規(guī)范,。像 SOAP 一樣,WSDL 也是一個棘手而復(fù)雜的 API,,而 SOAP 和 WSDL 僅僅涉及到了構(gòu)成 Web 服務(wù)這個大怪物的眾多 API 的表面(請參閱 參考資料),。Web 服務(wù)需要一次大修,感謝 Roy Fielding 的一份有影響的博士論文,,Web 服務(wù)得到了大修(請參閱 參考資料),。
Fielding 的論文描述了 REST 應(yīng)用程序聯(lián)網(wǎng)策略。REST 與全堆棧 Web 服務(wù)根本不同,,主要原因有三個:
- REST 的核心抽象是遠程資源而不是遠程過程調(diào)用,。
- REST 沒有發(fā)明一個詳盡的標(biāo)準(zhǔn)列表,而是采用現(xiàn)有的 Internet 標(biāo)準(zhǔn),,包括 HTTP,、XML 和 TCP/IP。
- REST 沒有覆蓋每個可能場景,,而是覆蓋了最常見的問題,。
請把 REST 想像成瀏覽。REST 客戶使用與瀏覽器相同的 HTTP 命令訪問資源,。當(dāng) REST 客戶訪問到資源的表示時,,客戶轉(zhuǎn)換到一個狀態(tài)。使用不同的 HTTP 命令,,REST 客戶可以創(chuàng)建,、讀取、更新或刪除資源的記錄,。
例如,,以典型的博客為例。通過輸入 URL,,例如 blog.rapidred.com,,得到貼子的列表。然后,,如果想編輯博客條目,,可以在 URL 中輸入 HTTP 參數(shù)(例如 blog.rapidred.com/edit?article=12345),然后顯示編輯表單,。由于每個博客條目都有自己的 URL,所以點擊鏈接或直接輸入 URL,,就可以用 HTTP 命令讀取,、修改或刪除內(nèi)容。
簡而言之,,REST 可以:
- 用 TCP/IP 命名標(biāo)準(zhǔn)命名 Web 上的資源
- 用 HTTP 查詢和操縱這些資源
- 使用基于文本的標(biāo)準(zhǔn)消息格式(例如 XML 或 HTML)來構(gòu)造數(shù)據(jù)
Ruby on Rails 用 REST 對 Web 服務(wù)提供了優(yōu)秀的支持,。
Action Web Services 概述
Rails 用叫做 Action Web Services 的模塊實現(xiàn) Web 服務(wù)。許多開發(fā)框架鼓勵視圖和 Web 服務(wù)使用獨立的控制器。這個策略可以維護控制器之間的風(fēng)格一致,。問題是針對所服務(wù)的每種內(nèi)容,,都需要一個新控制器。例如,,Ajax 用戶界面要求從控制器取得到 JavaScript 的遠程 XML 調(diào)用,。
不必為 Web 服務(wù)專門分配一個控制器,使用 Rails,,可以通用地用同一個控制器向基于 HTML 的視圖,、基于 XML 的 Web 服務(wù)和基于 XML 的 JavaScript 組件提供內(nèi)容。理解 Action Web Services 的最好方式就是在工作應(yīng)用程序的環(huán)境下查看它的實際作用,。
請用自己選擇的數(shù)據(jù)庫管理器創(chuàng)建一個叫做 service_development
的數(shù)據(jù)庫,。接下來,用以下命令創(chuàng)建 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 中的數(shù)據(jù)庫配置修改成與自己的數(shù)據(jù)庫配置匹配,并輸入 rake migrate
,。最后,,輸入 script/generate scaffold Person People
,為 Person
模型和 People
控制器生成工作臺?,F(xiàn)在可以用 script/server
啟動服務(wù)器了,。請把瀏覽器指向 localhost:3000/people,以看到針對 Person
的經(jīng)典的 Rails 腳手架,。圖 1 顯示了帶有標(biāo)準(zhǔn) Rails 腳手架的應(yīng)用程序:
圖 1. 簡單的 Rails 應(yīng)用程序
在我介紹 Rails 的 Web 服務(wù)之前,,請查看控制器代碼。編輯 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./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 發(fā)送請求,。
- Web 服務(wù)器根據(jù)域的配置把請求轉(zhuǎn)給 Ruby on Rails。
- Rails 路由器根據(jù) URL 模式把請求路由給控制器,。默認模式是 http://主機名/控制器/動作/參數(shù),。
- 路由器用與動作相同的參數(shù)調(diào)用控制器上的方法。
- 動作參數(shù)為視圖設(shè)置實例變量,,并呈現(xiàn)視圖,。
- 動作方法把實例變量拷貝到視圖。
例如,,請看 清單 2 中的 show
方法,??刂破髟O(shè)置視圖使用的 @person
實例變量。因為方法沒有指定視圖的名稱,,所以 Rails 用與控制器動作相同的名稱調(diào)用視圖 —— 在這個示例中,,視圖位于 app/views/people/show.rhtml。
再來看 list
方法,。如果想讓這個方法呈現(xiàn) XML,,需要:
- 刪除分頁
- 把
people
實例變量轉(zhuǎn)換成 XML
- 呈現(xiàn) XML 而不是 HTML
Rails 使得處理 Web 服務(wù)和呈現(xiàn)來自同一 Web 服務(wù)的視圖成為可能。實際上也不需要分頁,。為了把 Web 服務(wù)的 list
方法簡化一些,,可以把控制器中的 list
方法變成像清單 3 一樣,清除分頁,。還需要刪除靠近 app/views/people/list.rhtml 代碼底部的 “Next Page” 和 “Previous Page” 鏈接,。
清單 3. 簡化 list
def list
@people = Person.find_all
end
|
由于刪除了分頁,也就刪除了讓用戶界面更健壯的一個特性,,但是又得到了一些回報,。可以用相同的代碼來驅(qū)動 Web 服務(wù)和視圖,。如果日后發(fā)現(xiàn)需要分頁,,可以編寫一些定制的助手。
現(xiàn)在基本應(yīng)用程序出來了,,可以添加一些 Web 服務(wù)了,。
向 Rails 控制器添加 Web 服務(wù)
如果我想說大話,我可以說 “現(xiàn)在已經(jīng)有了一個 Web 服務(wù)”,。記得我對 REST 說過什么,?這種風(fēng)格的 Web 服務(wù)使用指定的資源。我的 Rails 應(yīng)用程序也具有指定的資源:host_name/people/list 調(diào)用我的 list
服務(wù),。REST 風(fēng)格的 Web 服務(wù)也使用 TCP/IP 和 HTTP,。我的 Rails 應(yīng)用程序就是這么做的。而且格式良好的 HTML 就是 XML 的子集,,也滿足最后一條 REST 要求,。只需在 localhost:3000/people/list 上調(diào)用 HTTP get
,并解析結(jié)果,,就可以得到人員列表,。這就是關(guān)鍵。REST 的工作方式與 Internet 的工作方式一樣,。但這并不是真正基于 REST 的 Web 服務(wù),。理想情況下,應(yīng)當(dāng)提供反映 Person
含義的 XML 文檔而不是用戶界面的結(jié)構(gòu),。
真正的服務(wù)應(yīng)當(dāng)產(chǎn)生純數(shù)據(jù)的表示,,一個專門針對服務(wù)的預(yù)期客戶而構(gòu)建的表示。但是示例應(yīng)用程序有兩個客戶:終端用戶和 REST 客戶,。要為兩個目的重用相同的代碼,,需要給 Rails 提供更多信息。Rails 的設(shè)計者可能決定使用額外的 URL 參數(shù),,但是處理 URL 可是一項費勁的工作,。Rails 不應(yīng)當(dāng)用這些細節(jié)增加用戶負擔(dān)。相反,,HTTP 提供了指定更多信息的工具:HTTP 頭,。
要理解 Web 服務(wù)的 REST 模型,了解一點 HTTP 是有幫助的,。curl
(請把它想像成 查看 URL)命令允許用一個命令查詢 URL,,并查看響應(yīng)?;?Unix 的操作系統(tǒng)默認包含 curl
,,可以為其他操作系統(tǒng)下載免費的 curl
工具。通過輸入 curl http://some-url
,,可以將請求限制成只輸出默認的響應(yīng)體(瀏覽器呈現(xiàn)的 HTML),。輸入 curl -i http://some-url
可以得到更多信息。這個命令返回 HTTP 頭,,如清單 4 所示,。可以看到頭配置由表示每個請求的配置的鍵-值對組成,。
清單 4. 用 curl 調(diào)用 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 利用達些命令執(zhí)行經(jīng)典的 CRUD(CRUD 是create, read,、update 和 delete 的共同縮寫)。HTTP 命令到 CRUD 的映射是這樣的:
- Create(創(chuàng)建):HTTP
put
- Read(讀?。篐TTP
get
- Update(更新):HTTP
post
- Delete(刪除):HTTP
delete
瀏覽器利用 HTTP 頭,,通過相同的服務(wù)器端代碼來滿足不同類型的請求。行為良好的應(yīng)用程序提供正確處理文檔的充足信息,。其中一條信息叫做 HTTP Accept
頭,。只要多花一點力氣,控制器就能利用一些助手,,用 Accept
頭決定如何響應(yīng)進入的請求,。然后,控制器可以呈現(xiàn)適當(dāng)?shù)捻憫?yīng),。請把 PeopleController
中的 list
方法改成像清單 5 一樣:
清單 5. 擴展 list方法以呈現(xiàn) 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 服務(wù),。生成的代碼是 Rails 中小型的特定于域的語句的優(yōu)美示例,它擴展 Ruby 以構(gòu)造一種 switch 語句,。它的工作方式是這樣的:
respond_to
方法接受單個代碼塊,,并傳遞一個實例變量(標(biāo)為 wants
)到代碼塊。
wants
對每個可能的類型都有一個方法,??刂破骺梢詾榭刂破髌谕拿總€類型指定一個代碼塊。
- 如果方法名稱與 HTTP
Accept
頭中的類型匹配,,wants
方法執(zhí)行對應(yīng)的代碼塊,。
- 如果沒有指定代碼塊(例如
wants.html
),Rails 就執(zhí)行默認動作(在這個示例中,,呈現(xiàn) app/views/people/list.rhtml),。
這個策略允許在所有預(yù)期的客戶之間共享相同的設(shè)置代碼。如果需要添加期望 HTML 的 JavaScript 客戶,,以便讓應(yīng)用程序支持 Ajax,,只需要添加 wants.js,如清單 6 所示:
清單 6. 為 JavaScript 客戶呈現(xiàn) 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
|
現(xiàn)在已經(jīng)看到了如何向只讀的方法中添加 REST Web 服務(wù),。show
方法也類似,,如清單 7 所示:
清單 7. 實現(xiàn) show
def show
@person = Person.find(params[:id])
respond_to do |wants|
wants.html
wants.xml { render :xml => @person.to_xml }
end
end
|
您可能已經(jīng)注意到,通過 REST 看到的只有只讀服務(wù),。原因是:讓應(yīng)用程序處理提交和刪除所需要的工作比較少,。刪除不需要額外的支持,因為當(dāng)前的代碼已經(jīng)用 URL 指定了要刪除的人的 ID,。Rails 自動轉(zhuǎn)換 post
請求中進入的 XML,,所以不需要構(gòu)建任何服務(wù)器端支持。實際上,,應(yīng)用程序不用變就能刪除,、更新和創(chuàng)建??梢孕扪a每個方法呈現(xiàn)的 HTTP 響應(yīng),,但是客戶代碼實際就在 HTTP 返回碼之后。
現(xiàn)在是調(diào)用 Web 服務(wù)的時候了,。
調(diào)用 Web 服務(wù)
使用現(xiàn)有 HTTP 協(xié)議這一策略使得調(diào)用變得簡單,。清單 8 顯示了 Ruby 版本。請注意 HTTP Accept
頭,。記住,,控制器根據(jù)這個頭決定內(nèi)容的類型。
清單 8. 從 Ruby 調(diào)用服務(wù)
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ù)調(diào)用,在 http://localhost:3000/people/list 上調(diào)用 HTTP get
方法,,并輸出響應(yīng),。Ruby 有很好的庫可以處理生成的 XML,但是它們超出了本文的范圍,。不需要用 Ruby 調(diào)用這個服務(wù),。只需要 HTTP 的庫,。清單 9 顯示這個服務(wù)的 Java 調(diào)用:
清單 9. 用 Java 代碼調(diào)用服務(wù)
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
頭設(shè)置成 text/xml
,,發(fā)出 get
,,并輸出結(jié)果。Java 代碼有許多 XML 框架(請參閱 參考資料),,但是我在這個示例中硬編碼了 XML,,以保持示例簡單。
post
的調(diào)用也相似,。清單 10 顯示了簡單的 post
:
清單 10. 用 Java 代碼調(diào)用 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 上調(diào)用 post
,,并在 HTTP 文檔體中傳遞一個 XML 文檔,創(chuàng)建了一個新 Person
,。(通常應(yīng)當(dāng)用 Java XML 庫構(gòu)建 XML 文檔,。這次我還是硬編碼了 XML 文檔,以保持示例簡單,。)Rails 支持自動把進入的 XML 轉(zhuǎn)換成 Person
屬性的 Ruby 散列表,。
結(jié)束語
在本文中,已經(jīng)看到只用少量代碼,,就使控制器支持基于 REST 的 Web 服務(wù),。動態(tài)類型化的 Internet 語句,例如 Ruby,,大量地利用 REST 代替基于 SOAP 的 Web 服務(wù),。一些簡單的調(diào)用,包括漂亮的 responds_to
語法和對進入提交的自動 XML 轉(zhuǎn)換,,使得可以容易地利用同一控制器處理 Web 服務(wù),、遠程 JavaScript 請求或 HTML。
Java 語言對 REST 也有非常好的支持,。畢竟,,servlet 實際上是服務(wù)器端基于 REST 的 Web 服務(wù)??梢栽?Java 端使用 servlet,,在 Ruby 端使用 Rails 控制器,把利用兩個平臺優(yōu)勢的應(yīng)用程序組合在一起。這就是 Web 服務(wù)的漂亮之處,。您真正需要的所有東西就是超群出眾的勇氣,。