玩轉 Java Web 應用開發:Play 框架

                                                                                                      

玩轉 Java Web 應用開發:Play 框架

 

簡介: 雖然目前有很多種 Java Web 應用開發框架,但 Play 框架是一種新興的框架,可以幫助開發人員高效的構建 Web 應用。本文詳細介紹了 Play 框架的使用,包括模型層、控制層、視圖層和 HTTP 路由等內容。通過本文的介紹,開發人員可以瞭解到如何用 Play 框架來快速開發 Web 應用。

概述

Play 框架是一個完整的 Web 應用開發框架,覆蓋了 Web 應用開發的各個方面。Play 框架在設計的時候借鑑了流行的 Ruby on Rails 和 Grails 等框架,又有自己獨有的優勢。使用 Play 框架可以方便和高效的開發出 Java Web 應用。通過 Play 框架提供的命令行工具,可以快速的創建出一個 Web 應用的基本骨架。它的 Java 代碼動態編譯機制,使得修改代碼之後,不需要重啓服務器就可以直接看到修改之後的結果,調試起來非常方便。它使用 JPA 規範來完成領域對象的持久化,可以很方便的使用不同的關係數據庫作爲後臺存儲。使用 Play 框架可以很容易的構建使用 REST 架構風格的應用。它使用 Groovy 作爲視圖層模板使用的表達式語言。模板之間的繼承機制也可以避免代碼的重複。總的來說,Play 框架非常適合快速 Web 應用開發。

Play 框架採用經典的 MVC 架構,把 Web 應用分成模型層、控制層和視圖層三個層次。每個層次對應的文件被存放在不同的目錄下面,方便組織和管理。使用 Play 框架的 Web 應用具有相同的目錄結構,如 圖 1 所示。

圖 1. 使用 Play 框架的 Web 應用的目錄結構

如 圖 1 所示,應用自身的文件被放在 app 目錄下面,三個子目錄分別存放的是 MVC 模式的三個層次的內容。其中 models 和 controllers 目錄下面是 Java 源文件,而 views 目錄下面則是視圖層使用的模板文件。conf 目錄下面存放的是應用的配置文件、HTTP 路由文件和國際化所需的消息文件。public 目錄則是存放 Web 應用的靜態文件,包括 JavaScript、CSS 和圖像文件等。lib 目錄存放所需的額外的 Java 庫。test 目錄存放的是測試結果。

開發環境

本文中使用的 Play 框架的版本是 1.0.3.1,使用的集成開發環境是 Eclipse 3.6, 使用 Dojo 作爲 JavaScript 框架。在 Play 框架官方網站(見 參考資料)下載 Play 框架的壓縮包之後,解壓到某個目錄,並把該目錄下面的 bin 目錄添加到環境變量中。接着啓動一個命令窗口,運行 play new developers_notebook 就可以創建出一個新的名爲 developers_notebook 的 Web 項目。在項目目錄的父目錄下面,運行 play eclipsify developers_notebook 就可以創建出來 Eclipse 工程。通過 Eclipse 導入此工程就可以在 Eclipse 裏面進行開發了。Play 框架的 support 目錄下的 eclipse 目錄下有個名爲 org.playframework.playclipse 的 Eclipse 插件,將此插件複製到 Eclipse 的 plugins 目錄就可以安裝。運行 play run 就可以運行此 Web 應用,訪問 http://localhost:9000就可以看到。每次在 Eclipse 裏面修改了代碼之後,不需要重新啓動應用,只需要刷新頁面就能看到更新之後的結果。這是 Play 框架的一個非常方便的特性。

本文中的示例應用稱爲“開發人員記事本”。開發人員可以用它來記錄開發過程中的一些注意事項。下面首先介紹 Play 框架中的模型層。

模型層

模型層包含的是 Web 應用中的領域對象。Play 框架推薦的實踐是模型層的對象不應該是僅包含 getter/setter 方法的簡單 Java Beans,而應該有自己的業務邏輯。Play 框架中應用的模型層類可以是任何的 Java 類。與一般的 Java Beans 不同的是,模型層類使用聲明爲 public 的域作爲對象的屬性。Play 框架會自動生成相應的 getter/setter 方法。這樣可以使得代碼更加簡潔。開發人員也可以提供自己的 getter/setter 方法實現。

