table

          使用 Struts 2 開發(fā) RESTful 服務(wù)

          級別: 初級

          李 剛, 自由撰稿人

          2009 年 8 月 28 日

          從 V2.1 開始,Struts 2 開始提供 Convention 插件,它允許根據(jù)“約定”來搜索 Action,以及管理 Action 和 Result 的映射。另外,Struts 2.1 還提供了 REST 插件,使 Struts 2 可以支持 Rails 風(fēng)格的 URL,以對外提供 REST 風(fēng)格的資源服務(wù)。本文作者通過代碼示例演示了這些特性。

          REST 簡介

          REST 是英文 Representational State Transfer 的縮寫,這個術(shù)語由 Roy Thomas Fielding 博士在他的論文《Architectural Styles and the Design of Network-based Software Architectures》中提出。從這篇論文的標題可以看出:REST 是一種基于網(wǎng)絡(luò)的軟件架構(gòu)風(fēng)格。

          提示:國內(nèi)很多網(wǎng)絡(luò)資料將 REST 翻譯為“表述性狀態(tài)轉(zhuǎn)移”,不過筆者對這個翻譯不太認同。因為這個專業(yè)術(shù)語無法傳達 REST 的含義,讀者可以先不要理會 REST 到底該如何翻譯,盡量先去理解 REST 是什么?有什么用?然后再來看這個術(shù)語的翻譯。關(guān)于 Roy Thomas Fielding 博士的原文參見如下地址:http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm。

          REST 架構(gòu)是針對傳統(tǒng) Web 應(yīng)用提出的一種改進,是一種新型的分布式軟件設(shè)計架構(gòu)。對于異構(gòu)系統(tǒng)如何進行整合的問題,目前主流做法都集中在使用 SOAP、WSDL 和 WS-* 規(guī)范的 Web Services。而 REST 架構(gòu)實際上也是解決異構(gòu)系統(tǒng)整合問題的一種新思路。

          如果開發(fā)者在開發(fā)過程中能堅持 REST 原則,將可以得到一個使用了優(yōu)質(zhì) Web 架構(gòu)的系統(tǒng),從而為系統(tǒng)提供更好的可伸縮性,并降低開發(fā)難度。關(guān)于 REST 架構(gòu)的主要原則如下:

          • 網(wǎng)絡(luò)上的所有事物都可被抽象為資源(Resource)。
          • 每個資源都有一個唯一的資源標識符(Resource Identifier)。
          • 同一資源具有多種表現(xiàn)形式。
          • 使用標準方法操作資源。
          • 通過緩存來提高性能。
          • 對資源的各種操作不會改變資源標識符。
          • 所有的操作都是無狀態(tài)的(Stateless)。

          僅從上面幾條原則來看 REST 架構(gòu),其實依然比較難以理解,下面筆者將從如下二個方面來介紹 REST。





          回頁首


          資源和標識符

          現(xiàn)在的 Web 應(yīng)用上包含了大量信息,但這些信息都被隱藏在 HTML、CSS 和 JavaScript 代碼中,對于普通瀏覽者而言,他們進入這個系統(tǒng)時無法知道該系統(tǒng)包含哪些頁面;對于一個需要訪問該系統(tǒng)資源的第三方系統(tǒng)而言,同樣無法明白這個系統(tǒng)包含多少功能和信息。

          URI 和 URL

          與 URI 相關(guān)的概念還有 URL,URL 是 Uniform Resource Locator,也就是統(tǒng)一資源定位符的意思。其中 http://www.crazyit.org 就是一個統(tǒng)一資源定位符,URL 是 URI 的子集。簡而言之:每個 URL 都是 URI,但不是每個 URI 都是 URL。

          從 REST 架構(gòu)的角度來看,該系統(tǒng)里包含的所有功能和信息,都可被稱為資源(Resource),REST 架構(gòu)中的資源包含靜態(tài)頁面、JSP 和 Servlet 等,該應(yīng)用暴露在網(wǎng)絡(luò)上的所有功能和信息都可被稱為資源。

          除此之外,REST 架構(gòu)規(guī)范了應(yīng)用資源的命名方式,REST 規(guī)定對應(yīng)用資源使用統(tǒng)一的命名方式:REST 系統(tǒng)中的資源必須統(tǒng)一命名和規(guī)劃,REST 系統(tǒng)由使用 URI(Uniform Resource Identifier,即統(tǒng)一資源標識符)命名的資源組成。由于 REST 對資源使用了基于 URI 的統(tǒng)一命名,因此這些信息就自然地暴露出來了,從而避免 “信息地窖”的不良后果。

          對于當(dāng)今最常見的網(wǎng)絡(luò)應(yīng)用來說,資源標識符就是 URI,資源的使用者則根據(jù) URI 來操作應(yīng)用資源。當(dāng) URI 發(fā)生改變時,表明客戶機所使用的資源發(fā)生了改變。

          從資源的角度來看,當(dāng)客戶機操作不同的資源時,資源所在的 Web 頁(將 Web 頁當(dāng)成虛擬的狀態(tài)機來看)的狀態(tài)就會發(fā)生改變、遷移(Transfer),這就是 REST 術(shù)語中 ST(State Tranfer)的由來了。

          客戶機為了操作不同狀態(tài)的資源,則需要發(fā)送一些 Representational 的數(shù)據(jù),這些數(shù)據(jù)包含必要的交互數(shù)據(jù),以及描述這些數(shù)據(jù)的元數(shù)據(jù)。這就是 REST 術(shù)語中 RE(Representational)的由來了。理解了這個層次之后,至于 REST 如何翻譯、或是否真正給它一個中文術(shù)語,讀者可自行決定。





          回頁首


          操作資源的方式

          對于 REST 架構(gòu)的服務(wù)器端而言,它提供的是資源,但同一資源具有多種表現(xiàn)形式(可通過在 HTTP Content-type 頭中包含關(guān)于數(shù)據(jù)類型的元數(shù)據(jù))。如果客戶程序完全支持 HTTP 應(yīng)用協(xié)議,并能正確處理 REST 架構(gòu)的標準數(shù)據(jù)格式,那么它就可以與世界上任意一個 REST 風(fēng)格的用戶交互。這種情況不僅適用于從服務(wù)器端到客戶端的數(shù)據(jù),反之亦然——倘若從客戶端傳來的數(shù)據(jù)符合 REST 架構(gòu)的標準數(shù)據(jù)格式,那么服務(wù)器端也可以正確處理數(shù)據(jù),而不去關(guān)心客戶端的類型。

          典型情況下,REST 風(fēng)格的資源能以 XHTML、XML 和 JSON 三種形式存在,其中 XML 格式的數(shù)據(jù)是 WebServices 技術(shù)的數(shù)據(jù)交換格式,而 JSON 則是另一種輕量級的數(shù)據(jù)交換格式;至于 XHTML 格式則主要由瀏覽器負責(zé)呈現(xiàn)。當(dāng)服務(wù)器為所有資源提供多種表現(xiàn)形式之后,這些資源不僅可以被標準 Web 瀏覽器所使用,還可以由 JavaScript 通過 Ajax 技術(shù)調(diào)用,或者以 RPC(Remote Procedure Call)風(fēng)格調(diào)用,從而變成 REST 風(fēng)格的 WebServices。

          REST 架構(gòu)除了規(guī)定服務(wù)器提供資源的方式之外,還推薦客戶端使用 HTTP 作為 Generic Connector Interface(也就是通用連接器接口),而 HTTP 則把對一個 URI 的操作限制在了 4 個之內(nèi):GET、POST、PUT 和 DELETE。通過使用通用連接器接口對資源進行操作的好處是保證系統(tǒng)提供的服務(wù)都是高度解耦的,從而簡化了系統(tǒng)開發(fā),改善了系統(tǒng)的交互性和可重用性。

          REST 架構(gòu)要求客戶端的所有的操作在本質(zhì)上是無狀態(tài)的,即從客戶到服務(wù)器的每個 Request 都必須包含理解該 Request 的所有必需信息。這種無狀態(tài)性的規(guī)范提供了如下幾點好處:

          • 無狀態(tài)性使得客戶端和服務(wù)器端不必保存對方的詳細信息,服務(wù)器只需要處理當(dāng)前 Request,而不必了解前面 Request 的歷史。
          • 無狀態(tài)性減少了服務(wù)器從局部錯誤中恢復(fù)的任務(wù)量,可以非常方便地實現(xiàn) Fail Over 技術(shù),從而很容易地將服務(wù)器組件部署在集群內(nèi)。
          • 無狀態(tài)性使得服務(wù)器端不必在多個 Request 中保存狀態(tài),從而可以更容易地釋放資源。
          • 無狀態(tài)性無需服務(wù)組件保存 Request 狀態(tài),因此可讓服務(wù)器充分利用 Pool 技術(shù)來提高穩(wěn)定性和性能。

          當(dāng)然,無狀態(tài)性會使得服務(wù)器不再保存 Request 的狀態(tài)數(shù)據(jù),因此需要在一系列 Request 中發(fā)送重復(fù)數(shù)據(jù),從而提高了系統(tǒng)的通信成本。為了改善無狀態(tài)性帶來的性能下降,REST 架構(gòu)填加了緩存約束。緩存約束允許隱式或顯式地標記一個 Response 中的數(shù)據(jù),這樣就賦予了客戶端緩存 Response 數(shù)據(jù)的功能,這樣就可以為以后的 Request 共用緩存的數(shù)據(jù),部分或全部的消除一些交互,增加了網(wǎng)絡(luò)的效率。但是用于客戶端緩存了信息,也就同時增加了客戶端與服務(wù)器數(shù)據(jù)不一致的可能,從而降低了可靠性。





          回頁首


          Struts 2 的 REST 支持

          約定優(yōu)于配置

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

          從 Struts 2.1 開始,Struts 2 改為使用 Convention 插件來支持零配置。Convention 插件徹底地拋棄了配置信息,不僅不需要使用 struts.xml 文件進行配置,甚至不需要使用 Annotation 進行配置。而是由 Struts 2 根據(jù)約定來自動配置。

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

          由于 Struts 2 的 Convention 插件的主要特點是“約定優(yōu)于配置”,當(dāng)我們已經(jīng)習(xí)慣了 Struts 2 的基本開發(fā)方法之后,如果希望改為使用 Convention 插件也非常容易,我們只要放棄 Stuts 2.1 應(yīng)用原有的配置文件,改為按 Convention 插件的約定來定義 Action 即可。

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





          回頁首


          RestActionMapper 簡介

          從本質(zhì)上來看,Struts 2 依然是一個 MVC 框架,最初設(shè)計 Struts 2 時并沒有按 REST 架構(gòu)進行設(shè)計,因此 Struts 2 本質(zhì)上并不是一個 REST 框架。由于 Struts 2 提供了良好的可擴展性,因此允許通過 REST 插件將其擴展成支持 REST 的框架。REST 插件的核心是 RestActionMapper,它負責(zé)將 Rails 風(fēng)格的 URL 轉(zhuǎn)換為傳統(tǒng)請求的 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 說明,我們發(fā)現(xiàn)它可接受如下幾個參數(shù):

          • struts.mapper.idParameterName:用于設(shè)置 ID 請求參數(shù)的參數(shù)名,該屬性值默認是 id。
          • struts.mapper.indexMethodName:設(shè)置不帶 id 請求參數(shù)的 GET 請求調(diào)用 Action 的哪個方法。該屬性值默認是 index。
          • struts.mapper.getMethodName:設(shè)置帶 id 請求參數(shù)的 GET 請求調(diào)用 Action 的哪個方法。該屬性值默認是 show。
          • struts.mapper.postMethodName:設(shè)置不帶 id 請求參數(shù)的 POST 請求調(diào)用 Action 的哪個方法。該屬性值默認是 create。
          • struts.mapper.putMethodName:設(shè)置帶 id 請求參數(shù)的 PUT 請求調(diào)用 Action 的哪個方法。該屬性值默認是 update。
          • struts.mapper.deleteMethodName:設(shè)置帶 id 請求參數(shù)的 DELETE 請求調(diào)用 Action 的哪個方法。該屬性值默認是 destroy。
          • struts.mapper.editMethodName:設(shè)置帶 id 請求參數(shù)、且指定操作 edit 資源的 GET 請求調(diào)用 Action 的哪個方法。該屬性值默認是 edit。
          • struts.mapper.newMethodName:設(shè)置不帶 id 請求參數(shù)、且指定操作 edit 資源的 GET 請求調(diào)用 Action 的哪個方法。該屬性值默認是 editNew。

          在 RestActionMapper 的方法列表中,我們看到 setIdParameterName、setIndexMethodName、setGetMethodName、setPostMethodName、setPutMethodName、setDeleteMethodName、setEditMethodName、setNewMethodName 等方法,這些方法對應(yīng)為上面列出的方法提供 setter 支持。

          通常情況下,我們沒有必要改變 RestActionMapper 的參數(shù),直接使用這些參數(shù)的默認值就可支持 Rails 風(fēng)格的 REST。根據(jù)前面介紹可以看出:支持 REST 風(fēng)格的 Action 至少包含如下 7 個方法:

          • index:處理不帶 id 請求參數(shù)的 GET 請求。
          • show:處理帶 id 請求參數(shù)的 GET 請求。
          • create:處理不帶 id 請求參數(shù)的 POST 請求。
          • update:處理帶 id 請求參數(shù)的 PUT 請求。
          • destroy:處理帶 id 請求參數(shù)的 DELETE 請求。
          • edit:處理帶 id 請求參數(shù),且指定操作 edit 資源的 GET 請求。
          • editNew:處理不帶 id 請求參數(shù),且指定操作 edit 資源的 GET 請求。

          如果請求需要向服務(wù)器發(fā)送 id 請求參數(shù),直接將請求參數(shù)的值附加在 URL 中即可。表 1 顯示了 RestActionMapper 對不同 HTTP 請求的處理結(jié)果。


          表 1. RestActionMapper 對 HTTP 請求的處理
          HTTP 方法 URI 調(diào)用 Action 的方法 請求參數(shù)
          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 插件允許開發(fā)者提交請求時額外增加一個 _method 請求參數(shù),該參數(shù)值可以為 PUT 或 DELETE,用于模擬 HTTP 協(xié)議的 PUT 和 DELETE 操作。





          回頁首


          為 Struts 2 應(yīng)用安裝 REST 插件

          安裝 REST 插件非常簡單,只需按如下步驟進行即可:

          1. 將 Struts 2 項目下 struts2-convention-plugin-2.1.6.jar、struts2-rest-plugin-2.1.6.jar 兩個 JAR 包復(fù)制到 Web 應(yīng)用的 WEB-INF\lib 路徑下。
          2. 由于 Struts 2 的 REST 插件還需要將提供 XML、JSON 格式的數(shù)據(jù),因此還需要將 xstream-1.2.2.jar、json-lib-2.1.jar、ezmorph-1.0.3.jar 以及 Jakarta-Common 相關(guān) JAR 包復(fù)制到 Web 應(yīng)用的 WEB-INF/lib 路徑下。
          3. 通過 struts.xml、struts.properties 或 web.xml 改變 struts.convention.default.parent.package 常量的值,讓支持 REST 風(fēng)格的 Action 所在的包默認繼承 rest-default,而不是繼承默認的 convention-default 父包。

          對于第三個步驟而言,開發(fā)者完全可以不設(shè)置該常量,如果開發(fā)者不設(shè)置該常量,則意味著開發(fā)者必須通過 Annotation 為每個 Action 類設(shè)置父包。





          回頁首


          實現(xiàn)支持 REST 的 Action 類

          在實現(xiàn)支持 REST 的 Action 之前,我們先為系統(tǒng)提供一個 Model 類:Book,該 Book 類非常簡單,代碼如下:

          public class Book
                      {
                      private Integer id;
                      private String name;
                      private double price;
                      // 無參數(shù)的構(gòu)造器
                      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 類提供一個業(yè)務(wù)邏輯組件:BookService。為了簡單起見,BookService 類不再依賴 DAO 組件訪問數(shù)據(jù)庫,而是直接操作內(nèi)存中的 Book 數(shù)組——簡單地說,本系統(tǒng)中狀態(tài)是瞬態(tài)的,沒有持久化保存,應(yīng)用運行過程中這些狀態(tài)一直存在,但一旦重啟該應(yīng)用,則系統(tǒng)狀態(tài)丟失。下面是 BookService 類的代碼:

          public class BookService
                      {
                      private static Map<Integer , Book> books
                      = new HashMap<Integer , Book>();
                      // 保留下本圖書的 ID
                      private static int nextId = 5;
                      // 以內(nèi)存中的數(shù)據(jù)模擬數(shù)據(jù)庫的持久存儲
                      static {
                      books.put(1 , new Book(1
                      , "瘋狂 Java 講義" , 99));
                      books.put(2 , new Book(2
                      , "輕量級 Java EE 企業(yè)應(yīng)用實戰(zhàn)" , 89));
                      books.put(3 , new Book(3
                      , "瘋狂 Ajax 講義", 78));
                      books.put(4 , new Book(4
                      , "Struts 2 權(quán)威指南" , 79));
                      }
                      // 根據(jù) ID 獲取
                      public Book get(int id)
                      {
                      return books.get(id);
                      }
                      // 獲取系統(tǒng)中全部圖書
                      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 個方法,用于實現(xiàn)對 Book 對象的 CRUD 操作。

          下面開始定義支持 REST 的 Action 類了,這個 Action 類與前面介紹 Struts 2 的普通 Action 存在一些差異——因為該 Action 不再用 execute() 方法來處理用戶請求,而是使用前面介紹的 7 個標準方法來處理用戶請求。除此之外,該 Action 總是需要處理 id 請求參數(shù),因此必須提供 id 請求參數(shù),并為之提供對應(yīng)的 setter 和 getter 方法。

          因為本系統(tǒng)已經(jīng)提供了 Book Model 類,并且為了更好的模擬 Rails 中 ActiveController(Controller)直接訪問 ActiveRecord(Model)的方式,本系統(tǒng)采用了 ModelDriven 的開發(fā)方式,下面是本系統(tǒng)中支持 REST 的 Action 類的代碼。

           // 定義返回 success 時重定向到 book Action
                      @Results(@Result(name="success"
                      , type="redirectAction"
                      , params = {"actionName" , "book"}))
                      public class BookController extends ActionSupport
                      implements ModelDriven<Object>
                      {
                      // 封裝 id 請求參數(shù)的屬性
                      private int id;
                      private Book model = new Book();
                      private List<Book> list;
                      // 定義業(yè)務(wù)邏輯組件
                      private BookService bookService = new BookService();
                      // 獲取 id 請求參數(shù)的方法
                      public void setId(int id)
                      {
                      this.id = id;
                      // 取得方法時順帶初始化 model 對象
                      if (id > 0)
                      {
                      this.model = bookService.get(id);
                      }
                      }
                      public int getId()
                      {
                      return this.id;
                      }
                      // 處理不帶 id 參數(shù)的 GET 請求
                      // 進入首頁
                      public HttpHeaders index()
                      {
                      list = bookService.getAll();
                      return new DefaultHttpHeaders("index")
                      .disableCaching();
                      }
                      // 處理不帶 id 參數(shù)的 GET 請求
                      // 進入添加新圖書。
                      public String editNew()
                      {
                      // 創(chuàng)建一個新圖書
                      model = new Book();
                      return "editNew";
                      }
                      // 處理不帶 id 參數(shù)的 POST 請求
                      // 保存新圖書
                      public HttpHeaders create()
                      {
                      // 保存圖書
                      bookService.saveOrUpdate(model);
                      addActionMessage("添加圖書成功");
                      return new DefaultHttpHeaders("success")
                      .setLocationId(model.getId());
                      }
                      // 處理帶 id 參數(shù)的 GET 請求
                      // 顯示指定圖書
                      public HttpHeaders show()
                      {
                      return new DefaultHttpHeaders("show");
                      }
                      // 處理帶 id 參數(shù)、且指定操作 edit 資源的 GET 請求
                      // 進入編輯頁面 (book-edit.jsp)
                      public String edit()
                      {
                      return "edit";
                      }
                      // 處理帶 id 參數(shù)的 PUT 請求
                      // 修改圖書
                      public String update()
                      {
                      bookService.saveOrUpdate(model);
                      addActionMessage("圖書編輯成功!");
                      return "success";
                      }
                      // 處理帶 id 參數(shù),且指定操作 deleteConfirm 資源的方法
                      // 進入刪除頁面 (book-deleteConfirm.jsp)
                      public String deleteConfirm()
                      {
                      return "deleteConfirm";
                      }
                      // 處理帶 id 參數(shù)的 DELETE 請求
                      // 刪除圖書
                      public String destroy()
                      {
                      bookService.remove(id);
                      addActionMessage("成功刪除 ID 為" + id + "的圖書!");
                      return "success";
                      }
                      // 實現(xiàn) ModelDriven 接口必須實現(xiàn)的 getModel 方法
                      public Object getModel()
                      {
                      return (list != null ? list : model);
                      }
                      }
                      

          上面 Action 代碼中粗體字代碼定義了 7 個方法,這 7 個方法正是前面提到的標準方法。除此之外,該 Action 里還包含一個額外的 deleteConfirm() 方法,這個方法用于處理帶 id 參數(shù)、且指定操作 deleteConfirm 資源的 GET 請求。也就是說,當(dāng)用戶請求 /book/1/deleteConfirm 時,該請求將由該方法負責(zé)處理。實際上,RestActionMapper 不僅可以將對 /book/1/edit 的請求映射到 Book 控制器的 edit() 方法,而 1 將作為 id 請求參數(shù)。實際上,它可以將任意 /book/1/xxx 的請求映射到 Book 控制器的 xxx() 方法,而 1 是請求參數(shù)。上面 Action 類使用了 @Results 進行修飾,這表明當(dāng) Action 的任何方法返回“success”邏輯視圖時,系統(tǒng)將重定向到 book.action。

          可能有讀者會對 index()、create()、show() 三個方法的返回值感到疑惑:它們不再直接返回普通字符串作為邏輯視圖名字,而是返回一個以字符串為參數(shù)的 DefaultHttpHeaders 對象?其實讀者不必對 DefaultHttpHeaders 感到疑惑,其實 DefaultHttpHeaders 只是普通字符串的加強形式,用于 REST 對處理結(jié)果進行更多額外的控制。當(dāng) Action 類的處理方法返回字符串作為邏輯視圖時,Struts 2 只能將其當(dāng)成一個簡單的視圖名,僅能根據(jù)該視圖名映射到實際視圖資源,僅此而已。如果使用 DefaultHttpHeaders 作為邏輯視圖,DefaultHttpHeaders 除了可以包含普通字符串作為邏輯視圖名之外,還可以額外增加更多的控制數(shù)據(jù),從而可以增強對 Response 的控制。關(guān)于 HttpHeaders 和 DefaultHttpHeaders 的介紹請參考 REST 插件的 API。

          還有一點需要指出,上面的 BookController 控制器實現(xiàn)類的類名并不以 Action 結(jié)尾,而是以 Controller 結(jié)尾,因此我們可以在 struts.xml 文件中配置如下常量:

           <!--  指定控制器類的后綴為 Controller -->
                      <constant name="struts.convention.action.suffix"
                      value="Controller"/>
                      本應(yīng)用里的 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>
                      





          回頁首


          實現(xiàn)視圖層

          定義了上面 Action 之后,接下來應(yīng)該為這些 Action 提供視圖頁面了,根據(jù) Convention 插件的約定,所有視圖頁面都應(yīng)該放在 WEB-INF\content 目錄下,例如當(dāng)用戶向 /book.action 發(fā)送請求時,該請求將由 BookController 的 index() 方法進行處理,該方法處理結(jié)束后返回“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> 圖書展示系統(tǒng) </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"> 創(chuàng)建新圖書 </a>
                      </body>
                      </html>
                      

          上面 JSP 頁面非常簡單,它負責(zé)迭代輸出 Action 里包含的集合數(shù)據(jù),向該應(yīng)用 book.action 發(fā)送請求將看到如圖 1 所示頁面。


          圖 1 使用 Struts 2 開發(fā)的 REST 服務(wù)
          圖 1 使用 Struts 2 開發(fā)的 REST 服務(wù)

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


          圖 2 REST 服務(wù)的 XML 形式
          圖 2 REST 服務(wù)的 XML 形式

          從圖 2 可以看出,該頁面正是 Action 所包含的全部數(shù)據(jù),當(dāng)使用 XML 顯示時 REST 插件將會負責(zé)把這些數(shù)據(jù)轉(zhuǎn)換成 XML 文檔。

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


          圖 3 REST 服務(wù)的 JSON 形式
          圖 3 REST 服務(wù)的 JSON 形式

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

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


          圖 4 編輯指定圖書
          圖 4 編輯指定圖書

          該頁面單擊“修改”按鈕時需要修改圖書信息,也就是需要使用 PUT 操作,但由于 HTML 不支持 PUT 操作,因此需要為該表單頁增加一個額外的請求參數(shù):_method,該請求參數(shù)的值為 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 請求參數(shù),參數(shù)值為 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() 方法將負責(zé)修改系統(tǒng)里指定 ID 對應(yīng)的圖書信息。

          與之類似的是,當(dāng)請求需要執(zhí)行 DELETE 操作時,一樣需要增加名為 _method 的請求參數(shù),并將該請求參數(shù)值設(shè)置為 delete。



          參考資料



          關(guān)于作者

           

          李剛,從事 Java EE 應(yīng)用開發(fā)近 10 年。曾任 LITEON 公司的 J2EE 技術(shù)主管,負責(zé)該公司的企業(yè)信息化平臺的架構(gòu)設(shè)計。曾任廣州電信、廣東龍泉科技等公司的技術(shù)培訓(xùn)教師。瘋狂 Java 聯(lián)盟(http://www.crazyit.org)站長。瘋狂 Java 實訓(xùn)營創(chuàng)始人,瘋狂 Java 體系圖書作者,曾任東方標準廣州中心軟件教學(xué)總監(jiān),曾兼任廣東技術(shù)師范學(xué)院計算機科學(xué)系的兼職副教授。國內(nèi)知名IT技術(shù)作家,已出版《瘋狂 Java 講義》、《輕量級 Java EE 企業(yè)應(yīng)用實戰(zhàn)》、《瘋狂 Ajax 講義》、《Struts 2.1 權(quán)威指南》、《Ruby On Rails 敏捷開發(fā)最佳實踐》等著作。

          posted on 2010-03-11 11:06 小卓 閱讀(508) 評論(0)  編輯  收藏 所屬分類: struts


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導(dǎo)航:
           
          主站蜘蛛池模板: 肥东县| 楚雄市| 嘉义市| 三明市| 安仁县| 西乌珠穆沁旗| 香港 | 巫溪县| 贵阳市| 烟台市| 东乡| 峡江县| 井研县| 务川| 清河县| 郑州市| 丹寨县| 永善县| 广宗县| 聂荣县| 南江县| 台江县| 邻水| 东阿县| 泰安市| 崇明县| 辽源市| 秦皇岛市| 凤凰县| 彭泽县| 肥乡县| 安宁市| 佳木斯市| 托克逊县| 临沧市| 达日县| 邵东县| 台安县| 涟源市| 靖边县| 湖口县|