REST 簡介
REST是英文Representational State Transfer的縮寫,這個術語由Roy Thomas Fielding博士在他的論文《Architectural Styles and the Design of Network-based Software Architectures》中提出。從這篇論文的標題可以看出:REST是一種基於網絡的軟件架構風格。
提示:國內很多網絡資料將REST翻譯為“表述性狀態轉移”,不過筆者對這個翻譯不太認同。因為這個專業術語無法傳達REST的含義,讀者可以先不要理會REST到底該如何翻譯,盡量先去理解REST是什麼?有什麼用?然後再來看這個術語的翻譯。關於Roy Thomas Fielding博士的原文參見如下地址:http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm。
REST 架構是針對傳統Web應用提出的一種改進,是一種新型的分布式軟件設計架構。對於異構系統如何進行整合的問題,目前主流做法都集中在使用 SOAP、WSDL 和 WS-* 規范的Web Services。而REST架構實際上也是解決異構系統整合問題的一種新思路。
如果開發者在開發過程中能堅持REST原則,將可以得到一個使用了優質Web架構的系統,從而為系統提供更好的可伸縮性,並降低開發難度。關於REST架構的主要原則如下:
網絡上的所有事物都可被抽象為資源(Resource)。
每個資源都有一個唯一的資源標識符(Resource Identifier)。
同一資源具有多種表現形式。
使用標准方法操作資源。
通過緩存來提高性能。
對資源的各種操作不會改變資源標識符。
所有的操作都是無狀態的(Stateless)。
僅從上面幾條原則來看REST架構,其實依然比較難以理解,下面筆者將從如下二個方面來介紹REST。
資源和標識符
現在的Web應用上包含了大量信息,但這些信息都被隱藏在 HTML、CSS 和 JavaScript 代碼中,對於普通浏覽者而言,他們進入這個系統時無法知道該系統包含哪些頁面;對於一個需要訪問該系統資源的第三方系統而言,同樣無法明白這個系統包含多少功能和信息。
URI 和 URL
與 URI 相關的概念還有 URL,URL 是 Uniform Resource Locator,也就是統一資源定位符的意思。其中 http://www.crazyit.org 就是一個統一資源定位符,URL 是 URI 的子集。簡而言之:每個 URL 都是 URI,但不是每個 URI 都是 URL。
從REST架構的角度來看,該系統裡包含的所有功能和信息,都可被稱為資源(Resource),REST 架構中的資源包含靜態頁面、JSP 和 Servlet 等,該應用暴露在網絡上的所有功能和信息都可被稱為資源。
除此之外,REST 架構規范了應用資源的命名方式,REST 規定對應用資源使用統一的命名方式:REST 系統中的資源必須統一命名和規劃,REST 系統由使用 URI(Uniform Resource Identifier,即統一資源標識符)命名的資源組成。由於REST對資源使用了基於 URI 的統一命名,因此這些信息就自然地暴露出來了,從而避免 “信息地窖”的不良後果。
對於當今最常見的網絡應用來說,資源標識符就是 URI,資源的使用者則根據 URI 來操作應用資源。當 URI 發生改變時,表明客戶機所使用的資源發生了改變。
從資源的角度來看,當客戶機操作不同的資源時,資源所在的Web頁(將Web頁當成虛擬的狀態機來看)的狀態就會發生改變、遷移(Transfer),這就是REST術語中 ST(State Tranfer)的由來了。
客戶機為了操作不同狀態的資源,則需要發送一些 Representational 的數據,這些數據包含必要的交互數據,以及描述這些數據的元數據。這就是REST術語中 RE(Representational)的由來了。理解了這個層次之後,至於REST如何翻譯、或是否真正給它一個中文術語,讀者可自行決定。
操作資源的方式
對於REST架構的服務器端而言,它提供的是資源,但同一資源具有多種表現形式(可通過在 HTTP Content-type 頭中包含關於數據類型的元數據)。如果客戶程序完全支持 HTTP 應用協議,並能正確處理REST架構的標准數據格式,那麼它就可以與世界上任意一個REST風格的用戶交互。
上面的情況不僅適用於從服務器端到客戶端的數據,反之亦然——倘若從客戶端傳來的數據符合REST架構的標准數據格式,那麼服務器端就可以正確處理數據,而不去關心客戶端的類型。
典型情況下,REST 風格的資源能以 XHTML、XML 和 JSON 三種形式存在,其中 XML 格式的數據是 WebServices 技術的數據交換格式,而 JSON 則是另一種輕量級的數據交換格式;至於 XHTML 格式則主要由浏覽器負責呈現。
當服務器為所有資源提供多種表現形式之後,這些資源不僅可以被標准Web浏覽器所使用,還可以由 JavaScript 通過 Ajax 技術調用,或者以 RPC(Remote Procedure Call)風格調用,從而變成REST風格的 WebServices。
REST 架構除了規定服務器提供資源的方式之外,還推薦客戶端使用 HTTP 作為 Generic Connector Interface(也就是通用連接器接口),而 HTTP 則把對一個 URI 的操作限制在了 4 個之內:GET、POST、PUT 和 DELETE。通過使用通用連接器接口對資源進行操作的好處是保證系統提供的服務都是高度解耦的,從而簡化了系統開發,改善了系統的交互性和可重用性。
REST 架構要求客戶端的所有的操作在本質上是無狀態的,即從客戶到服務器的每個 Request 都必須包含理解該 Request 的所有必需信息。這種無狀態性的規范提供了如下幾點好處:
無狀態性使得客戶端和服務器端不必保存對方的詳細信息,服務器只需要處理當前 Request,而不必了解前面 Request 的歷史。
無狀態性減少了服務器從局部錯誤中恢復的任務量,可以非常方便地實現 Fail Over 技術,從而很容易地將服務器組件部署在集群內。
無狀態性使得服務器端不必在多個 Request 中保存狀態,從而可以更容易地釋放資源。
無狀態性無需服務組件保存 Request 狀態,因此可讓服務器充分利用 Pool 技術來提高穩定性和性能。
當然,無狀態性會使得服務器不再保存 Request 的狀態數據,因此需要在一系列 Request 中發送重復數據的,從而提高了系統的通信成本。為了改善無狀態性帶來的性能下降,REST 架構填加了緩存約束。緩存約束允許隱式或顯式地標記一個 Response 中的數據,這樣就賦予了客戶端緩存 Response 數據的功能,這樣就可以為以後的 Request 共用緩存的數據,部分或全部的消除一些交互,增加了網絡的效率。但是用於客戶端緩存了信息,也就同時增加了客戶端與服務器數據不一致的可能,從而降低了可靠性。
Struts 2 的REST支持
約定優於配置
Convention 這個單詞的翻譯過來就是“約定”的意思。有 Ruby On Rails 開發經驗的讀者知道 Rails 有一條重要原則:約定優於配置。Rails 開發者只需要按約定開發 ActiveRecord、ActiveController 即可,無需進行配置。很明顯,Struts 2 的 Convention 插件借鑒了 Rails 的創意,甚至連插件的名稱都借鑒了“約定優於配置”原則。
從Struts 2.1 開始,Struts 2 改為使用 Convention
RestActionMapper 簡介
從本質上來看,Struts 2 依然是一個 MVC 框架,最初設計Struts 2時並沒有按REST架構進行設計,因此Struts 2本質上並不是一個REST框架。
由於Struts 2提供了良好的可擴展性,因此允許通過REST插件將其擴展成支持REST的框架。REST 插件的核心是 RestActionMapper,它負責將 Rails 風格的 URL 轉換為傳統請求的 URL。
用 WinRAR 打開 struts2-rest-plugin-2.1.6 文件,看到該文件裡包含一個 struts-plugin.xml 文件,該文件中包含如下一行:
<!-- 定義支持REST的 ActionMapper -->
<bean type="org.apache.struts2.dispatcher.mapper.ActionMapper"
name="rest" class="org.apache.struts2.rest.RestActionMapper" />
通過查看 RestActionMapper 的 API 說明,我們發現它可接受如下幾個參數:
struts.mapper.idParameterName:用於設置 ID 請求參數的參數名,該屬性值默認是 id。
struts.mapper.indexMethodName:設置不帶 id 請求參數的 GET 請求調用 Action 的哪個方法。該屬性值默認是 index。
struts.mapper.getMethodName:設置帶 id 請求參數的 GET 請求調用 Action 的哪個方法。該屬性值默認是 show。
struts.mapper.postMethodName:設置不帶 id 請求參數的 POST 請求調用 Action 的哪個方法。該屬性值默認是 create。
struts.mapper.putMethodName:設置帶 id 請求參數的 PUT 請求調用 Action 的哪個方法。該屬性值默認是 update。
struts.mapper.deleteMethodName:設置帶 id 請求參數的 DELETE 請求調用 Action 的哪個方法。該屬性值默認是 destroy。
struts.mapper.editMethodName:設置帶 id 請求參數、且指定操作 edit 資源的 GET 請求調用 Action 的哪個方法。該屬性值默認是 edit。
struts.mapper.newMethodName:設置不帶 id 請求參數、且指定操作 edit 資源的 GET 請求調用 Action 的哪個方法。該屬性值默認是 editNew。
在RestActionMapper的方法列表中,我們看到 setIdParameterName、setIndexMethodName、setGetMethodName、setPostMethodName、setPutMethodName、setDeleteMethodName、setEditMethodName、setNewMethodName 等方法,這些方法對應為上面列出的方法提供 setter 支持。
通常情況下,我們沒有必要改變 RestActionMapper 的參數,直接使用這些參數的默認值就可支持 Rails 風格的REST。根據前面介紹可以看出:支持REST風格的 Action 至少包含如下 7 個方法:
index:處理不帶 id 請求參數的 GET 請求。
show:處理帶 id 請求參數的 GET 請求。
create:處理不帶 id 請求參數的 POST 請求。
update:處理帶 id 請求參數的 PUT 請求。
destroy:處理帶 id 請求參數的 DELETE 請求。
edit:處理帶 id 請求參數,且指定操作 edit 資源的 GET 請求。
editNew:處理不帶 id 請求參數,且指定操作 edit 資源的 GET 請求。
如果請求需要向服務器發送 id 請求參數,直接將請求參數的值附加在 URL 中即可。表 12.3 顯示了 RestActionMapper 對不同 HTTP 請求的處理結果。
表 12.3 RestActionMapper 對 HTTP 請求的處理
HTTP 方法 URI 調用 Action 的方法 請求參數 GET /book index POST /book create PUT /book/2 update id=2 DELETE /book/2 destroy id=2 GET /book/2 show id=2 GET /book/2/edit edit id=2 GET /book/new editNew 不幸地是,標准 HTML 語言目前根本不支持 PUT 和 DELETE 兩個操作,為了彌補這種不足,REST 插件允許開發者提交請求時額外增加一個 _method 請求參數,該參數值可以為 PUT 或 DELETE,用於模擬 HTTP 協議的 PUT 和 DELETE 操作。
為Struts 2應用安裝REST插件
安裝REST插件非常簡單,只需按如下步驟進行即可:
(1)將Struts 2項目下 struts2-convention-plugin-2.1.6.jar、struts2-rest-plugin-2.1.6.jar 兩個 JAR 包復制到Web應用的Web-INF\lib 路徑下。
(2)由於Struts 2的REST插件還需要將提供 XML、JSON 格式的數據,因此還需要將 xstream-1.2.2.jar、json-lib-2.1.jar、ezmorph-1.0.3.jar 以及 Jakarta-Common 相關 JAR 包復制到Web應用的Web-INF/lib 路徑下。
(3)通過 struts.xml、struts.properties 或Web.xml 改變 struts.convention.default.parent.package 常量的值,讓支持REST風格的 Action 所在的包默認繼承REST-default,而不是繼承默認的 convention-default 父包。
對於第三個步驟而言,開發者完全可以不設置該常量,如果開發者不設置該常量,則意味著開發者必須通過 Annotation 為每個 Action 類設置父包。
實現支持REST的 Action 類
在實現支持REST的 Action 之前,我們先為系統提供一個 Model 類:Book,該 Book 類非常簡單,代碼如下:
public class Book
{
private Integer id;
private String name;
private double price;
// 無參數的構造器
public Book(){}
//id 屬性的 setter 和 getter 方法
public void setId(Integer id)
{
this.id = id;
}
public Integer getId()
{
return this.id;
}
// 省略 name 和 price 的 setter 和 getter 方法
...
}
除了提供上面的Book類之外,我們還為該 Book 類提供一個業務邏輯組件:BookService。為了簡單起見,BookService 類不再依賴 DAO 組件訪問數據庫,而是直接操作內存中的 Book 數組——簡單地說,本系統中狀態是瞬態的,沒有持久化保存,應用運行過程中這些狀態一直存在,但一旦重啟該應用,則系統狀態丟失。下面是 BookService 類的代碼:
public class BookService
{
private static Map<Integer , Book> books
= new HashMap<Integer , Book>();
// 保留下本圖書的 ID
private static int nextId = 5;
// 以內存中的數據模擬數據庫的持久存儲
static {
books.put(1 , new Book(1
, "瘋狂 Java 講義" , 99));
books.put(2 , new Book(2
, "輕量級 Java EE 企業應用實戰" , 89));
books.put(3 , new Book(3
, "瘋狂 Ajax 講義", 78));
books.put(4 , new Book(4
, "Struts 2 權威指南" , 79));
}
// 根據 ID 獲取
public Book get(int id)
{
return books.get(id);
}
// 獲取系統中全部圖書
public List<Book> getAll()
{
return new ArrayList<Book>(books.values());
}
// 更新已有的圖書或保存新圖書
public void saveOrUpdate(Book book)
{
// 如果試圖保存的圖書的 ID 為 null,表明是保存新的圖書
if (book.getId() == null)
{
// 為新的圖書分配 ID。
book.setId(nextId++);
}
// 將保存 book
books.put(book.getId() , book);
}
// 刪除圖書
public void remove(int id)
{
books.remove(id);
}
}
從上面粗體字代碼可以看出,BookService 提供了 4 個方法,用於實現對 Book 對象的 CRUD 操作。
下面開始定義支持REST的 Action 類了,這個 Action 類與前面介紹Struts 2的普通 Action 存在一些差異——因為該 Action 不再用 execute() 方法來處理用戶請求,而是使用前面介紹的 7 個標准方法來處理用戶請求。除此之外,該 Action 總是需要處理 id 請求參數,因此必須提供 id 請求參數,並為之提供對應的 setter 和 getter 方法。
因為本系統已經提供了 Book Model 類,並且為了更好的模擬 Rails 中 ActiveController(Controller)直接訪問 ActiveRecord(Model)的方式,本系統采用了 ModelDriven 的開發方式,下面是本系統中支持REST的 Action 類的代碼。
// 定義返回 success 時重定向到 book Action
@Results(@Result(name="success"
, type="redirectAction"
, params = {"actionName" , "book"}))
public class BookController extends ActionSupport
implements ModelDriven<Object>
{
// 封裝 id 請求參數的屬性
private int id;
private Book model = new Book();
private List<Book> list;
// 定義業務邏輯組件
private BookService bookService = new BookService();
// 獲取 id 請求參數的方法
public void setId(int id)
{
this.id = id;
// 取得方法時順帶初始化 model 對象
if (id > 0)
{
this.model = bookService.get(id);
}
}
public int getId()
{
return this.id;
}
// 處理不帶 id 參數的 GET 請求
// 進入首頁
public HttpHeaders index()
{
list = bookService.getAll();
return new DefaultHttpHeaders("index")
.disableCaching();
}
// 處理不帶 id 參數的 GET 請求
// 進入添加新圖書。
public String editNew()
{
// 創建一個新圖書
model = new Book();
return "editNew";
}
// 處理不帶 id 參數的 POST 請求
// 保存新圖書
public HttpHeaders create()
{
// 保存圖書
bookService.saveOrUpdate(model);
addActionMessage("添加圖書成功");
return new DefaultHttpHeaders("success")
.setLocationId(model.getId());
}
// 處理帶 id 參數的 GET 請求
// 顯示指定圖書
public HttpHeaders show()
{
return new DefaultHttpHeaders("show");
}
// 處理帶 id 參數、且指定操作 edit 資源的 GET 請求
// 進入編輯頁面 (book-edit.jsp)
public String edit()
{
return "edit";
}
// 處理帶 id 參數的 PUT 請求
// 修改圖書
public String update()
{
bookService.saveOrUpdate(model);
addActionMessage("圖書編輯成功!");
return "success";
}
// 處理帶 id 參數,且指定操作 deleteConfirm 資源的方法
// 進入刪除頁面 (book-deleteConfirm.jsp)
public String deleteConfirm()
{
return "deleteConfirm";
}
// 處理帶 id 參數的 DELETE 請求
// 刪除圖書
public String destroy()
{
bookService.remove(id);
addActionMessage("成功刪除 ID 為" + id + "的圖書!");
return "success";
}
// 實現 ModelDriven 接口必須實現的 getModel 方法
public Object getModel()
{
return (list != null ? list : model);
}
}
上面 Action 代碼中粗體字代碼定義了 7 個方法,這 7 個方法正是前面提到的標准方法。除此之外,該 Action 裡還包含一個額外的 deleteConfirm() 方法,這個方法用於處理帶 id 參數、且指定操作 deleteConfirm 資源的 GET 請求。也就是說,當用戶請求 /book/1/deleteConfirm 時,該請求將由該方法負責處理。
實際上,RestActionMapper 不僅可以將對 /book/1/edit 的請求映射到 Book 控制器的 edit() 方法,而 1 將作為 id 請求參數。實際上,它可以將任意 /book/1/xxx 的請求映射到 Book 控制器的 xxx() 方法,而 1 是請求參數。
上面 Action 類使用了 @Results 進行修飾,這表明當 Action 的任何方法返回“success”邏輯視圖時,系統將重定向到 book.action。
可能有讀者會對 index()、create()、show() 三個方法的返回值感到疑惑:它們不再直接返回普通字符串作為邏輯視圖名字,而是返回一個以字符串為參數的 DefaultHttpHeaders 對象?其實讀者不必對 DefaultHttpHeaders 感到疑惑,其實 DefaultHttpHeaders 只是普通字符串的加強形式,用於REST對處理結果進行更多額外的控制。
當 Action 類的處理方法返回字符串作為邏輯視圖時,Struts 2 只能將其當成一個簡單的視圖名,僅能根據該視圖名映射到實際視圖資源,僅此而已。如果使用 DefaultHttpHeaders 作為邏輯視圖,DefaultHttpHeaders 除了可以包含普通字符串作為邏輯視圖名之外,還可以額外增加更多的控制數據,從而可以增強對 Response 的控制。關於 HttpHeaders 和 DefaultHttpHeaders 的介紹請參考REST插件的 API。
還有一點需要指出,上面的 BookController 控制器實現類的類名並不以 Action 結尾,而是以 Controller 結尾,因此我們可以在 struts.xml 文件中配置如下常量:
<!-- 指定控制器類的後綴為 Controller -->
<constant name="struts.convention.action.suffix"
value="Controller"/>
本應用裡的 struts.xml 文件如下:
程序清單:codes\12\12.6\BookShow\WEB-INF\src\struts.xml
<?xml version="1.0" encoding="GBK" ?>
<!-- 指定 Struts 2 配置文件的 DTD 信息 -->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.1//EN"
"http://struts.apache.org/dtds/struts-2.1.dtd">
<!-- 指定 Struts 2 配置文件的根元素 -->
<struts>
<constant name="struts.i18n.encoding" value="GBK"/>
<!-- 指定控制器類的後綴為 Controller -->
<constant name="struts.convention.action.suffix"
value="Controller"/>
<constant name="struts.convention.action.mapAllMatches"
value="true"/>
<!-- 指定 Action 所在包繼承的父包 -->
<constant name="struts.convention.default.parent.package"
value="rest-default"/>
</struts>
實現視圖層
定義了上面 Action 之後,接下來應該為這些 Action 提供視圖頁面了,根據 Convention 插件的約定,所有視圖頁面都應該放在Web-INF\content 目錄下,例如當用戶向 /book.action 發送請求時,該請求將由 BookController 的 index() 方法進行處理,該方法處理結束後返回“index”字符串,也就是將會使用 WEIN-INF\content\book-index.jsp 頁面作為視圖資源。下面是 book-index.jsp 頁面的代碼:
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title> 圖書展示系統 </title>
<link href="<%=request.getContextPath() %>/css/demo.css"
rel="stylesheet" type="text/css" />
</head>
<body>
<s:actionmessage />
<table>
<tr>
<th> 圖書 ID</th>
<th> 書名 </th>
<th> 價格 </th>
<th> 操作 </th>
</tr>
<s:iterator value="model">
<tr>
<td><s:property value="id"/></td>
<td>${name}</td>
<td>${price}</td>
<td><a href="book/${id}"> 查看 </a> |
<a href="book/${id}/edit"> 編輯 </a> |
<a href="book/${id}/deleteConfirm"> 刪除 </a></td>
</tr>
</s:iterator>
</table>
<a href="<%=request.getContextPath() %>/book/new"> 創建新圖書 </a>
</body>
</html>
上面 JSP 頁面非常簡單,它負責迭代輸出 Action 裡包含的集合數據,向該應用 book.action 發送請求將看到如圖 1 所示頁面。
圖 1 使用Struts 2開發的REST服務
Struts 2 的REST插件支持一種資源具有多少表示形式,當浏覽者向 book.xml 發送請求將可以看到如圖 2 所示頁面。
圖 2REST服務的 XML 形式
從圖 2 可以看出,該頁面正是 Action 所包含的全部數據,當使用 XML 顯示時REST插件將會負責把這些數據轉換成 XML 文檔。
除此之外,REST 插件還提供了 JSON 格式的顯示方式,當開發者向 book.json 發送請求將看到如圖 3 所示頁面。
圖 3REST服務的 JSON 形式
Struts 2 的REST插件默認支持 XHTML、XML 和 JSON 三種形式的數據。
當浏覽者單擊頁面右邊的“編輯”鏈接,將會向 book/idVal/edit 發送請求,這是一個包含 ID 請求參數、且指定操作 edit 資源的請求,因此將由 BookController 的 edit() 方法負責處理,處理結束後進入 book-edit.jsp 頁面。浏覽器裡將看到如圖 4 所示頁面。
圖 4 編輯指定圖書
該頁面單擊“修改”按鈕時需要修改圖書信息,也就是需要使用 PUT 操作,但由於 HTML 不支持 PUT 操作,因此需要為該表單頁增加一個額外的請求參數:_method,該請求參數的值為 put。該表單頁的代碼如下:
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title> 編輯 ID 為 <s:property value="id"/> 的圖書 </title>
<link href="<%=request.getContextPath() %>/css/demo.css"
rel="stylesheet" type="text/css" />
</head>
<body>
<s:form method="post"
action="%{#request.contextPath}/book/%{id}">
<!-- 增加 _method 請求參數,參數值為 put 用於模擬 PUT 操作 -->
<s:hidden name="_method" value="put" />
<table>
<s:textfield name="id" label="圖書 ID" disabled="true"/>
<s:textfield name="name" label="書名"/>
<s:textfield name="price" label="價格" />
<tr>
<td colspan="2">
<s:submit value="修改"/>
</td>
</table>
</s:form>
<a href="<%=request.getContextPath() %>/book"> 返回首頁 </a>
</body>
</html>
該表單將提交給 BookController 的 update() 方法處理,update() 方法將負責修改系統裡指定 ID 對應的圖書信息。
與之類似的是,當請求需要執行 DELETE 操作時,一樣需要增加名為 _method 的請求參數,並將該請求參數值設置為 delete。