領域對象持久化

領域對象的實例一般需要持久化下來。最常見的持久化方式就是使用關係數據庫。Play 框架使用 JPA 規範來進行領域對象的持久化。具體的後臺實現使用的是 Hibernate。開發人員只需要使用 JPA 規範定義的標註,就可以聲明領域的持久化行爲。比較好的做法是將領域對象類繼承自 Play 框架提供的 play.db.jpa.Model 類。play.db.jpa.Model 類提供了一個域 id作爲對象的標識符,也是對應的數據庫表中的主鍵。play.db.jpa.JPASupport 類是 play.db.jpa.Model的父類,提供了一些實用方法用來完成從領域對象到數據庫之間的映射。表 1 中列出了一些重要的方法,包括常用的增刪改查操作。

表 1. play.db.jpa.JPASupport API 說明

表 1 中列出的方法中,find() 和 all() 方法的返回值是 play.db.jpa.JPASupport.JPAQuery 類的實例,表示一個領域對象實例的查詢結果。對於此查詢結果,可以進行進一步的操作。具體的操作,如 表 2 所示。

表 2. play.db.jpa.JPASupport.JPAQuery API 說明

使用 表 2 中給出的方法,就可以在領域對象類中添加一些非常實用的方法,而不需要把這些方法添加到額外的服務層中。在示例應用中,Note 這個領域對象類表示的是用戶添加的記錄。代碼清單 1 中給出了 Note 類中的一些實用方法。

清單 1. 領域對象類中的實用方法

// 創建新的領域對象 Note 的實例,edit() 方法的使用與 create() 類似
Map<String, String[]> params = new HashMap<String, String[]>();
params.put("note.title", new String[] {"My note"});
params.put("note.content", new String[] {"My note's content"});
Note.create(Note.class, "note", params).save();
// 使用 find() 來進行查找
List<Note> notes = Note.find("byTitle", "My note").fetch();
// 使用 findById() 來查找單個實例
Note note1 = Note.findById(1);
// 使用 delete() 來刪除對象實例
Note.delete("byTitle", "My note");
// 返回查詢結果中的第 2 到第 11 條記錄。
Note.find("byTitle", "My note").from(1).fetch(10);

在介紹完 Play 框架的模型層之後,下面介紹控制層。 控制層

Play 框架中的控制層是模型層和視圖層之間的橋樑。控制層負責接收 HTTP 請求並返回相應的響應。一般來說,控制層的典型實現是接收到 HTTP 請求之後,從請求中獲取一些參數,再調用服務層對應的處理方法。服務層的方法會對領域對象進行操作,完成具體的業務邏輯。最後,某種格式的響應被返回給請求者,如 HTML 頁面、JSON 數據和 XML 數據等。Play 框架的控制層實現使得完成這樣的典型場景變得非常簡單。

Play 框架中的每個控制器都是一個普通的 Java 類,繼承自 play.mvc.Controller 類,在包 controllers 中。控制器類中的每個公開的靜態方法都表示一個動作。每個動作負責完整的請求 / 響應的流程,也就是說,所有前面提到的所有請求/響應的過程都需要在每個動作中來完成。

參數綁定

在控制層實現中很繁瑣但是必不可少的操作就是解析 HTTP 請求中的參數。不同的 Web 開發框架會提供自己的參數解析方式。Play 框架也提供了相應的支持。Play 框架可以解析 HTTP 請求中查詢字符串和 URI 路徑中包含的以及請求體中以格式編碼的參數。所有這些參數都放在 params 對象中,其中包含 get()、getAll() 和 put() 等方法用來獲取和設置參數的值。除了這種傳統的使用方式之外,Play 框架還支持直接把參數的值綁定到動作方法的參數上面。比如一個動作方法的聲明是 show(String username),那麼請求中的參數 username 的值會在 show() 方法被調用時作爲實際參數傳遞進去。Play 框架會負責完成相應的類型轉換。值得一提的是對於日期類型(java.util.Date)的參數,Play 框架支持多種類型的日期格式的轉換。比如動作方法的聲明是 display(Date postedAt),而請求的格式可能是/display?postedAt=2010-09-22,Play 框架會自動完成相應的類型轉換。

