使用 Struts 2 開發 RESTful 服務

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 插件來支持零配置。Convention 插件徹底地拋棄了配置信息,不僅不需要使用 struts.xml 文件進行配置,甚至不需要使用 Annotation 進行配置。而是由 Struts 2 根據約定來自動配置。

Convention 這個單詞的翻譯過來就是“約定”的意思。有 Ruby On Rails 開發經驗的讀者知道 Rails 有一條重要原則:約定優於配置。Rails 開發者只需要按約定開發 ActiveRecord、ActiveController 即可,無需進行配置。很明顯,Struts 2 的 Convention 插件借鑑了 Rails 的創意,甚至連插件的名稱都借鑑了“約定優於配置”原則。

由於 Struts 2 的 Convention 插件的主要特點是“約定優於配置”,當我們已經習慣了 Struts 2 的基本開發方法之後,如果希望改爲使用 Convention 插件也非常容易,我們只要放棄 Stuts 2.1 應用原有的配置文件,改爲按 Convention 插件的約定來定義 Action 即可。

以 Convention 插件爲基礎,Struts 2.1 又新增了 REST 插件,允許 Struts 2 應用對外提供 REST 服務。REST 插件也無需使用 XML 進行配置管理。Struts 2.1 通過 REST 插件完全可以提供讓人和機器客戶端共同使用的資源,並支持 Ruby On Rails 風格的 URL。


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 中即可。表 1 顯示了 RestActionMapper 對不同 HTTP 請求的處理結果。

表 1. 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 服務
圖 1 使用 Struts 2 開發的 REST 服務

Struts 2 的 REST 插件支持一種資源具有多少表示形式,當瀏覽者向 book.xml 發送請求將可以看到如圖 2 所示頁面。

圖 2 REST 服務的 XML 形式
圖 2 REST 服務的 XML 形式

從圖 2 可以看出,該頁面正是 Action 所包含的全部數據,當使用 XML 顯示時 REST 插件將會負責把這些數據轉換成 XML 文檔。

除此之外,REST 插件還提供了 JSON 格式的顯示方式,當開發者向 book.json 發送請求將看到如圖 3 所示頁面。

圖 3 REST 服務的 JSON 形式
圖 3 REST 服務的 JSON 形式

Struts 2 的 REST 插件默認支持 XHTML、XML 和 JSON 三種形式的數據。

當瀏覽者單擊頁面右邊的“編輯”鏈接,將會向 book/idVal/edit 發送請求,這是一個包含 ID 請求參數、且指定操作 edit 資源的請求,因此將由 BookController 的 edit() 方法負責處理,處理結束後進入 book-edit.jsp 頁面。瀏覽器裏將看到如圖 4 所示頁面。

圖 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。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章