除了常見的基本數據類型之外,Play 框架還支持直接綁定領域對象的實例。比如動作方法的聲明是 create(Note note),可以在參數中直接指定對象實例的屬性的值。請求的格式可能是 /create?title=Note123&content=Good。Play 框架會負責創建一個 Note 類的實例,並根據參數的值設置該實例的屬性 title 和 content 的值。這種綁定方式不僅支持簡單對象,還支持嵌套對象和列表。比如 /create?tags[0]=ajax&tags[1]=web 可以設置列表類型屬性 tags 的值。

Play 框架的這種綁定方式還支持文件對象,使得上傳文件變得非常簡單。只需要在表單中添加文件上傳的控件(<input type="file">)並使用 multipart/form-data編碼來提交請求,在動作方法的參數中就可以獲取到上傳文件對應的 java.io.File 對象。比如動作方法的聲明可能是 upload(File picture)。上傳的文件被保存在臨時目錄中,在請求完成之後會被自動刪除。可以在動作方法中完成對上傳文件的操作。

返回響應結果

在控制層的動作方法完成了與業務邏輯相關的處理之後,需要把響應返回給客戶端。響應的結果可能是正確完成,也可能是出現錯誤。Play 框架提供了方便的實現用來返回不同類型的響應。使用 play.mvc.Controller 類提供的不同方法就可以生成這些響應內容。

請求正確完成,HTTP 狀態代碼爲 200。使用 ok() 方法生成不帶內容的響應。使用 render() 方法來生成使用模板的響應。使用renderText() 方法生成 text/plain 類型的純文本響應。使用 renderXml() 方法生成 text/xml 類型的 XML 格式的響應。使用 renderJSON() 方法生成 application/json 類型的 JSON 格式的響應。使用 renderBinary() 方法生成二進制內容的響應。

跳轉到新的頁面,HTTP 狀態代碼爲 3XX。使用 redirect() 方法來跳轉到新的 URL。使用 notModified() 方法來返回狀態代碼 304。
HTTP 狀態代碼 4XX。使用 unauthorized() 方法返回狀態代碼 401。使用 forbidden() 方法返回狀態代碼 403。使用 notFound() 方法返回狀態代碼 404。

服務器內部錯誤,HTTP 狀態代碼 5XX。使用 error() 方法返回狀態代碼 500。

從上面列出的方法可以看出,Play 框架使用一些有意義的方法名稱替換掉了難以記憶的 HTTP 狀態代碼,使用起來更加方便。同時,對於常見的響應格式,包括 HTML、XML、JSON 和二進制內容,都提供了相應的方法,使得開發人員不會遺漏掉響應中 Content-Type 的聲明。

方法攔截

控制層的方法通常需要執行一些橫切的邏輯,比如用戶認證、加載通用信息和記錄日誌等。在 Spring 框架中,這些橫切的邏輯是通過面向方面編程(AOP)的支持來實現的。Play 框架提供了更加簡單易用的方法攔截支持,通過簡單的標註就可以定義一些執行攔截操作的方法。這些方法必須非公開的靜態方法。Play 框架支持的方法攔截標註有 @Before、@After、@Finally 和 @With 等四種。

用 @Before 標註的方法在動作方法執行之前被調用。@After 標註的方法在動作方法執行之後被調用。@Finally 標註的方法在動作方法的響應結果已經成功生成之後被調用。這三個標註都支持額外的兩個屬性:priority 表示標註的方法的優先級,0 爲最高;unless 是一個字符串數組,表示不適用此攔截方法的動作方法的名稱。如 @Before(unless="index") 表示此攔截方法不會應用在動作方法 index() 上。

如果控制器類中存在繼承體系結構的話,父類中聲明的攔截方法對於所有子類的動作方法都是適用的。在有些情況下,開發人員可能希望把攔截方法定義在不同的類體系結構中。由於 Java 不支持多繼承,無法通過繼承的方式來應用來自不同類體系結構上的攔截方法。針對這種情況,Play 框架提供了 @With 標註。在控制器類 ControllerA 中定義的攔截方法可以通過 @With 標註來應用到另外一個控制器類 ControllerB 上,而且不通過繼承方式來實現。只需要在 ControllerB 中聲明 @With(ControllerA.class) 即可。

在介紹完 Play 框架的控制層之後,下面介紹視圖層。

視圖層

Web 開發框架的使用者都習慣於使用某種模板技術來生成 HTML 頁面,這些技術包括常見的 JSP、ASP 和 PHP 等。Play 框架也提供了自己的模板技術,可以用來動態的創建 HTML、XML、JSON 以及其它文本類型的內容。Play 框架的模板技術使用的是 Groovy 語言。Groovy 語言的靈活性和簡潔性使得 Play 框架的模板簡單而且易用。在模板中可以混用靜態內容和生成動態內容的各種元素。在模板中可以使用的動態元素如 表 3 所示。

表 3. 模板中可用的動態元素

Play 框架中的標籤的作用相當於 JSP 中的標籤。Play 框架本身提供一些常用的標籤,開發人員也可以根據需要開發自己的標籤。Play 框架內置提供的標籤說明如 表 4 所示。

表 4. Play 框架提供的標籤

在模板中可以使用來自不同地方的變量。首先是在模板生成的時候,由控制器中的動作方法通過 renderArgs 對象來添加的。如 renderArgs.put("username", "Alex") 就把一個變量 username 添加到了模板中。其次是一些隱含的變量,如 request 表示當前的 HTTP 請求,session 表示當前的會話,params 表示請求中的參數和 out 表示用來輸出響應的 java.io.Writer 對象。最後就是可以通過 #{set} 來設置變量。

模板的繼承

Play 框架中可以使用 #{extends} 和 #{doLayout} 來實現模板之間的繼承。模板的繼承機制對於實現靈活的頁面佈局很有幫助。一個模板可以定義清楚頁面的基本佈局結構,其它模板可以繼承此模板並添加具體的內容。這樣就可以避免在不同模板中重複相同的頁面元素。

在父模板中可以包含任意的內容。在需要由子模板填充的位置,使用 #{doLayout /} 進行聲明即可。在子模板中通過 #{extends} 來聲明所繼承的模板。如 #{extends 'main.html'} 就聲明繼承自模板 main.html。當子模板被生成之後,將包含父模板中的內容。而子模板中只需要定義擴展的內容即可。

自定義標籤

Play 框架自身提供的標籤只能解決一些常見的需求,很多時候開發人員需要根據需要開發出自己的標籤。一個標籤的定義非常簡單,就是一個模板文件。模板文件被存放在 app/views/tags目錄下,文件的名稱就是標籤的名稱。在標籤對應的模板裏面,開發人員可以添加任意的內容。標籤也是支持傳入參數的。在標籤對應的模板文件中可以用在參數名稱前面加上 _ 的方式來引用參數的值。比如一個標籤在使用時的方式是 #{myTag name:'Alex' /},那麼在該標籤的模板文件中,就可以用 ${_name} 來引用參數 name 的值。有些標籤是支持在使用的時候添加標籤體的,如 #{anotherTag} 測試文字 #{/anotherTag}。對於這種情況,在標籤的模板文件中可以用 #{doBody} 來引用標籤體中的內容。
在介紹完 Play 框架的視圖層之後,下面介紹 HTTP 路由。

HTTP 路由

在前面介紹過,Play 框架中的控制器用來接受 HTTP 請求並返回相應的響應。這個過程的重要一環就是 HTTP 請求的 URI 與控制器之間的映射關係。Play 框架提供了靈活的 HTTP 路由功能來完成這個映射。路由信息被保存在 config/routes 文件中,採用簡單的方式進行聲明。每條路由記錄包含 3 個元素,分別是 HTTP 方法的名稱、匹配的 URI 模式以及對應的控制器動作方法。路由記錄表示的含義是當使用給定的 HTTP 方法來請求對應模式的 URI 的時候,控制器動作方法就會被調用。

Play 框架支持的 HTTP 方法有 GET、POST、PUT、DELETE 和 HEAD。使用通配符 *可以匹配任何方法。在 URI 模式的聲明中可以使用正則表達式來表示複雜的映射規則。URI 模式中還可以使用 {...} 來聲明動態的部分。每個動態部分都是有名稱的,可以在控制器動作方法中通過 params 對象來獲取。比如,/notes/home 這樣的 URI 模式會匹配 /notes/home,但是 /notes/{id} 可以匹配 /notes/123 和 /notes/abc,而且 URI 模式中 /notes/ 後面的部分可以作爲參數 id 的值被獲取到。URI 模式 /notes/{<[0-9]+>id} 使用了正則表達式,只會匹配 /notes/後面緊跟的全是數字的情況。在聲明控制器的動作方法的時候,需要使用帶名稱空間的全名,如 myapp.Notes.show。有些動作方法是帶參數的,可以在聲明的時候預先綁定一些參數值,這樣可以方便的添加一些 URI 別名。比如動作方法 Notes.show() 有一個參數 id 用來指明要顯示的內容的 ID。如果參數 id 的值爲 0,則會顯示所有內容的一個列表。這樣的話,就可以定義一個類似 GET /notes/all Notes.show(id:0) 的路由聲明。這樣暴露出來的 URI 更加簡潔和易於記憶。

在路由文件中的路由聲明是按照從上到下的優先級來進行匹配的。比較具體的 URI 模式應該放在比較通用的模式之前。對於靜態文件,可以通過一個特殊的動作方法 staticDir 進行聲明。比如 GET /files staticDir:files 就聲明瞭 files 目錄中包含的是靜態文件。

在介紹完 HTTP 路由之後,下面介紹 Play 框架獨特的無狀態的體系結構。

無狀態的體系結構

HTTP 協議本身就被設計成無狀態的,採用請求 / 響應的模式。不同的請求之間並不存在相互關係。但是這種架構模式在開發某些 Web 應用的時候不是很方便。有些應用要求用戶進行認證登錄之後才能進行某些操作。同樣的 URL,認證和未認證用戶看到的內容是不同的。而且用戶認證成功之後,他應該在一段時間內保持這種認證狀態。否則的話,用戶每次都需要輸入用戶名和密碼才能訪問受限的內容。對於這種情況,很多 Web 開發框架提供了會話的支持,允許應用保存一些與會話相關的數據。Java Servlet 規範中的 javax.servlet.http. HttpSession 就是一種會話的接口。應用的服務器會負責維護每個會話相關的數據。這些數據可以通過一個會話 ID 來進行標識。這個標識會利用瀏覽器的 cookie 機制保存在瀏覽器端,也可以作爲請求 URL 的參數來傳遞。服務器端通過此標識來識別每個會話。在處理相應的請求的時候,就可以根據會話 ID 來獲取保存在服務器端上的會話數據。會話機制的問題是會影響應用的可伸縮性。如果一個應用使用多臺服務器的話,就需要額外的機制來保證同一用戶在不同機器上面的會話是同步的。而無狀態的實現則不存在這個問題,對於某一個請求,由不同機器來處理的結果都是相同的。

Play 框架的設計架構就是無狀態的。它沒有提供服務器端的機制用來維護跨多個請求的數據。如果確實需要保存這樣的數據的話,可以考慮下面幾種方案:

保存在 Session 或 Flash 作用域中。Play 框架中仍然有會話的機制,但是並沒有提供在服務器端保存會話數據的能力。會話數據是保存在瀏覽器的 cookie 中的,由瀏覽器在每次請求的時候自動發送。通過這種方式來達到維護會話數據的目的。由於會話數據是保存在 cookie 中,其大小是有限制的,一般不能超過 4K 字節,而且只能保存字符串類型的數據。Flash 作用域和會話一樣,也是通過 cookie 來保存的。所不同的是,Flash 作用域中的數據只在下次請求中是有效的。

保存在持久化的數據存儲中,如數據庫中。如果需要在多個請求中使用同一個領域對象的話,可以把這個對象的 ID 保存在 Session 或 Flash 作用域中,而在控制器動作方法中使用此 ID 來從數據庫中查詢相應的對象。

保存在暫時性數據存儲中,如緩存中。Play 框架內置了緩存的支持,通過調用類 play.cache.Cache 就可以對緩存進行操作。與使用持久化存儲類似,緩存中的鍵的值可以保存在 Session 或 Flash 作用域中。

對於熟悉了 Java Servlet 規範的開發人員來說,需要一些時間來適應 Play 框架的這種無狀態的體系結構。不過這種結構對於應用的可伸縮性來說,確實是非常有好處的。

介紹完無狀態的體系結構之後,下面介紹一些其它話題。

其它話題

測試

Play 框架對應用的測試也提供了良好的支持。Play 框架一共支持三種類型的測試,分別是單元測試、功能測試和界面測試。單元測試主要用來測試應用的模型層代碼。單元測試用例的 Java 類繼承自 play.test.UnitTest ,可以使用 JUnit 4 提供的標註和斷言。功能測試主要用來測試應用的控制層代碼。功能測試用例的 Java 類繼承自 play.test.FunctionalTest。在測試用例中,可以通過 GET()、POST()、PUT()、DELETE() 和 makeRequest() 等方法來發出 HTTP 請求,也可以直接調用控制器中的動作方法。除此之外,還可以使用一些與 HTTP 響應相關的斷言。如 assertStatus()、assertContentType() 和 assertHeaderEquals() 分別用來驗證 HTTP 狀態代碼、內容類型和 HTTP 頭。界面測試使用 Selenium 工具來進行。開發人員可以使用 Selenium 的語法來編寫測試用例,也可以使用 Play 框架提供的 #{selenium} 標籤。

在進行測試的時候,需要準備一些測試數據。測試數據可以用 YAML 的格式保存在文本文件中,並通過 play.test.Fixtures.laod() 方法來加載這些數據到數據庫中。當測試結束之後,可以通過 deleteAll() 方法來刪除這些數據。

在測試的時候,需要用 play test命令以測試模式啓動應用,再用瀏覽器訪問 http://localhost:9000/@tests 進行測試。

任務調度

在 Web 應用開發中,有時候會需要定期執行一些調度任務,比如數據庫備份和數據同步等。這些任務不是通過 HTTP 請求來觸發的,而是定時執行的。Play 框架提供了內置的任務調度支持的能力。創建新任務的時候,只需要繼承自 play.jobs.Job類,並覆寫 doJob()方法即可。如果要創建的任務有返回結果的話,覆寫 doJobWithResult()方法即可。任務創建完成之後,可以選擇不同的調度方式。一種方式是在應用啓動的時候執行一次。只需要在任務的 Java 類上添加標註 @OnApplicationStart即可。對於定期執行的任務,Play 框架提供了兩個標註:一個是 @Every,用來按照固定的時間間隔調度任務,如 @Every("1h")聲明任務每個小時執行一次;另外一個是 @On,用來聲明描述調度策略的 CRON 表達式。

安全

Play 框架提供了對 Web 應用安全性方面的支持,可以防範一些常見的攻擊方式。前面提到過,Play 框架中的會話數據是保存在瀏覽器的 cookie 中的。這些數據是經過簽名的,可以防止被惡意攻擊者所篡改。應用中的重要數據也不應該保存在會話中。Play 框架中的模板在輸出 HTML 內容的時候,會自動對內容進行轉義,可以防範跨站點腳本攻擊。對於 SQL 注入攻擊,開發人員應該儘量使用提供的 find() 方法來查詢領域對象。對於自己創建的查詢語句,應該在語句中使用佔位符並進行參數綁定,而不是通過字符串相加的方式來創建。爲了防範跨站點請求僞造,Play 框架中的控制器的動作方法都可以使用 checkAuthenticity() 方法來聲明調用此方法時的請求中必須包含合法的令牌。這個令牌用來確保當前請求是由應用自身發出的,而不是被僞造的。通過 session.getAuthenticityToken() 方法可以生成一個只對當前會話有效的令牌,需要在請求的時候附帶此令牌。如果是通過頁面上的表單來提交請求的話,Play 框架也提供了一個標籤 #{authenticityToken /} 用來生成一個包含了令牌的隱藏域,可以直接在模板中使用。

總結

Play 框架作爲一個優秀的 Java Web 應用開發框架,可以幫助開發人員快速高效的構建 Web 應用。它爲開發人員提供了一個良好的基礎架構,並屏蔽了很多底層的實現細節。開發人員可以用一個簡單的視角來看待 Web 應用開發,而不需要關心過多的細節。Web 開發人員可以熟悉 Play 框架,並在開發中選用這個框架。

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