26Play框架教程2学习笔记
文章目錄
- 1 play框架01
- 1.1 概述
- 1.2 特性
- 1.2.1 無縫集成現有開發環境
- 1.2.2 熱重載和修改Bug
- 1.2.3 簡單的無狀態MVC架構
- 1.2.4 HTTP到代碼的映射
- 1.2.5 高效的模板引擎
- 1.2.6 內置JPA支持
- 1.2.7 Full Stack應用框架
- 1.2.8 Play的特性總結
- 2 play框架02
- 2.1 app目錄
- 2.2 conf目錄
- 2.3 public 的目錄
- 2.4 lib目錄
- 2.5 project目錄
- 2.6 target目錄
- 3 play框架03
- 3.1 安裝Play
- 3.2 創建項目
- 3.3 運行程序
- 3.4 配置數據庫
- 3.5 補充
- 3.6 將play項目導入myeclipse
- 4 play框架04
- 4.1 路由
- 4.1.1 HTTP方法
- 4.1.2 URI表達式
- 4.1.3 定義Java調用
- 4.1.4 404作為Action
- 4.1.5 指定靜態參數
- 4.1.6 變量和腳本
- 4.1.7 staticDir:mapping
- 4.1.8 staticFile:mapping
- 4.1.9 虛擬主機
- 4.2 逆向生成URL
- 4.3 設置content type
- 4.4 HTTP內容協商
- 4.3.1 在HTTP頭中設置content type
- 4.3.2 自定義格式
- 4.5 關于REST
- 5 play框架05
- 5.1 概述
- 5.2 獲取HTTP參數
- 5.2.1 使用Map參數
- 5.2.2 高級HTTP綁定
- 5.2.2.1 簡單類型
- 5.2.2.2 日歷類型
- 5.2.2.3 文件類型
- 5.2.2.4 數組和集合類型
- 5.2.2.5 POJO對象綁定
- 5.2.3 JPA對象綁定
- 5.2.4 自定義綁定
- 5.3 結果返回
- 5.3.1 返回文本內容
- 5.3.2 返回JSON字符串
- 5.3.3 返回XML字符串
- 5.3.4 返回二進制內容
- 5.3.5 下載附件功能
- 5.3.6 執行模板
- 5.2.7 為模板作用域添加數據
- 5.3.8 更簡單方式
- 5.3.9 指定其他模板進行渲染
- 5.3.10 重定向URL
- 5.3.11 自定義Web編碼
- 5.4 Action鏈
- 5.5 攔截器
- 5.5.1 @Before
- 5.5.2 @After
- 5.5.3 @Catch
- 5.5.4 @Finally
- 5.5.5 使用@with注解增加更多攔截器
- 5.6 Session和Flash作用域
- 6 play框架06
- 6.1 模板語法
- 6.1.1 表達式(expression):${…}
- 6.1.2 標簽(tag): #{tagName /}
- 6.1.3 引用(action):@{…}或者@@{…}
- 6.1.4 國際化(messages):&{…}
- 6.1.5 注釋(comment):
- 6.1.6 腳本(script): %{…}%
- 6.2 模板繼承
- 7 play框架07
- 7.1 屬性模擬
- 7.2 數據庫配置
- 7.3 數據持久化
- 7.4 無狀態模型
- 8 play框架08
- 8.1 Job實現
- 8.2 Bootstrap Job
- 8.2.1 應用啟動
- 8.2.2 應用停止
- 8.3 Scheduled Job
- 8.4 Job的直接調用
- 9 play框架的請求處理流程
- 9.1 PlayHandler
- 9.2 Invoker與Invocation
- 9.3 Result類
- 9.4 Template類
- 10 play框架的攔截器
- 11 play源碼分析
- 11.1總體流程
- 11.2 Start流程
- 11.3 啟動HTTP服務
1 play框架01
? play框架01–介紹
1.1 概述
Play框架顛覆了臃腫的企業級Java EE規范,以Restful為目標并專注于開發效率,是Java敏捷開發的最佳參考方案。
開發者只要具備Java以及數據庫的相關基礎知識就可以輕松上手,從而讓Web應用開發變得更加容易,提高項目催化速度。
作為Full Stack的Java Web應用框架,Play包括了所有開發中涉及的領域:NIO應用容器,無狀態MVC模型,Hibernate數據持久化,
Groovy模板引擎,以及建立Web應用所需要的各種工具類。需要注意的是,這里雖然使用了Groovy,但只是將其作為頁面模板語言,
和Freemaker、Velocity使用自己定義的語言是同樣的道理。
Groovy的成熟以及它和Java的相似性決定了采用Groovy遠遠好于定義自己的模板語言。
1.2 特性
1.2.1 無縫集成現有開發環境
Play1.x是基于Java的Web開發框架,允許開發者使用自己常用的集成開發工具(如Eclipse)和類庫。
如果讀者已經以Java作為開發方向,那么無須進行開發語言、IDE或者類庫的切換,要做的就是在更加高效的Java環境中開發Web應用。
1.2.2 熱重載和修改Bug
Java在過去因為開發效率低下而臭名昭著,主要是因為其重復和乏味的編譯-打包-部署周期。因此在設計框架的時候對這些因素都進行了重新考量,目標是讓Play應用的開發過程變得更加高效。
Play框架會自動編譯Java源文件,而不用重新啟動Web服務器將代碼熱加載至JVM。這樣做的好處是:當代碼修改完保存后,框架自動編譯并重載修改后的類,只需刷新瀏覽器就可以查看更改的結果,就像在LAMP或者Rails環境中開發一樣。另外一個好處是:開發的時候甚至可以只用簡單的文本編輯器,而不使用功能完備的Java IDE進行開發。
1.2.3 簡單的無狀態MVC架構
一端是數據庫,另一端是Web瀏覽器,為什么我們需要在這兩者之間保存狀態?
有狀態并且基于組件的Java Web框架能夠更加容易地保存頁面狀態,但這同樣帶來了很多其他的問題:如果用戶在新的瀏覽器窗口中重新打開應用會發生什么?用戶按了后退按鈕又會是什么結果?
無共享架構是很多Web應用框架所提倡的(ROR,Django等)。由于瀏覽器變得越來越強大,我們并不需要技巧性地構建HTTP模型來創建偽造的狀態,只需在客戶端使用Ajax或者離線存儲技術就可以很容易地解決狀態問題。無共享架構的另一優勢是使頁面的呈現更加平滑,更容易地實現局部頁面更新(或者漸進式的頁面處理流程)。
1.2.4 HTTP到代碼的映射
如果讀者使用過其他的Java Web框架(比如說Struts)可能會發現,這些框架的底層實現其實是對HTTP協議做了進一步封裝,
所以它們提供的Java API和自身的理念會讓人覺得很不自然。Play框架在設計過程中換了一種思維方式,
即Web應用框架也應該提供完整、直接的方式去訪問HTTP————這也是Play框架和其他Java Web框架最根本的差異。
HTTP,Request/Response模式,Rest架構風格,HTTP內容協商(Content–type negotiation),URI等等,
所有這些都是Play框架的主要概念。如果用戶需要將URI綁定到指定的Java方法調用,只需要在路由文件中以如下方式進行配置:
GET /clients/{id} Clients.show如果Ajax,REST以及管理頁面之間的“前進/后退”操作是日常開發中需要頻繁考慮的需求,
那么Play框架無疑是最佳的選擇,因為針對這些問題它都提供了非常優秀的解決方案。
Play是一個完全無狀態的,只面向請求/響應的框架,所有HTTP請求都具有相同的處理流程:
? 1.框架接收HTTP請求。
? 2.路由組件找到最匹配的規則,接受并處理請求,隨后調用相應的Action方法。
? 3.執行Action中的應用代碼。
? 4.如果需要生成復雜的視圖,使用模板文件進行渲染。
? 5.Action方法的返回結果(HTTP響應代碼以及內容)被轉換為HTTP響應。
1.2.5 高效的模板引擎
? 也許讀者已經深深地感受到了JSP和表達式語言背后的理念,但是為什么在創建標簽庫的時候需要如此多的配置文件?為什么不能直接訪問底層的模型對象?JSP中太多的限制確實讓開發者感到失望,受JSP啟發又不被其約束,Play框架提供了自定義的模板引擎機制。
開發者再也不需要編寫這些令人厭倦的代碼了:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %><c:choose><c:when test="${emails.unread != null && fn:size(emails.unread)}">You have ${fn:size(emails.unread)} unread email(s)!</c:when><c:otherwise>You have no unread emails!</c:otherwise> </c:choose>相信開發者更傾向于用以下方式來書寫模板代碼:
You have ${emails.unread ?: 'no'} ${emails.unread?.pluralize('email')} !Play模板引擎使用的表達式語言為Groovy,它提供了與Java一致的語法。
Play主要使用模板機制來渲染HTML,當然也可以生成其他的文檔格式,比如e-mail messages,JSON等等。
1.2.6 內置JPA支持
? JPA(Java Persistence API)是Java中最簡潔的對象關系映射(object-relational mapping即ORM)API。
如果讀者以前了解或者使用過JPA,就會發現與其他框架相比,在Play中使用會更加方便。這是因為Play框架對其做了進一步封裝,
不需要任何配置,Play會自動開啟JPA實體管理器(EM),一旦代碼被調用就自動進行持久化操作。
此外,實體如果繼承Play提供的play.db.jpa.Model類,操作代碼將會更加簡潔,更加美觀:
public static void messages(int page) {User connectedUser = User.find("byEmail", connected()).first();List<Message> messages = Message.find("user = ? and read = false order by date desc",connectedUser).from(page * 10).fetch(10);render(connectedUser, messages); }1.2.7 Full Stack應用框架
Play框架的最初設計受到實際Java Web開發的啟發,包含了所有創建主流Web應用所需要的工具:
-
通過JDBC提供關系數據庫支持。
-
使用Hibernate進行對象關系映射(JPA)。
-
使用分布式Memcached集成緩存支持。
-
以JSON或者XML的形式提供web service支持。
-
基于OpenID的分布式用戶信息驗證。
-
Web應用可以部署在任何應用服務器上(Tomcat,Jboss,GAE,Cloud等)。
-
圖像處理API(驗證碼)。
此外Play還提供了很多實用的模塊。開發者可以結合這些模塊構建Web應用,
? 這使得我們可以以更加簡單,更加直接的方式重用Java代碼、模板以及靜態資源(比如JavaScript和CSS文件)。
1.2.8 Play的特性總結
自動編譯和重載:當編輯Java文件并保存后,刷新瀏覽器就能立即查看結果。
使用Play開發不需要手動編譯、部署以及重新啟動Web服務器等操作。
無狀態模型:Play是真正的無共享框架,為REST而準備。它可以將同一個應用的多個實例分別部署在多臺服務器上,因而擴展性非常強。
高效的模板引擎:基于表達式語言Groovy的清晰模板引擎,提供了模板的繼承、導入以及標簽自定義等功能。
快速解決錯誤:當錯誤發生時,Play會在瀏覽器中顯示出錯代碼塊并提示問題發生的確切位置。
Full Stack:提供創建Web應用所需的全部功能,集成了Hibernate、OpenID、Memcached等第三方類庫。
純Java:Play采用Java編寫代碼,可以方便地使用任何Java類庫,并且能夠非常好地和Eclipse、Netbeans等IDE集成,只需通過命令生成匹配的項目文件即可。
基于非阻塞的IO模型:允許創建基于長輪詢和WebSocket的主流Web應用。
有趣并且高效:省去了Java應用重啟的時間,提高了應用的開發效率。
2 play框架02
? play框架02–細說目錄結構
play的目錄結構制作的相當精簡,以下是從play官網截下的圖片:
2.1 app目錄
app目錄是代碼目錄,包含了所有的Java或者Scala的源碼,一般的“hello-world”sample程序都含有controllers、models、和views三個目錄,分別對應MVC三層結構中的:C、M和V;我想這大家都和清楚,大家還可以根據自己的項目需要創建其他的目錄,
例如utils、dao等等。例如以下:
如果有需要,你還可以建一個名為“assets”的目錄,里面可以放LESS或者CoffeeScript源文件。
注意:這些controllers、models和views等目錄可以隨著你項目的需要而改變,
例如:你可以寫成com.yourcompany.controllers、com.yourcompnay.model和com.yourcompany.views而不必非得寫成controllers、models和views。
2.2 conf目錄
在這個目錄里,放置的都是這個應用的一些配置文件信息,有兩個主要的文件:
一個是application.conf:意思很明顯,就是整個應用的配置信息,里面會有一些配置的參數。
包括數據庫鏈接中數據源的信息填寫,日志打印的級別等信息等等,還可以自定義一些參數。
注意:在conf中,play默認定義的有:數據庫信息、應用信息(名字、 Secret key、語言等)、日志;
這三塊兒的信息,在conf中直接改后,效果會在應用程序中直接出現。
假如你想一用conf中自定義的配置參數:例如上圖中的阿里云相關的信息,你需要在application.conf中定義之后,在程序中使用
Play.configuration.getString("oss.access_id").getOrElse("diSnug5q4zb9y2mq")來調用。實際上某人的那三塊信息也是這么來調用的。
假如你在application.conf中不想定義過多的自定義信息,你也可以寫一個自定義的conf文件,然后在application.conf中引用(include “fileName.conf”)如下:
routes:路由。非常重要的部分!使用方法非常簡單,在這里定義你需要的rest接口,然后接口后面對應的處理函數。如下圖:
2.3 public 的目錄
這里放置的都是前端頁面相關的信息,例如js、css、json文件、圖片等等。
這些目錄文件的名字是可以改的,但是引用的時候需要注意目錄名字。包括public的名字也是可以改的。前端頁面中需要其中的靜態文件的話,需要再routes中添加:
然后在前端需要靜態文件的地方這么引用:
這里就是用的public目錄下images目錄中的靜態文件。
2.4 lib目錄
如果之前你是做J2EE項目的,這個目錄你一定清楚,這就是放置其他依賴包的地方。
(當然如果Maven有依賴鏈接,盡量用Maven的依賴鏈接)
build.sbt file:
這個文件是整個項目添加依賴包的地方,所有的依賴都寫在這里。
如果你是J2EE開發者的話,你一定知道Maven的pom.xml文件,在這里,build.sbt文件就相當于pom.xml的文件。
2.5 project目錄
這個目錄包含了sbt構建之后的東西:
1、pulgins.sbt:插件sbt
2、build.properties:包含了sbt的版本。
2.6 target目錄
target目錄包含了應用編譯之后的東西,就是編譯后的可執行文件。
3 play框架03
? play框架03–創建項目
3.1 安裝Play
從下載頁面下載最新的二進制包,然后在你喜歡的地方解壓它。
? 如果你用的是Windows,最好避免在路徑中混入空格。比如c:\play就是個比c:\Documents And Settings\user\play更好的選擇。
為了方便操作,你需要添加Play文件夾到你的系統路徑中。這樣你就不需要在play命令前面敲一大通路徑名了。
要想檢查安裝是否成功,打開一個新的命令行窗口,敲下play;應該會出來play的基本使用幫助。
3.2 創建項目
現在Play已經安好了,是時候開始寫博客應用。創建一個Play應用非常簡單,僅需要play命令行工具。之后會生成Play應用的基本架構。
打開一個新的命令行并敲入:
~$ play new yabe它會提醒輸入應用的全名。輸入yabe。
play new命令創建了一個新的文件夾yabe/外加一系列文件和文件夾。其中包括下面各部分:
? app/ 包括應用的核心,劃分為models,controllers和views文件夾。它也可以包括其他Java的包。這是.java源代碼文件所在之處。
? conf/ 包括所有的應用配置文件,特別是主application.conf文件,路由定義文件和用于國際化的信息文件。
? lib/ 包括所有可選的Java庫,比如標準的.jar。
? public/ 包括所有可以公開的資源,比如Javascript文件,樣式表和圖片。
? test/ 包括所有的應用測試。測試可以是Java的JUnit測試或者Selenium測試。
因為Play只使用UTF-8編碼,故所有的文本文件都需要使用UTF-8編碼。確保你的文本編輯器已經做了相應的配置。
如果你開發過Java應用,你可能會奇怪.class文件到哪兒去了。答案是……沒有.class文件了:Play并不使用任何class文件;相反它直接處理Java源代碼。實際上我們使用Eclipse的編譯器來即時編譯Java源代碼
這導致了開發過程中的兩點重要的改進。
? 第一個,Play會自動監測Java源代碼的改變并在運行時自動重載。
? 第二個,當一個Java異常發生時,Play能向你展示更好的錯誤報告 - 帶對應的源代碼的哦~
事實上Play在應用的tmp/文件夾下有字節碼的緩存,但只用于加速重新啟動項目的過程。
如果需要,你可以用play clean清空緩存。
3.3 運行程序
現在可以測試一下新建立的程序了。回到命令行窗口,在yabe/目錄下輸入play run命令。
Play框架將載入程序,啟動Web服務器并監聽9000端口。
打開瀏覽器鍵入http://localhost:9000,程序顯示了一個缺省的歡迎頁。
現在我們來看看這個頁面是怎樣顯示的。
程序的主入口配置在conf/routes文件里。這個文件定義了程序所有可訪問的URL。
打開routes文件,會看到第一個“route”:
GET / Application.index它告訴Play,當服務器收到來自于/路徑的GET請求時要調用Application.index的方法。
在這個程序中,Application.index是controllers.Application.index簡寫,因為controllers包是隱式附加的。
在創建一個標準Java程序時,通常會定義一個入口方法,比如:
public static void main(String[] args) { ... }Play程序則有多個入口方法,每個URL就有一個。這些方法稱為action方法。定義Action方法的類稱為controller。
看看什么是controller。打開yabe/app/controllers/Application.java源文件:
package controllers; import play.mvc.*; public class Application extends Controller { public static void index() { render(); } }controller 類繼承于play.mvc.Controller類,這個類提供了許多controller需要的方法,比如在index action中的render方法。
index action定義為public static void,因為controller類不需要實例化和返回值。
index action很簡單,只是調用了render()方法來通知Play渲染模板。
使用模板是返回HTTP響應的一個最通用的方式。
模板是在/app/views 目錄下的簡單文本文件。
因為這里沒有指定一個模板,index action會使用一個默認的模板:Application/index.html。
打開/yabe/app/views/Application/index.html:
#{extends 'main.html' /}#{set title:'Home' /} #{welcome /}在這個模板中,只有Play tag,與JSP tag類似,#{welcome /} tag會在瀏覽器中生成歡迎信息。
#{extends /} tag 表示這個模板繼承于main.html這個模板。模板繼承可用來創建復雜的web也并重用公共部分。
打開/yabe/app/views/main.html模板:
<!DOCTYPE html> <html> <head> <title>#{get 'title' /}</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <link rel="stylesheet" type="text/css" media="screen" href="@{'/public/stylesheets/main.css'}" /> <link rel="shortcut icon" type="image/png" href="@{'/public/images/favicon.png'}" /> </head> <body> #{doLayout /} </body> </html>#{doLayout /}tag表示index.html插入內容的地方。
試著編輯controller類來看看Play怎么自動加載它。
打開yabe/app/controllers/Application.java,刪除render()后的分號,讓它出錯,就像這樣:
public static void index() { render() }然后到瀏覽器刷新這個頁面,Play會檢測源文件變更并試著加載程序controller,
但是因為controller有錯誤,所以在瀏覽器中顯示一個編譯錯誤。
把剛才的錯誤修改正確,在編輯模板,打開yabe/app/views/Application/index.html覆蓋歡迎消息。
#{extends 'main.html' /} #{set title:'Home' /} <h1>A blog will be here</h1>在瀏覽器刷新這個頁面。
3.4 配置數據庫
在開始寫代碼之前還要多做一件事。作為博客引擎,我們需要一個數據庫。為了便于與開發,Play內置了一個叫做H2的數據庫。
當然如果需要,我們也可以切換到一個更加健壯的數據庫。
你可以選擇設置數據時存儲在內存中,還是在文件系統中(這樣即使你重新啟動,你的數據也會保留)。
在一開始,我們將對應用模型做許多測試和改動。因此,最好選擇存儲在內存中,這樣每次啟動,都不會跟舊數據有任何牽連。
打開yabe/app/application.conf,解除這一行的注釋:
正如你在注釋中看到的一樣,你可以冗余的配置任何JDBC數據庫,甚至配置鏈接池。
現在回到瀏覽器并刷新歡迎頁面。Play將自動啟動數據庫。
檢查下面一行是否出現在應用日志中:
INFO ~ Connected to jdbc:h2:mem:play3.5 補充
如果運行play run 命令出現下面提示:
解決辦法:
找到play\framework\build.bat 修改
為
java -XX:+CMSClassUnloadingEnabled %DEBUG_PARAM% -Dfile.encoding=UTF8 -Dplay.version="%PLAY_VERSION%" -Dsbt.ivy.home="%~dp0..\repository" -Dplay.home="%~dp0." -Dsbt.boot.properties="file:///%p%sbt/sbt.boot.properties" -jar "%~dp0sbt\sbt-launch.jar" %*3.6 將play項目導入myeclipse
前提:已安裝play并配置了環境變量
第一步 :打開你的項目將下面幾項刪除(沒有就跳過)
第二步:在cmd中來到項目的路徑下(cd,不是來到項目里,而是項目名前一級目錄),
? 然后輸入play eclipsify +項目名
第三步:導入
4 play框架04
4.1 路由
Play框架中的路由器是負責將傳入的HTTP請求映射為Action調用(即控制器中被聲明為public static void的方法)的組件。
HTTP請求被MVC框架視為事件,其主要包括以下兩塊內容:
? 。請求路徑(比如/clients/1542,/photos/list),其中可以包含查詢字符串。
? 。HTTP方法(GET,POST,PUT,DELETE)。
Play路由器使用的配置文件為conf/routes,該文件列出了應用需要的所有路由規則。
每條路由由HTTP方法和與Java調用相關聯的URI組成。以下是路由配置的例子:
GET /clients/{id} Clients.show? 路由配置總是從HTTP方法開始,URI作為中間部分,最后的元素是Java調用。
在路由文件中可以使用#進行注釋:
# Display a client GET /clients/{id} Clients.show4.1.1 HTTP方法
? HTTP協議支持以下所列的方法,用于指定客戶請求服務器的動作,其中GET和POST是最為常用的兩種方法:
? 。GET
? 。POST
? 。PUT
? 。DELETE
? 。HEAD
Play同時也支持以WebSocket的方式來調用服務器端的Action方法
如果在路由文件中指定*作為HTTP方法,那么這個路由會匹配任何HTTP請求:
* /clients/{id} Clients.show使用上述的路由配置,以下兩個HTTP請求都會被框架接受:
GET /clients/1541 PUT /clients/12124.1.2 URI表達式
? URI表達式定義了路由規則需要的請求路徑,請求路徑中允許存在動態內容,但必須被聲明在{}中。
/clients/all? 以上的路由配置只能精確匹配到:
/clients/all但是如果以包含動態部分配置路由規則:
/clients/{id}? 則可以分別匹配:
/clients/12121? 和
/clients/toto如果某條路由配置的URI中需要包含多個動態部分,可以采用下例方法進行配置:
/clients/{id}/accounts/{accountId}? 默認情況下,動態部分的匹配策略采用的是正則表達式/[^/]+/。
也可以為動態部分定義自己的正則表達式,以下是使用正則表達式的例子。
路由規則只允許接受id為數字的值:
/clients/{<[0-9]+>id}? 路由規則確保id是長度為4到10字符的小寫單詞:
/clients/{<[a-z]{4,10}>id}? 正則表達式的使用非常靈活,還可以定義更多的路由規則,本節就不做贅述了。
注意:
動態部分指定后,控制器可以在HTTP參數map中獲取該值。
默認情況下,Play將URI尾部的斜線(“/”)作為重要的組成部分,因為有無“/”將會出現不同的結果。比如:
GET /clients Clients.index? 該路由規則會匹配/clients,而不是/clinets/(注意這里的區別),但可以通過在斜線后面增加問號來同時匹配兩個URI:
GET /clients/? Clients.index注意:
URI除了尾斜線不允許有其他可選的部分。
4.1.3 定義Java調用
? 路由定義的最后部分為需要調用的Java方法:控制器中必須定義指定的Action方法,否則會提示找不到控制器方法的錯誤信息;
必須聲明為public static void方法;控制器需作為play.mvc.Controller的子類定義在controllers包中。
? 如果控制器沒有在controllers包中定義,在配置路由規則時可以在其名稱之前增加Java包(比如admin.Dashboard.index)的說明。
由于controllers包本身被Play默認包含,所以用戶在配置路由時不需要顯式地指定。
GET /admin admin.Dashboard.index4.1.4 404作為Action
? 可以直接使用404作為路由配置中的Action部分。如果這樣進行配置,對應的URL路徑就會被Play應用所忽略。
比如:
# 忽略favicon請求 GET /favicon.ico 4044.1.5 指定靜態參數
? 在某些情況下,可能會需要基于不同的參數值定義特殊路由。以下是預先定義好的Action:
public static void page(String id) {Page page = Page.findById(id);render(page); }? 針對該Action,常規的路由配置為:
GET /pages/{id} Application.page? 現在給參數id=home的頁面指定一條特殊的URL,需要通過設置靜態參數來實現:
GET /home Application.page(id:'home') GET /pages/{id} Application.page? 當參數id=home時,兩條路由配置等價,但是由于前者具有較高的優先級,
? 所以被作為默認的URL來調用Application.page。
4.1.6 變量和腳本
? 與模板中的使用方法類似,在routes文件中可以使用${…}作為變量表達式,使用%{…}作為腳本表達式,
比如:
%{ context = play.configuration.getProperty('context', '') }%# 主頁 GET ${context} Secure.login GET ${context}/ Secure.login? 在路由文件中定義變量和腳本的典型例子是CRUD模塊的routes文件。
該文件中使用crud.types標簽對model類型進行迭代,為每種類型生成控制器路由定義。
以后文章會詳細介紹CRUD模塊的使用。
#{crud.types} GET /? ${type.controllerClass.name.substring(12).replace('$','')}.index GET /${type.controllerName} ${type.controllerClass.name.substring(12).replace('$','')}.list GET /${type.controllerName}/new ${type.controllerClass.name.substring(12).replace('$','')}.blank GET /${type.controllerName}/{id} ${type.controllerClass.name.substring(12).replace('$','')}.show GET /${type.controllerName}/{id}/{field} ${type.controllerClass.name.substring(12).replace('$','')}.attachment GET /${type.controllerName}/{id}/edit ${type.controllerClass.name.substring(12).replace('$','')}.edit POST /${type.controllerName} ${type.controllerClass.name.substring(12).replace('$','')}.create POST /${type.controllerName}/{id} ${type.controllerClass.name.substring(12).replace('$','')}.save DELETE /${type.controllerName}/{id} ${type.controllerClass.name.substring(12).replace('$','')}.delete #{/crud.types}Play會按照聲明的順序,優先選擇最先聲明的路由,比如:
GET /clients/all Clients.listAll GET /clinets/{id} Clients.show? 在上例的路由配置中,雖然請求/clients/all可以同時匹配這兩條路由配置,
但按照聲明的優先順序會被第一條路由攔截,并調用相應的Clients.listAll方法。
如果id參數需要匹配5個數字,在不使用重復規則的前提下,只能連續使用五個\d元字符,而使用重復規則后,規則的如下:
GET /clinets/{<\d{5}>id} Clients.index? 以下路由規則匹配2個大寫字母以及3-4個數字:
GET /clinets/{<[A-Z]{2}[0-9]{3,4}>id} Clients.index4.1.7 staticDir:mapping
? Play的路由配置使用特殊的Action(staticDir)將存放靜態資源的public目錄開放。
該目錄里包含的資源可以是圖片,Javascript,Stylesheet等,這些資源將直接響應給客戶端,并不需要服務器做進一步加工處理:
GET /public/ staticDir:public
當客戶端請求/public/*路徑時,Play會從應用的public文件夾中獲取相應的靜態資源。這里的優先級與標準路由配置一樣適用。
4.1.8 staticFile:mapping
? 還可以直接將URL路徑映射為靜態文件:
GET /home staticFile:/public/html/index.html? 當客戶端通過GET方法請求/home時,服務器將不做任何處理直接把/public/html目錄下面的index.html文件返回給客戶端。
4.1.9 虛擬主機
Play的路由器具有主機匹配功能,當Action的變量需要從主機參數(指子域名,而不是子目錄)中獲取時,就顯得特別有用。
比如SAAS應用可以使用如下方式配置路由規則:
GET {client}.mysoftware.com/ Application.index? 根據以上配置,框架會自動獲取client的值作為請求的參數:
public static void index(String client) {... }? 如果在模板中使用@@{…}標簽,那么框架會根據指定的條件來選擇對應的路由,這種方式在很多場合下都非常實用。比如,需要在產品中使用額外的服務器來提供靜態資源,則可以采用如下方式進行路由配置:
#{if play.Play.mode.isDev()}GET /public/ staticDir:public #{/} #{else}GET assets.myapp.com/ staticDir:public #{/}? 對應模板中的代碼如下:
<img src="@@{'/public/images/logo.png'}">? 當應用在DEV模式下運行時,靜態資源的URL為http://locahost:9000/public/images/logo.png;
? 如果運行在PROD模式下,URL為http://assets.myapp.com/images/logo.png。
4.2 逆向生成URL
Play路由器是按照Java調用生成URL的,所以可以將URI表達式都集中到同個配置文件中,使得重構應用變得更加便捷。
比如,為conf/routes文件添加如下路由配置:
GET /clients/{id} Clients.show? 之后在Java代碼中,就可以調用Client.show來生成URL:
map.put("id", 1541); String url = Router.reverse("Clients.show", map).url; // GET /clients/1541注意:
URL的生成已經集成到框架的大部分組件當中,一般我們不需要直接調用Router.reverse方法。
如果增加的參數不包含在URI表達式中,這些參數會被添加到查詢字符串中:
map.put("id", 1541); map.put("display", "full"); String url = Router.reverse("Clients.show", map).url; // GET /clients/1541?display=full? 同樣地,路由器會根據優先順序匹配最適的URL。
4.3 設置content type
? Play會根據request.format設定的值,選擇指定的media類型來響應HTTP請求。
該值通過文件擴展名來決定使用何種視圖模板進行渲染,并且通過Play框架中的mime-types.properties文件進行映射處理(映射關系詳見play\framework\src\play\libs\mime-types.properties文件),為media類型設定Content-type響應。
? Play請求的默認格式為html,因此index()控制器方法默認的渲染模板文件為index.html。
如果需要指定其他的格式,有以下四種方式:
(1)可以在程序代碼調用render()方法之前進行格式設置。比如將media類型設置為text/css,就可以使用CSS文件進行渲染:
public static void index() {request.format = "css"; render(); }(2)推薦一種更直接的做法,直接在routes文件中使用URL來指定格式。以下列路由配置為例:首先客戶端通過index.xml請求服務器,服務器端將響應格式設置為xml,最后使用index.xml模版進行渲染。
GET /index.xml Application.index(format:'xml')? 同樣地,我們也可以使用CSS進行渲染:
GET /stylesheets/dynamic_css css.SiteCSS(format:'css')(3)Play還可以直接從URL中獲取請求格式,動態指定渲染的模板類型。參考如下路由配置:
GET /index.{format} Application.index? 當請求為/index.xml時,服務器會將返回格式設置為xml并使用相應的XMl文件進行渲染;
? 請求為/index.txt時,則會使用文本進行渲染。
(4)使用Play中的HTTP內容協商進行格式設置,詳見以后更新的內容。
4.4 HTTP內容協商
? Play與其他REST架構的框架一樣,直接使用HTTP方法,而不是試圖隱藏HTTP或者在上面構建抽象層。
內容協商是HTTP的特性,它允許HTTP服務器根據客戶端的請求類型,實現同個URL提供不同的media類型響應。
客戶端可以在Accept header中設置media屬性,指定可接收的響應類型。如果用戶需要XML響應,則進行如下設置:
Accept:application/xml? 客戶端可以指定多種media類型,或使用cacth-all通配符(/)來指定任何media類型。
Accept:application/xml,image/png,*/*? 常規的Web瀏覽器總是在Accept header中包含了通配符的值,這樣瀏覽器便會接受任何media類型。
Play將HTML作為默認格式進行渲染,因此在客戶端使用HTTP內容協商就顯得特別有用:通過Ajax請求返回JSON格式,
或是使文檔以PDF和EPUB形式顯示等
4.3.1 在HTTP頭中設置content type
? 如果Accept header中包含了text/html,application/xhtml或者通配符 /,Play會選擇使用其默認的請求格式(即HTML)。
只有當通配符的值被顯式指定時,Play才會選擇其默認的請求格式。
Play內置了一些常規格式支持:html、txt、json、xml。
下例代碼定義了控制器方法(Action)進行數據渲染:
public static void index() { final String name = "Peter Hilton"; final String organisation = "Lunatech Research"; final String url = "http://www.lunatech-research.com/"; render(name, organisation, url); }? 如果在瀏覽器中訪問http://localhost:9000,Play默認會使用index.html模板進行渲染,因為瀏覽器發送了包含text/html的Accept header。
通過將請求的格式設置為xml,可以使用index.xml模板響應標識為Accept: text/xml的請求:
<?xml version="1.0"?> <contact> <name>${name}</name> <organisation>${organisation}</organisation> <url>${url}</url> </contact>? 下表針對index()控制器方法給出了Play內置的Accept header請求格式映射:
Accept header包含了Play能夠映射成的所有格式(最后轉化為相應的模板文件),如表3.1:
? (表3.1 Play內置的Accept header請求格式映射)
| null | null | index.html | null格式請求提供默認模版擴展 |
| image/png | null | index.html | media類型沒有映射為指定格式 |
| /, image/png | html | index.html | 默認將media類型映射為html格式 |
| text/html | html | index.html | 內置映射 |
| application/xhtml | html | index.html | 內置映射 |
| text/xml | xml | index.xml | 內置映射 |
| application/xml | xml | index.xml | 內置映射 |
| text/plain | txt | index.txt | 內置映射 |
| text/javascript | json | index.json | 內置映射 |
| application/json, / | json | index.json | 內置映射, 忽略默認media類型 |
4.3.2 自定義格式
? 在Play中可以通過檢查HTTP請求頭,為應用選擇相應的media類型來實現自定義格式。
比如使用@Before標簽攔截該控制器下的所有Action,檢查請求的media類型是否為text/x-vcard:
@Before static void setFormat() { if (request.headers.get("accept").value().equals("text/x-vcard")) { request.format = "vcf"; } }? 如果檢查后發現請求頭中media類型為text/x-vcard時,將調用index.vcf模板渲染:
BEGIN:VCARD VERSION:3.0 N:${name} FN:${name} ORG:${organisation} URL:${url} END:VCARD4.5 關于REST
REST全稱為Representational State Transfer,即表述性狀態傳輸。
它是一種為分布式超媒體系統(比如萬維網)而設計的軟件架構方式。REST定義了一些關鍵的規則:
? 。應用的所有功能都被劃分為資源。
? 。每個資源都使用URI來唯一訪問。
? 。所有資源共享統一的接口用于客戶端與資源之間進行狀態傳輸。
如果應用使用的是HTTP協議,那么這些接口是通過一系列可用的HTTP方法來定義的。
HTTP協議往往通過以下方法來使用資源的狀態:
? 。客戶端-服務器模式。
? 。無狀態模式。
? 。緩存模式。
? 。分層模式。
如果應用遵循了REST設計規則,那么該應用就可以被稱為RESTful了。
Play框架可以很容易地構建RESTful應用:
? 。Play的路由器通過解析URI和HTTP方法,將請求路由至Action方法,基于正則表達形式的URI為開發提供了更好的靈活性。
? 。協議是無狀態的,這意味著在兩次成功的請求之間不會把任何狀態保存在服務器中。
? 。Play將HTTP作為關鍵的特性,因此框架提供了對HTTP信息的完全訪問。
5 play框架05
? play框架05–控制層
5.1 概述
Play的控制層位于應用的controllers包中,其中的Java類即為控制器(Controller)。
如圖4.1所示,Application.java和MyController.java都屬于控制層。
? (圖4.1 控制器為controllers包中的Java類)
控制器需要繼承play.mvc.Controller:
package controllers;import models.Client; import play.mvc.Controller;public class Clients extends Controller {public static void show(Long id) {Client client = Client.findById(id);render(client);}public static void delete(Long id) {Client client = Client.findById(id);client.delete();}}在控制器中,每個以public static聲明,返回值為void的方法稱為Action。
Action的方法聲明如下:
public static void action_name(params…);Play會自動將HTTP請求參數轉化為與之相匹配的Action方法參數,這部分內容會在后面的獲取HTTP參數小節進行詳細講解。
通常情況下,Action方法無需返回任何值,以調用結果方法來終止執行。
在上述例子中,render(…)方法就是用來渲染模板的結果方法。
HTTP請求中往往包含各種參數,這些參數的傳遞形式如下:
-
URI路徑:在路徑/clients/1541中,1541是URI的動態部分。
-
查詢字符串:clients?id=1541。
-
請求體:如果請求是來自HTML的表單提交(GET或者POST),
那么請求體包含的是表單數據(采用x-www-urlform-encoded作為編碼格式)。
針對以上幾種情況,Play會自動提取這些HTTP參數并將他們保存在Map<String,String>類型的變量中,以參數名作為Map的key。
這些參數名分別來自于:
- URI中動態部分的名稱(在routes文件中定義)。
- 查詢字符串中“名稱/值”對中的名稱部分 。
- 采用x-www-urlform-encoded編碼的表單數據的參數名。
5.2 獲取HTTP參數
5.2.1 使用Map參數
HTTP請求中參數對象(params)在任何控制器中都是可訪問的(該實現在play.mvc.Controller超類中定義),
它包含了當前所有HTTP請求的參數,并且可以通過get()方法得到,具體如下:
public static void show(){String id=params.get("id");String[] names=params.getAll("name"); }這些參數也可以進行類型轉換:
public static void show(){Long id=params.get("id",Long.class); }本節將推薦一種更好的解決方案。Play框架提供了自動將Action聲明的參數與HTTP參數自動匹配的功能(只需要保持Action方法的參數名和HTTP參數名一致即可):
/clients?id=1541Action方法可以在聲明中以id作為參數,以此匹配HTTP中變量名為id的參數:
public static void show(String id){System.out.println(id); }當然,也可以使用其他Java參數類型,而不僅僅是String。
在下面的例子中框架會自動將參數轉換為正確的數據類型:
public static void show(Long id){System.out.println(id); }如果參數含有多個值,那么可以定義數組參數,具體如下:
public static void show(Long[] id){for(Long anId:id){System.out.println(anId);} }參數甚至可以是List類型:
public static void show(List<Long> id){for(Long anId:id){System.out.println(anId);} }注意:
如果Action與HTTP之間的參數無法匹配,Play會將該參數設置為默認值(通常情況下對象類型為null,原始數據類型為0)。
如果參數可以匹配但不能正確進行數據轉換,那么Play會先生成錯誤并添加到驗證器的error對象集合中,然后將參數設置為默認值。
5.2.2 高級HTTP綁定
5.2.2.1 簡單類型
Play可以實現所有Java原生的簡單數據類型的自動轉換,
主要包括:int,long,boolean,char,byte,float,double,Integer,Long,Boolean,Char,String,Float,Double。
日期類型
如果HTTP參數字符串符合以下幾種數據格式,框架能夠自動將其轉換為日期類型:
yyyy-MM-dd'T'hh:mm:ss’Z' // ISO8601 + timezone yyyy-MM-dd'T'hh:mm:ss" // ISO8601 yyyy-MM-dd yyyyMMdd'T'hhmmss yyyyMMddhhmmss dd'/'MM'/'yyyy dd-MM-yyyy ddMMyyyy MMddyy MM-dd-yy MM'/'dd'/'yy而且還能通過@As注解,指定特定格式的日期,例如:
archives?from=21/12/1980 public static void articlesSince(@As("dd/MM/yyyy") Date from) {List<Article> articles = Article.findBy("date >= ?", from);render(articles); }也可以根據不同地區的語言習慣對日期的格式做進一步的優化,具體如下:
public static void articlesSince(@As(lang={"fr,de","*"}, value={"dd-MM-yyyy","MM-dd-yyyy"}) Date from) {List<Article> articles = Article.findBy("date >= ?", from);render(articles); }在這個例子中,對于法語和德語的日期格式是dd-MM-yyyy,其他語言的日期格式是MM-dd-yyyy。
語言值可以通過逗號隔開,且需要與參數的個數相匹配。
如果沒有使用@As注解來指定,Play會采用框架默認的日期格式。為了使默認的日期格式能夠正常工作,
按照以下方式編輯application.conf文件:
date.format=yyyy-MM-dd在application.conf文件中設置默認的日期格式之后,就可以通過${date.format()}方法對模板中的日期進行格式化操作了。
5.2.2.2 日歷類型
日歷類型和日期類型非常相像,當然Play會根據本地化選擇默認的日歷類型。
讀者也可以通過@Bind注解來使用自定義的日歷類型。
5.2.2.3 文件類型
在Play中處理文件上傳是件非常容易的事情,首先通過multipart/form-data編碼的請求將文件發送到服務器,
然后使用java.io.File類型提取文件對象:
public static void create(String comment, File attachment) {String s3Key = S3.post(attachment);Document doc = new Document(comment, s3Key);doc.save();show(doc.id); }新創建文件的名稱與原始文件一致,保存在應用的臨時文件下(Application_name/tmp)。
在實際開發中,需要將其拷貝到安全的目錄,否則在請求結束后會丟失。
5.2.2.4 數組和集合類型
所有Java支持的數據類型都可以通過數組或者集合的形式來獲取。
數組形式:
public static void show(Long[] id){... }List形式:
public staic void show(List<Long> id){... }集合形式:
public static void show(Set<Long> id){... }Play還可以處理Map<String, String>映射形式:
public static void show(Map<String, String> client) {... }例如下面的查詢字符串會轉化為帶有兩個元素的map類型,
第一個元素key值為name,value為John;
第二個元素key值為phone,value為111-1111, 222-2222。:
?user.name=John&user.phone=111-1111&user.phone=222-22225.2.2.5 POJO對象綁定
Play使用同名約束規則(即HTTP參數名必須與模型類中的屬性名一致),自動綁定模型類:
public static void create(Client client){client.save();show(client); }以下的查詢字符串可以通過上例的Action創建client:
?client.name=Zenexity&client.email=contact@zenexity.fr框架通過Action創建Client的實例,并將HTTP參數解析為該實例的屬性。如果出現參數無法解析或者類型不匹配的情況,會自動忽略。
參數綁定是遞歸執行的,這意味著可以深入到關聯對象:
?client.name=Zenexity &client.address.street=64+rue+taitbout &client.address.zip=75009 &client.address.country=FrancePlay的參數綁定提供數組的支持,可以將對象id作為映射規則,更新一組模型對象。
假設Client模型有一組聲明為List的customers屬性,那么更新該屬性需要使用如下查詢字符串:
?client.customers[0].id=123 &client.customers[1].id=456 &client.customers[2].id=7895.2.3 JPA對象綁定
通過HTTP參數還可以實現JPA對象的自動綁定。Play會識別HTTP請求中提供的參數user.id,自動與數據庫中User實例的id進行匹配。
一旦匹配成功,HTTP請求中的其他User屬性參數可以直接更新到數據庫相應的User記錄中:
public static void save(User user){user.save(); }和POJO映射類似,可以使用JPA綁定來更改對象,但需要注意的是必須為每個需要更改的對象提供id:
user.id = 1 &user.name=morten &user.address.id=34 &user.address.street=MyStreet5.2.4 自定義綁定
綁定機制支持自定義功能,可以按照讀者的需求,自定義參數綁定的規則。
? @play.data.binding.As
@play.data.binding.As注解可以依據配置提供綁定的支持。下例使用DateBinder指定日期的數據格式:
public static void update(@As("dd/MM/yyyy") Date updatedAt) {... }@As注解還具有國際化支持,可以為每個本地化提供專門的注解:
public static void update(@As(lang={"fr,de","en","*"},value={"dd/MM/yyyy","dd-MM-yyyy","MM-dd-yy"})Date updatedAt) {... }@As注解可以和所有支持它的綁定一起工作,包括用戶自定義的綁定。以下是使用ListBinder的例子:
public static void update(@As(",") List<String> items) {... }上例中的綁定使用逗號將字符串分隔成List。
? @play.data.binding.NoBinding
@play.data.binding.NoBinding注解允許對不需要綁定的屬性進行標記,以此來解決潛在的安全問題。
比如:
//User為Model類 public class User extends Model {@NoBinding("profile") public boolean isAdmin;@As("dd, MM yyyy") Date birthDate;public String name; }//editProfile為Action方法 public static void editProfile(@As("profile") User user) {... }在上述例子中,user對象的isAdmin屬性始終不會被editProfile方法(Action)所修改,
即使有惡意用戶偽造POST表單提交user.isAdmin=true信息,也不能修改user的isAdmin權限。
play.data.binding.TypeBinder
@As注解還提供完全自定義綁定的功能。自定義綁定必須是TypeBinder類的實現:
public class MyCustomStringBinder implements TypeBinder<String> {public Object bind(String name, Annotation[] anns, String value, Class clazz) {return "!!" + value + "!!";} }定義完成后,就可以在任何Action中使用它:
public static void anyAction(@As(binder=MyCustomStringBinder.class) String name) {... }@play.data.binding.Global
Play中還可以自定義全局Global綁定。以下是為java.awt.Point類定義綁定的例子:
@Global public class PointBinder implements TypeBinder<Point> {public Object bind(String name, Annotation[] anns, String value, Class class) {String[] values = value.split(",");return new Point(Integer.parseInt(values[0]),Integer.parseInt(values[1]));} }因此外部模塊很容易通過自定義綁定來提供可重用的類型轉換組件。
5.3 結果返回
Action方法需要對客戶端作出HTTP響應,最簡單的方法就是發送結果對象。當對象發送后,常規的執行流程就會中斷。
以下面這段代碼為例,最后一句System.out.println的輸出不會被執行:
public static void show(Long id) {Client client = Client.findById(id);render(client);System.out.println("This message will never be displayed !"); }render(…)方法向模板發送client對象,之后的其他語句將不會執行,所以在控制臺中,
并不會打印出“This message will never be displayed !”。
5.3.1 返回文本內容
renderText(…)方法直接將文本內容寫到底層HTTP響應中:
public static void countUnreadMessages(){Integer unreadMessages=MessagesBos.countUnreadMessage();renderText(unreadMessages); }也可以通過Java標準的格式化語法對輸出的文本進行處理:
public static void countUnreadMessages(){Integer unreadMessages=MessagesBox.countUnreadMessages();renderText("There are %s unread messages",unreadMessages); }5.3.2 返回JSON字符串
越來越多的應用使用JSON作為數據格式進行交互,Play對此進行了很好的封裝,
只需要使用renderJSON(…)方法就可以輕松地返回JSON字符串。
在使用renderJSON(…)方法時,Play會自動將服務器返回的響應的content type值設置為application/json,
并且將renderJSON(…)方法中的參數以JSON格式返回。
在使用renderJSON(…)方法時,可以輸入字符串格式的參數,自行指定JSON返回的內容。
public static void countUnreadMessages() {Integer unreadMessages = MessagesBox.countUnreadMessages();renderJSON("{\"messages\": " + unreadMessages +"}"); }以上范例在使用renderJSON(…)方法時,傳入了拼接成JSON格式的字符串參數。
Play框架會對其進行自動設置,改變content type的值為application/json。
當然,renderJSON(…)方法的功能并不只有這些。因為大部分的應用需求,都會要求服務端返回比較復雜的JSON格式,如果都采用字符串拼接的方式組成JSON內容,就太不人性化了。renderJSON(…)的輸入參數還可以是復雜的對象,如果采用這種方式使用renderJSON(…)方法,Play在執行renderJSON(…)時,底層會先調用GsonBuilder將對象參數進行序列化,之后再將復雜的對象以JSON的格式返回給請求。這樣開發者就可以完全透明地使用renderJSON(…)方法,不需要做其他的任何操作了,以下代碼范例將會展示renderJSON(…)的這個功能。
public static void getUnreadMessages() {List<Message> unreadMessages = MessagesBox.unreadMessages();renderJSON(unreadMessages); }5.3.3 返回XML字符串
與使用renderJSON(…)方法返回JSON內容類似,如果用戶希望以XML格式對內容進行渲染,
可以在Controller控制器中直接使用renderXml(…)方法。
使用renderXml(…)方法時,Play會自動將服務器返回的響應的content type值設置為application/xml。
在使用renderXml(…)方法時,可以輸入字符串格式的參數,自行指定XML返回的內容。
public static void countUnreadMessages() {Integer unreadMessages = MessagesBox.countUnreadMessages();renderXml("<unreadmessages>"+unreadMessages+"</unreadmessages>"); }如果希望將復雜的對象以XML格式進行渲染,可以在使用renderXml(…)方法時輸入org.w3c.dom.Document格式的對象,
或者直接輸入POJO對象。以POJO對象作為參數使用renderXml(…)方法時,Play會使用XStream將其進行序列化操作。
同樣的,這些序列化操作都不需要由開發者去做,全部交給Play就行,開發者需要做的就是按照規范簡單地調用renderXml(…)方法即可。
public static void getUnreadMessages() {Document unreadMessages = MessagesBox.unreadMessagesXML();renderXml(unreadMessages); }5.3.4 返回二進制內容
Play為開發者提供了renderBinary(…)方法,可以非常方便的返回二進制數據(如存儲在服務器里的文件、圖片等)給客戶端。
以下代碼范例將會展示如何使用renderBinary(…)方法進行二進制圖片的渲染。
public static void userPhoto(long id) { final User user = User.findById(id); response.setContentTypeIfNotSet(user.photo.type());java.io.InputStream binaryData = user.photo.get();renderBinary(binaryData); }首先,開發者需要建立用于持久化的域模型User,該User模型具有play.db.jpa.Blob類型的屬性photo。
play.db.jpa.Blob是經過Play封裝的特有的屬性類型,可以很方便的處理二進制數據。之后,在Controller控制器中使用時,
需要調用域模型的findById(…)方法加載持久化的數據,并將圖片以二進制數據流InputStream的形式進行渲染。
5.3.5 下載附件功能
如果開發者希望將存儲在服務器端的文件,采用下載的形式渲染給客戶端用戶,需要對HTTP的header進行設置。
通常的做法是通知Web瀏覽器將二進制響應數據以附件的形式,下載至用戶的本地電腦上。
在Play中完成這個功能非常簡單,只需要在使用renderBinary(…)方法時多傳入一個文件名的參數即可。
這樣做會觸發renderBinary(…)的額外功能,提供文件名并設置響應頭的Content-Disposition屬性。
之后二進制文件(包括圖片)將會以附件下載的形式,渲染給用戶。
public static void userPhoto(long id) { final User user = User.findById(id); response.setContentTypeIfNotSet(user.photo.type());java.io.InputStream binaryData = user.photo.get();renderBinary(binaryData, user.photoFileName); }5.3.6 執行模板
如果需要響應的內容比較復雜,那么就應該使用模板來進行處理:
public class Clients extends Controller{public static void index(){render();} }模板的名稱遵從Play的約束規則,默認的模板路徑采用控制器和Action的名稱相結合的方式來定義,
比如在上述例子中,模板對應的路徑為:app/views/Clients/index.html。
5.2.7 為模板作用域添加數據
通常情況下模板文件都需要數據進行顯示,可以使用renderArg()方法為模板注入數據:
public class Clients extends Controller {public static void show(Long id) {Client client = Client.findById(id);renderArgs.put("client", client);render(); } }在模板執行過程當中,client變量可以被使用:
<h1>Client ${client.name}</h1>5.3.8 更簡單方式
這里介紹一種更簡單的方式向模板傳遞數據。
直接使用render(…)方法注入模板數據:
public static void show(Long id){Client client=Client.findById(id);render(client); }以該方式進行數據傳遞,模板中可訪問的變量與Java本地變量的名稱(也就是render()方法中的參數名)一致。
當然也可以同時傳遞多個參數:
public static void show(Long id){Client client=Client.findById(id);render(id,client); }注意:
render()方法只允許傳遞本地變量。
5.3.9 指定其他模板進行渲染
如果讀者不希望使用默認的模板進行渲染,那么可以在renderTemplate(…)方法的第一個參數中指定其他自定義的模板路徑,
例如:
public static void show(Long id) {Client client = Client.findById(id);renderTemplate("Clients/showClient.html", id, client); }5.3.10 重定向URL
redirect(…)方法產生HTTP重定向響應,可以將請求轉發到其他URL:
public static void index(){redirect("http://www.oopsplay.org"); }5.3.11 自定義Web編碼
Play推薦開發者使用UTF-8作為應用開發的編碼格式,如果不進行任何設置,Play框架默認使用的也就是UTF-8格式。
但是具體情況并不總是這么理想,有些特殊的需求可能要求某些響應(response)的格式為ISO-8859-1,
或者要求整個應用都必須保持ISO-8859-1編碼。
為當前響應設置編碼格式
如果需要改變某一個響應(response)的編碼格式,可以直接在Controller控制器中進行修改,
具體做法如下所示:
response.encoding = "ISO-8859-1";? 當開發表單提交功能時,如果開發者希望某一表單提交的內容采用非框架默認使用的編碼(即Play框架采用默認的編碼格式UTF-8,
而該form表單提交的內容希望采用ISO-8859-1編碼格式),Play的做法有一些特殊。在書寫form表單的HTML代碼時,
需要對采用何種編碼格式進行兩次標識。首先需要在標簽中添加accept-charset屬性(如:accept-charset=“ISO-8859-1”),
accept-charset屬性會通知瀏覽器當form表單提交的時候,采用何種編碼格式;
其次,需要在form表單中添加hidden隱藏域,name屬性規定為“charset”,value屬性為具體需要的編碼格式,
這樣做的目的是當form提交的時候,可以通知服務端的Play采用何種編碼方式,
具體范例如下:
<form action="@{application.index}" method="POST" accept-charset="ISO-8859-1"><input type="hidden" name="_charset_" value="ISO-8859-1"> </form>定義全局編碼格式
通常情況下,整個應用應該保持統一的編碼格式。
如果開發者需要設置應用全局的編碼格式,可以在application.conf配置文件中修改application.web_encoding屬性,配置相應的編碼。
5.4 Action鏈
Play中的Action鏈與Servlet API中的forward不盡相同。Play的每次HTTP請求只能調用一個Action,
如果需要調用其他的Action,那么必須將瀏覽器重定向到相應的URL。
在這種情況下,瀏覽器的URL始終與正在執行的Action保持對應關系,使得后退、前進、刷新操作更加清晰。
調用控制器中其他Action方法也可以實現重定向,框架會攔截該調用并生成正確的HTTP重定向。
具體實現如下:
public class Clients extends Controller {public static void show(Long id) {Client client = Client.findById(id);render(client);}public static void create(String name) {Client client = new Client(name);client.save();show(client.id);} }相應的路由規則定義如下:
GET /clients/{id} Clients.show POST /clients Clients.create按照定義,Action鏈的生命周期為:
-
瀏覽器向/clients發送POST請求;
-
路由器調用Clients控制器中的create方法;
-
create方法直接訪問show方法;
-
Java調用被攔截,路由器逆向生成帶有id參數的URL來調用Clients.show;
-
HTTP響應重定向為:/clients/3132;
-
瀏覽器地址欄中URL展現為:/clients/3132;
5.5 攔截器
控制器中可以定義攔截方法(也可稱之為攔截器),為控制器及其子類的所有Action提供服務。
當所有的Action都需要進行通用的處理時,該功能就顯得非常有用:比如驗證用戶的合法性,加載請求范圍內的信息等。
讀者在使用時需要注意的是,這些攔截器方法不能定義為public,但必須是static,并通過有效的攔截標記進行注解。
5.5.1 @Before
使用@Before注解的方法會在每個Action調用之前執行。如創建具有用戶合法性檢查的攔截器:
public class Admin extends Application {@Beforestatic void checkAuthentification() {if(session.get("user") == null) login();}public static void index() {List<User> users = User.findAll();render(users);}...}如果不希望@Before注解攔截所有的Action方法,那么可以使用unless參數列出需要排除的方法:
public class Admin extends Application {@Before(unless="login")static void checkAuthentification() {if(session.get("user") == null) login();}public static void index() {List<User> users = User.findAll();render(users);}...}或者直接使用only參數把需要攔截的方法列舉出來:
public class Admin extends Application {@Before(only={"login","logout"})static void doSomething() { ...}... }unless和only參數對@After,@Before以及@Finally注解都適用。
5.5.2 @After
使用@After注解的方法會在每個Action調用之后執行:
public class Admin extends Application {@Afterstatic void log() {Logger.info("Action executed ...");}public static void index() {List<User> users = User.findAll();render(users);}...}5.5.3 @Catch
如果有Action方法拋出了異常,那么使用@Catch注解的方法就會執行,且拋出的異常會以參數的形式傳遞到@Catch注解的方法中。
具體實現如下:
public class Admin extends Application {@Catch(IllegalStateException.class)public static void logIllegalState(Throwable throwable) {Logger.error("Illegal state %s…", throwable);}public static void index() {List<User> users = User.findAll();if (users.size() == 0) {throw new IllegalStateException("Invalid database - 0 users");}render(users);} }使用@Catch注解和普通的Java異常處理程序一樣,捕獲父類往往可以獲得更多的異常類型。
如果擁有多個需要捕獲的方法,可以通過指定優先級來確定他們的執行順序。具體實現如下:
public class Admin extends Application {@Catch(value = Throwable.class, priority = 1)public static void logThrowable(Throwable throwable) {// Custom error logging…Logger.error("EXCEPTION %s", throwable);}@Catch(value = IllegalStateException.class, priority = 2)public static void logIllegalState(Throwable throwable) {Logger.error("Illegal state %s…", throwable);}public static void index() {List<User> users = User.findAll();if(users.size() == 0) {throw new IllegalStateException("Invalid database - 0 users");}render(users);} }5.5.4 @Finally
@Finally注解的方法總是在每個Action調用之后執行(無論Action是否成功執行):
public class Admin extends Application {@Finallystatic void log() {Logger.info("Response contains : " + response.out);}public static void index() {List<User> users = User.findAll();render(users);}...}如果@Finally注解的方法中包含的參數是可拋出的異常,其方法中的內容還是可以繼續執行的,具體如下:
public class Admin extends Application {@Finallystatic void log(Throwable e) {if( e == null ){Logger.info("action call was successful");} else{Logger.info("action call failed", e);}}public static void index() {List<User> users = User.findAll();render(users);}... }5.5.5 使用@with注解增加更多攔截器
如果某個控制器是其他一些類的父類,那么該控制器中定義的所有攔截器會影響到所有子類。
由于Java不允許多重繼承,對單純通過繼承來使用攔截器造成了一定的局限性。
Play可以通過@With注解,調用其他控制器中已經定義好的攔截方法,從而突破這一局限。
比如創建Secure控制器,定義checkAuthenticated()攔截方法驗證用戶合法性:
public class Secure extends Controller {@Beforestatic void checkAuthenticated() {if(!session.containsKey("user")) {unAuthorized();}} }在其他的控制器中,可以通過@With(Secure.class)注解將其包含進來:
@With(Secure.class) public class Admin extends Application {... }5.6 Session和Flash作用域
在Play開發中,如果需要在HTTP請求之間保存數據,可以將數據保存在Session或者Flash內。
保存在Session中的數據在整個用戶會話中都是有效的,而保存在Flash的數據只對下一次請求有效。
特別需要注意的是,Session和Flash作用域中的數據都是采用Cookie機制添加到隨后的HTTP響應中的(并沒有存儲在服務器上的),
所以數據大小非常有限(不能超過4K),而且只能存儲字符串類型的數據。
由于Cookie是使用密鑰簽名過的,所以客戶端不能輕易修改Cookie的數據(否則會失效)。
不要將Play的Session當作緩存來使用,如果需要在特定的會話中緩存一些數據,那么可以使用Play內置的緩存機制,
并將session.getId()作為緩存的key進行儲存。
public static void index() {List messages = Cache.get(session.getId() + "-messages", List.class);if(messages == null) { // 處理緩存失效messages = Message.findByUser(session.get("user"));Cache.set(session.getId() + "-messages", messages, "30mn");}render(messages); }Session在用戶關閉瀏覽器后就會失效,除非修改配置文件中的application.session.maxAge屬性。
設置方法如下:
application.session.maxAge=7d # Remember for one week.使用Play內置的Cache緩存時需要注意,Cache與傳統Servlet的HTTP Session對象是不同的。
框架無法保證這些緩存對象會一直存在,所以在業務代碼中必須處理緩存失效的問題,以便保持應用完全無狀態化。
6 play框架06
? play框架06–模板語法、模板繼承
Play具有高效的模板體系,采用Groovy作為其表達式語言,允許動態生成HTML、XML、JSON或者任何基于文本格式的文檔,
并且具有創建可重用標簽(tag)的功能。
模板儲存在Play應用的app/views目錄下。
6.1 模板語法
與其他的語言一樣,Play的模板也具有嚴格定義的語法。模板語法被劃分為多種元素,用于完成不同類型的任務。
Play模板的本質是普通的文本文件,其中帶有占位符的部分可以生成動態內容。
模板的動態部分采用Groovy語言編寫,其語法與Java非常類似。
框架可以將需要渲染的結果追加至HTTP響應的數據部分,并發送至模板。
所有的動態部分將會在模板的執行期間被解析。
? (表1 模板語法)
| 表達式 | 用于輸出表達式的值。如 ${note.title} 的作用是將域對象note的屬性title的值輸出。 | ${…} |
| 標簽 | 用于調用Play框架內置的或是開發人員自定義的標簽。如#{get ‘title’ /}:獲取變量 title 的值,該值僅在模板頁面中有效。 | #{…} |
| 引用 | 用于生成調用控制器中Action方法的URL,在頁面鏈接中使用的最為頻繁。@{…} 和 @@{…} 的區別在于生成的URL分別是相對路徑還是絕對路徑。如: 1.首頁:生成指向首頁的鏈接。 2.@{’/public/stylesheets/main.css’}:引入CSS靜態資源文件。 | @{…} 和@@{…} |
| 國際化 | 用于顯示經過國際化處理后的消息內容。 | &{…} |
| 注釋 | 用于在模板中添加注釋。如:{ 這是注釋 }。 | {…} |
| 腳本 | 用于添加復雜的 Groovy 腳本,可以聲明變量和執行業務邏輯。 | %{…}% |
6.1.1 表達式(expression)😒{…}
構建動態部分最簡單的方法就是聲明表達式。表達式需要以 ${ 開頭, 并以 } 結尾,作為占位符使用。
具體例子如下:
<h1>Client ${client.name}</h1>以上是輸出客戶姓名的表達式例子。在該例子中,將客戶類定義為Client。
經過控制器中Action的業務邏輯執行,首先需要向模板注入對象client,之后就可以在模板中使用${…}表達式語法輸出client對象的name屬性。
如果不能確定向模板注入的client對象是否為null,可以使用如下Groovy快捷語法:
<h1>Client ${client?.name}</h1>此時,只有client不為null的情況下,才進行client.name的輸出。
6.1.2 標簽(tag): #{tagName /}
標簽是能夠附帶參數調用的模板片段,如果標簽只有一個參數,按照約定,參數的名稱為arg,并且該參數名是可以省略的。
例如,可以使用#{script}標簽加載JavaScript文件:
#{script 'jquery.js' /}Play模板中的標簽必須是閉合的,可以通過兩種方式閉合標簽。采用直接閉合的形式:
#{script 'jquery.js'/}或者起始標簽和結束標簽成對地使用:
#{script 'jquery.js'}#{/script}#{script}是Play模板的內置標簽,由框架實現,可以直接使用。后面會進一步介紹如何自定義標簽來滿足開發中一些特定的需求。
#{list}標簽可以對集合類進行迭代操作,使用時需要注意,必須帶有兩個參數(items以及as):
<h1>Client ${client.name}</h1> <ul>#{list items:client.accounts, as:'account' }<li>${account}</li>#{/list} </ul>上例中使用#{list}標簽對client.accounts集合進行迭代,并將集合中的每一條數據作為account在頁面中輸出。
在應用中,模板引擎默認對所有的動態表達式進行轉義,以此來避免XSS的安全問題。
如果模板中變量${title}的內容為
Title
,在頁面輸出時會自動進行轉義: ${title} --> <h1>Title</h1>也可以通過調用擴展方法raw(),以非轉義的形式在頁面中輸出,具體使用方法如下:
${title.raw()} --> <h1>Title</h1>如果需要顯示大量的非轉義HTML內容,可以使用#{verbatim /}標簽:
#{verbatim}${title} --> <h1>Title</h1> #{/verbatim}6.1.3 引用(action)😡{…}或者@@{…}
在前面的章節已經有過一些介紹,Play通過路由器可以(逆向)生成URL,匹配指定的路由。
在模板中使用@{…}引用可以達到相同的目的:
<h1>Client ${client.name}</h1> <p><a href="@{Clients.showAccounts(client.id)}">All accounts</a> </p> <hr /><a href="@{Clients.index()}">Back</a>該實例中,@{Clients.showAccounts(client.id)}調用了Clients控制器中的showAccounts Action方法,并傳遞了client.id參數。
@@{…}引用的使用語法與@{…}相同,只不過生成的是絕對URL(尤其適用于郵箱)。
6.1.4 國際化(messages):&{…}
如果應用需要進行國際化操作,那么可以在模板中使用&{…}顯示國際化信息。
需要進行國際化的應用首先需要在conf/messages文件中進行國際化定義:
clientName=The client name is %s之后在模板中就可以通過&{…}顯示該國際化信息了:
<h1>&{'clientName',client.name}</h1>6.1.5 注釋(comment):
*{…}*使用*{…}*標記的內容會被模板引擎忽略,起到注釋作用:*{**** Display the user name ****}* <div class="name">${user.name} </div>6.1.6 腳本(script): %{…}%
腳本是更加復雜的表達式集合,能夠聲明一些變量或者定義一些語句。
Play的模板中使用%{…}%插入腳本:
%{fullName = client.name.toUpperCase()+' '+client.forname; }%<h1>Client ${fullName}</h1>也可以直接使用out內置對象輸出動態內容:
%{fullName = client.name.toUpperCase()+' '+client.forname;out.print('<h1>'+fullName+'</h1>'); }%在模板中還可以使用腳本編寫結構化語句,執行一些邏輯操作,比如迭代:
<h1>Client ${client.name}</h1> <ul> %{for(account in client.accounts) { }%<li>${account}</li> %{} }% </ul>使用模板時切記:模板不適合處理復雜的業務邏輯,所以在模板中請盡量使用標簽,或者直接將處理交給控制器或模型對象。
6.2 模板繼承
Play提供繼承機制為多模板之間進行頁面布局或設計提供服務。
#{extends /} 和 #{doLayout /}可以使頁面的共享和重用變得更加方便簡潔,
同時也能夠幫助開發人員實現頁面中動態內容和靜態外觀裝飾的分離。
在模板與裝飾之間可以使用#{get}和#{set}標簽進行參數傳遞。
將simpledesign.html定義為父模板,也就是裝飾模板。
可以將頁面中重用的靜態內容定義在裝飾模板中,如導航條、頁面頂端的圖片banner、頁面底端的footer說明等。
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head><title>#{get 'title' /}</title><link rel="stylesheet" type="text/css" href="@{'/public/stylesheets/main.css'}" /> </head><body><h1>#{get 'title' /}</h1>#{doLayout /}<div class="footer">Built with the play! framework</div> </body> </html>在simpledesign.html中將、
、 中的內容定義為公用元素,所有繼承于該模板的頁面都會包含這些內容。其中#{doLayout /}標簽起到占位的作用,包含其子模板的頁面內容。
其他所有繼承于simpledesign.html模板的頁面內容都將顯示在#{doLayout /}所占的頁面區塊。
其他頁面使用#{extends}標簽可以非常簡單地植入該裝飾模板,
具體使用方法如下:
#{extends 'simpledesign.html' /}#{set title:'A decorated page' /} This content will be decorated.該子模板使用#{extends ‘simpledesign.html’ /}標簽來繼承simpledesign.html,
使用#{set title:‘A decorated page’ /}標簽傳遞頁面的title變量,
最后在頁面中輸出This content will be decorated。
7 play框架07
? play框架07–域模型
7.1 屬性模擬
查看Play提供的示例應用,模型類里面會頻繁地使用聲明為public的變量。即使是經驗尚淺的Java開發者,也懂得慎用public類型的變量。
在Java開發中(當然還有其他的面向對象語言),實踐經驗是這樣告訴我們的:將所有的成員變量聲明為私有,只提供獲取與修改的方法。
這樣做的目的在于增強程序的封裝性,而“封裝”在面向對象設計中恰恰是非常關鍵的概念。
Java沒有真正的內置屬性定義機制,而是使用Java Bean來進行約束:Java對象的屬性通過一對getXxx/setXxx的方法來修改,
如果對象是只讀的那么只需要提供getXxx方法。
在過去的開發中我們一直這樣做,但是編碼過程就顯得有些乏味了。
每個屬性必須聲明為private,同時還有相應的getXxx/setXxx方法,
而且大多數情況下,getXxx和setXxx方法的實現都是類似的。
private String name; public String getName() {return name; } public void setName(String value) {name = value; }Play框架的模型部分會自動生成getXxx/setXxx方法,保持代碼的簡潔。
也就是說,在Play中開發者可以直接把屬性變量聲明為public,運行時Play會自動生成相應的getXxx/setXxx方法(在這里我們將聲明為public的字段都視為屬性)。
public class Product {public String name;public Integer price; }上述代碼被框架載入后就會轉換成如下形式:
public class Product {public String name;public Integer price;public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getPrice() {return price;}public void setPrice(Integer price) {this.price = price;} }因為變量被聲明為public,可以使用如下方式對屬性進行操作:
product.name = "My product"; product.price = 58;程序在加載時會自動地轉換成:
product.setName("My product"); product.setPrice(58);注意:
因為這些getXxx/setXxx方法是在運行時動態生成的,所以不能直接調用。
如果在編碼階段使用他們,編譯器會因為找不到該方法而報錯誤。
當然也可以自己定義相應的getXxx/setXxx方法,Play會優先選擇手動編寫的方法。
如果需要保護Product類的price屬性就可以定義setPrice()方法:
public class Product {public String name;public Integer price;public void setPrice(Integer price) {if (price < 0) {throw new IllegalArgumentException("Price can’t be negative!");}this.price = price;} }如果為Product類的price屬性賦負值就會拋出異常:
product.price = -10: // Oops! IllegalArgumentExceptionPlay總會優先使用已經定義的getXxx/setXxx方法:
@Entity public class Data extends Model {@Requiredpublic String value;public Integer anotherValue;public Integer getAnotherValue() {if(anotherValue == null) {return 0;}return anotherValue;}public void setAnotherValue(Integer value) {if(value == null) {this.anotherValue = null;} else {this.anotherValue = value * 2;}} public String toString() {return value + " - " + anotherValue;} }補充:
@Entity注解的作用是通知Play自動開啟JPA實體管理器,@Required是對該屬性的約束。
該類繼承于play.db.jpa.Model,Model提供了非常簡單的對象處理方式,在后面章節會做詳細介紹。
針對以上例子可以進行如下測試斷言:
Data data = new Data(); data.anotherValue = null; assert data.anotherValue == 0; data.anotherValue = 4 assert data.anotherValue == 8;以上的斷言都會執行通過,而且因為這種改進的類遵從JavaBean規范,可以滿足開發中的更復雜需求。
7.2 數據庫配置
通常情況下,開發者需要將模型對象持久化。最常用的方法是把這些數據保存到數據庫中。
在Play應用的開發過程中,開發者可以迅速配置嵌入式內存數據庫或者直接將數據保存到文件系統中。
開啟內存數據庫H2,只需要在conf/application.conf文件中進行如下配置:
db=mem補充:
H2是開放源代碼的Java數據庫,其具有標準的SQL語法和Java接口,可以自由使用和分發,且非常簡潔和快速。
將數據保存在內存中相比從磁盤上訪問能夠極大地提高應用的性能,
但由于內存容量的限制,內存數據庫適用于開發階段,或者原型示例開發。
如果需要將數據保存在文件系統中,則使用如下配置:
db=fs如果需要連接到MySQL服務器,則使用如下配置:
db=mysql:user:pwd@database_namePlay框架集成了H2數據庫和MySQL數據庫的驅動程序,存放在$PLAY_HOME/framework/lib/目錄下。
如果需要使用PostgreSQL,Oracle或者其他數據庫,需要在該目錄(或者應用程序的lib/目錄)下添加相應的數據庫驅動。
Play可以連接任何JDBC兼容的數據庫,只需要將相應的驅動類庫添加到/lib目錄中,并在conf/application.conf文件中定義JDBC配置:
db.url=jdbc:mysql://localhost/test db.driver=com.mysql.jdbc.Driver db.user=root db.pass=123456還可以在conf/application.conf文件中用配置選項指定JPA方言:
jpa.dialect=<dialect>補充:
由于不同的數據庫產品支持不同的ANSI SQL標準,所以Hibernate必須要使用“方言”才能與各種數據庫成功的進行通信。
在Play中,大多數情況下會自動根據配置信息識別特定數據庫方言,但是存在某些數據庫,Play無法判斷其使用的方言。
這時就需要開發者顯式地在Play配置文件中指定。
除了使用Hibernate外,在編碼時還可以直接從play.db.DB中獲得java.sql.Connection,然后使用標準SQL語句來執行數據庫操作。
Connection conn = DB.getConnection(); conn.createStatement().execute("select * from products");7.3 數據持久化
Play的持久層框架采用的是Hibernate,使用Hibernate(通過JPA)自動地將Java對象持久化到數據庫。
當在任意的實體類上增加@javax.persistence.Entity注解后,Play會自動為其開啟JPA實體管理器。
@Entity public class Product {public String name;public Integer price; }注意:
Play應用開發者一開始可能經常會犯的錯誤是使用Hibernate的@Entity注解來取代JPA。
這里請讀者注意,Play是直接調用JPA的API來使用Hibernate。
也可以直接從play.db.jpa.JPA對象中得到實體管理器,通過實體管理器可以將Model持久化到數據庫或者執行HQL語句,
例如:
EntityManager em = JPA.em(); em.persist(product); em.createQuery("from Product where price > 50").getResultList();Play為JPA的使用提供了非常好的支持,只需要繼承Play提供的play.db.jpa.Model類:
@Entity public class Product extends Model {public String name;public Integer price; }接著就可以執行Product實例中CRUD操作進行對象持久化:
Product.find("price > ?", 50).fetch(); Product product = Product.findById(2L); product.save(); product.delete();補充:ActiveRecord模式
ActiveRecord也屬于ORM層,由Rails最早提出,遵循標準的ORM模型:表映射到記錄,記錄映射到對象,字段映射到對象屬性。
配合遵循的命名和配置慣例,能夠很大程度的快速實現模型的操作,而且簡潔易懂。
Play也提倡使用ActiveRecord模式進行快速開發,其主要思想是:
7.4 無狀態模型
Play被設計成為“無共享”的架構,目的就是為了保持應用的完全無狀態化。
這樣做的好處在于可以讓一個應用同一時刻在多個服務器節點上運行。
Play為了保持模型無狀態化,需要避免一些常見的陷阱,最重要的就是不要因為多請求而將對象保存到Java堆中。
Play應用中多請求之間保存數據有以下幾種解決方案:
1.如果數據很小而且非常簡單,那么可以將其存儲在Session或者Flash作用域,
但是這些作用域最大只允許存放4K的內容,并且存儲的數據只能為字符串類型。
2.將數據保存到持久化存儲中(數據庫或者文件系統)。
比如用戶創建了需要跨越多個請求的對象,就可以按照以下步驟對其進行操作:
-
在第一次請求時初始化對象并將它保存到數據庫中。
-
將創建的對象的id保存在Flash作用域中。
-
在以后不停的請求鏈執行過程中,使用id從數據庫中獲取對象,更新并重新保存它。
3.將數據保存在瞬時存儲中(比如Cache):
-
在第一次請求時初始化對象并將它保存在緩存中。
-
將創建的對象的id保存在Flash作用域中。
-
在請求鏈的執行過程中,從Cache里獲取對象(通過保存在Flash作用域中的對象id),更新后并將它再次保存回Cache。
-
當請求鏈結束后,將對象進行持久化操作(數據庫或者文件系統)。
根據具體應用的需求,第三種解決方案使用緩存可以是一種非常好的選擇,也是Java Servlet Session的良好的替代方案。
但緩存并不是可靠的數據存儲方式,因此如果選擇將對象保存到緩存中,就必須確保能夠將它重新讀取回來。
8 play框架08
? play框架08–Job異步處理
8.1 Job實現
在Play中建立Job只需要繼承play.jobs.Job類:
package jobs; import play.jobs.*; public class MyJob extends Job {public void doJob() {// 執行一些業務邏輯} }如果希望創建具有返回值的Job,那么需要覆蓋doJobWithResult()方法:
package jobs; import play.jobs.*; public class MyJob extends Job<String> {public String doJobWithResult() {// 執行一些業務邏輯return result;} }上例自定義的Job覆蓋了doJobWithResult()方法,并且方法的返回類型為String,事實上Job可以返回任何類型的值。
8.2 Bootstrap Job
8.2.1 應用啟動
Bootstrap Job,顧名思義就是在應用開始時運行的任務,
只需要添加@OnApplicationStart注解就可以把當前Job設置為Bootstrap Job:
import play.jobs.*;@OnApplicationStart public class Bootstrap extends Job {public void doJob() {if(Page.count() == 0) {new Page("root").save();Logger.info("A root page has been created.");}}}需要注意的是,Bootstrap Job不需要任何返回值。
如果有多個帶有@OnApplicationStart注解的Bootstrap Job,那么默認情況下這些Job會按照定義的先后順序執行。
當所有的Bootstrap Job執行完成之后,Web應用就處于等待階段,等待處理那些即將到來的請求。
如果希望Web應用啟動后,能夠在執行Bootstrap Job的同時,又能很快地處理到來的請求,
可以為@OnApplicationStart注解添加async=true屬性:@OnApplicationStart(async=true)。
這樣應用程序開啟后,Bootstrap Job就會作為后臺程序異步執行了。
不僅如此,所有的異步Job(async=true)也會在Web應用開啟之后同時運行。
注意:
Play具有兩種不同的工作模式:開發模式(DEV)和產品模式(PROD),因此Job的啟動時間也有略微差異。
在DEV模式下,直到第一個HTTP請求到達時才會開啟應用,且不會預先編譯Java文件。
如果在該模式下更改Java源文件可以立即生效,刷新瀏覽器即可查看修改后的結果。
此外,應用還會在需要的時候自動重啟;而在PROD模式下,應用會在服務器啟動時同步開啟,一旦應用開啟就會自動編譯所有的Java文件,之后不再重載任何文件(包括模板文件和配置文件),所以如果有文件修改必須重啟應用才能生效。
所以DEV模式下Job會延遲啟動。
8.2.2 應用停止
Web應用停止或關閉的時候,常常也需要進行一些額外的操作,如進行數據的清理、日志的打印等。
如果開發者需要這類任務調度操作,可以使用Play提供的@OnApplicationStop注解。
import play.jobs.*;@OnApplicationStop public class Bootstrap extends Job {public void doJob() {Fixture.deleteAll();} }用法非常簡單,繼承Job類之后,重寫doJob()方法即可。
8.3 Scheduled Job
Scheduled Job是指可以被框架周期性執行的任務,可以使用@Every注解指定時間間隔控制Scheduled Job運行,
例如:
import play.jobs.*;@Every("1h") public class Bootstrap extends Job {public void doJob() {List<User> newUsers = User.find("newAccount = true").fetch();for(User user : newUsers) {Notifier.sayWelcome(user);}}}在實際開發中,@Every注解并不能夠完全滿足開發需求,比如有時候需要指定Scheduled Job在具體的某個時間點執行。
這時候可以使用@On注解指定時間點來執行Job,例如:
import play.jobs.*;/** Fire at 12pm (noon) every day **/ @On("0 0 12 * * ?") public class Bootstrap extends Job {public void doJob() {Logger.info("Maintenance job ...");...}}與Bootstrap Job一樣,Scheduled Job也是不需要任何返回值的,即使返回了也會丟失。
補充:
@On標簽中使用的是Quartz庫的CRON表達式。CRON表達式是由7個子表達式組成的字符串,每個子表達式都描述了單獨的日程細節。
這些子表達式用空格分隔,分別表示:
Seconds 秒 Minutes 分鐘 Hours 小時 Day-of-Month 一個月中的某一天 Month 月 Day-of-Week 一周中的某一天 Year 年(可選) 具體CRON表達式的例子:"0 0 12 ? * WED",表示“每周三的中午12:00”。8.4 Job的直接調用
Play的Job除了被框架自動調用外,也可以通過now()方法手動調用Job對象的實例,隨時觸發Job來執行指定任務。
使用now()方法調用Job后,任務會立即執行:
public static void encodeVideo(Long videoId) {new VideoEncoder(videoId).now();renderText("Encoding started"); }now()方法的返回值是Promise對象,通過該值可以在結束后獲得Job的執行結果。
9 play框架的請求處理流程
? Play框架的請求處理流程
Play框架使用事件驅動模型,以提供在不可預知的使用環境下的靈活的處理能力。
在一個web應用中,事件主要指用戶向服務器發起一次HTTP請求。對于Play框架,此類事件定義在routes文件中,play根據routes文件的內容以及用戶的請求,確定應該調用哪些過程。Play框架使用了Netty服務器,該服務器使用管道(pipeline),提供了在高并發情況下的優秀的異步處理能力。
當服務器接收到一個用戶請求的時候,將獲取一個管道,將請求相關信息傳入,之后交由Play框架處理。Play框架會根據該請求的內容,查找相應的路由規則,根據路由規則調用相應的事件處理流程,并(一般來說會)最終向用戶返回結果,完成一次事件處理:
? 圖1. 事件流向
用戶請求處理流程相關類
作為一個web應用框架,Play框架的最基本的功能就是響應用戶請求。
在本小節中,將概要講述當一個用戶請求(request)到來時,play將啟動怎樣的流程來對該請求進行處理,并最終返回相應給用戶。
本小節的重點在于闡明流程,由于這一流程涉及到M-V-C三個方面,對于其具體實現細節,將在后文敘述。
相關類介紹
在介紹處理流程之前,需要介紹在這里流程中涉及到的一些類。本小節將重點介紹這些類的類結構,以及它們在這個流程中發揮的主要作用。
9.1 PlayHandler
PlayHandler繼承了org.jboss.netty.channel.SimpleChannelUpstreamHandler,用于在管道中處理服務器監聽到的用戶請求。
類圖如下:
圖2 PlayHandler類圖其中比較重要的就是
messageReceived(final ChannelHandlerContext ctx,final MessageEvent messageEvent)方法。該方法為對父類同名函數的重寫(Override)。父類提供這個函數,由子類提供各自的具體實現。
當有消息到來的時候,服務器調用handler類的messageReceived函數,利用多態,將執行不同的實現。
在HttpServerPipelineFactory.getPipeline()中,當每次需要獲得pipeline時,新建一個PlayHandler的實例,注冊到pipeline中。
因此,每次的請求都會對應一個新的PlayHandler實例,接著PlayHandler.messageReceiveed()方法被調用以處理用戶請求。PlayHandler.messageReceived()方法執行過程將在后文敘述。
9.2 Invoker與Invocation
Play框架采用了命令模式(Command pattern),用于在多線程多任務情況下調度任務,
命令模式的原型如下:
? 圖3 命令模式原型
在命令模式中,Client調用Invoker,往其中添加命令。
Command代表了一個命令,開發者繼承Command并實現一個具體的命令,提交給Invoker進行執行。
Invoker負責管理這些命令,在合適的時候執行它們,并提供處理結果。
命令模式把請求一個操作的對象與知道怎么執行一個操作的對象分割開
如此一來,開發者只需要關注命令的實現,而不需要關注何時、如何執行該命令。
在Play框架中,Invoker及其內部類Invocation實現了命令模式,Invocation相當于Command類,
由子類實現其execute方法(在這里表現為run()方法)。它們的類圖如下:
圖4 Invoker類圖 圖5 Invocation及DirectInvocation類圖在Invoker中,主要是invoke方法與invokeInThread方法。
前者使用(ScheduledThreadPoolExecutor) executor調度線程執行Invocation;
后者直接在當前線程中執行任務,當執行不成功時,等待一段時間之后重新執行。
Invoke方法還提供了另一個版本的重載函數,可以在等待一段時間之后再執行當前任務:invoke(final Invocation invocation, longmillis)。
關于java.util.concurrent.ScheduledThreadPoolExecutor:繼承自java.util.concurrent.ThreadPoolExecutor,用于在給定的延遲后執行命令,或者定期執行命令,當需要多個輔助線程,或者要求ThreadPoolExecutor具有額外的靈活性或功能時,此類要優于Timer。
Invoker.invoke(Invocation)的代碼如下,可以看到executor是如何調度Invocation的:
publicstatic Future<?> invoke(final Invocation invocation) {Monitor monitor = MonitorFactory.getMonitor("Invokerqueue size", "elmts.");monitor.add(executor.getQueue().size());invocation.waitInQueue = MonitorFactory.start("Waiting forexecution");returnexecutor.submit(invocation);} publicvoid run() {if (waitInQueue != null) {waitInQueue.stop();}try {preInit();if (init()) {before();execute();after();onSuccess();}} catch (Suspend e) {suspend(e);after();} catch (Throwable e) {onException(e);} finally {_finally();}}在Invocation中實現了模板方法模式(Template method pattern),定義了run()方法的執行步驟,
而將各個步驟的實現方式交由其子類實現,以此方式來完成命令模式中的自定義命令。
模板方法模式原型如下:
圖3-6 模板方法模式原型在Play框架的模板方法模式具體實現中,Invocation實現了Runnable接口,實現了run()方法,
代碼如下:
run()方法即為模板方法,在run()方法中,按順序調用了init(),before(),execute(),after(),onSuccess()等方法,
這些方法需要由Invocation的子類實現,從而實現不同的功能。通過這樣的設計,將執行過程分解為多個部分,
每個部分由子類實現,而各部分的執行順序由父類規定。
在下文中,將提到PlayHandler.NettyInvocation,即為Invocation的一個子類。
9.3 Result類
Result繼承自RuntimeException,封裝了視圖層渲染結果(可能是HTML文件,也可能是XML文件或者二進制文件等),
類繼承關系如下:
圖3-7 Result類繼承結構圖FastRunTimeException是在Play框架中定義的一個可以快速實例化并拋異常類(Exception),
主要是修改了fillInStackTrace()方法,使其直接返回null,從而實現快速實例化:
public Throwable fillInStackTrace() {returnnull;}在Result中,提供了apply()方法,該方法需要其子類重寫,實現將相應的內容輸出的功能。
在Result之下,play提供了多個Result的子類,用于執行對不同的相應的操作,其中比較常見的是RenderTemplate類,實現了對模板文件進行最后輸出的功能。
將Result作為一個Exception并以try/catch的形式來捕獲Result,而不是用返回值的方式,這是Play框架比較奇特的一點。
這不符合通常對“異常(Exception)”的看法——一般來說,只有程序出現不可預知的情況的時候,才會使用try/catch代碼塊來捕獲Exception。然而,將渲染結果當做異常拋出并捕捉,將簡化代碼,由Java自身決定執行過程,提高了開發者的開發效率;
另外,在處理用戶請求過程中,多處地方可能直接返回結果(如直接返回404頁面),
框架在處理過程中將所有的處理結果歸到同一個地方并統一作判斷和處理,
如果將結果作為返回值,則需要更繁復的方法來對結果進行收集和處理。
9.4 Template類
Template類提供了對模板文件的封裝,并實現了對模板文件的編譯與執行。
類繼承關系如下:
圖3-8 Template類繼承結構圖最終實際使用的是GroovyTemplate類,該類代表一個模板文件,
提供對頁面進行渲染(將template文件轉換為html)的方法(render(Map<String, Object>))。
在前面討論的RenderTemplate中,在構造函數中傳入一個(Template)template(即一個RenderTemplate“擁有”一個Template實例),
并執行
this.content = template.render(args);調用Template.render(args)將渲染結果保存在content中。
在一個請求處理流程中,依次會經歷PlayHandler->Invoker->Invocation->Result->Template
10 play框架的攔截器
? Play框架的攔截器
在控制器里,定義攔截器方法。攔截器將被控制器類及后代的所有action調用。
這些方法必須是static的,但不能是public的,并使用有效的攔截注釋。
@Before
用@Before注釋的方法將在控制器的每個action前被調用執行
public class weixinIntercept extends Controller{@Before(unless="login") //""中寫的是方法的具體位置,例:wechat.account.wechatInformationstatic void check(){if(session.get("user") == null)login();} } /*可以使用unless和only,還可以使用@After,@Before,@Finally注釋 */控制器繼承
如果一個控制器類是其他控制器類的子類,那么會按照繼承順序應用于相應的子類。
使用@With注釋添加更多的攔截器
public class Security extends Controller{@Beforeprotected static void checkAuthentic(){if(!session.containsKey("user"){unAuthen();}} }//另一個控制器 @With(Security.class) public class Admin extends Controller{...... }11 play源碼分析
==Play源代碼分析1—Server啟動過程==? Play是個Rails風格的Java Web框架,需要了解背景請看:
如何調試請看此處。以下進入正題_
Server啟動過程主要涉及三個地方:
11.1總體流程
Server.main為入口方法:
public static void main(String[] args) throws Exception {…Play.init(root, System.getProperty("play.id", ""));if (System.getProperty("precompile") == null) {new Server();} else {Logger.info("Done.");} }做兩件事:
Play.init
public static void init(File root, String id) {…readConfiguration();Play.classes = new ApplicationClasses();…// Build basic java source pathVirtualFile appRoot = VirtualFile.open(applicationPath);roots.add(appRoot);javaPath = new ArrayList<VirtualFile>(2);javaPath.add(appRoot.child("app"));javaPath.add(appRoot.child("conf"));// Build basic templates pathtemplatesPath = new ArrayList<VirtualFile>(2);templatesPath.add(appRoot.child("app/views"));// Main route fileroutes = appRoot.child("conf/routes");…// Load modulesloadModules();…// Enable a first classloaderclassloader = new ApplicationClassloader();// PluginsloadPlugins();// Done !if (mode == Mode.PROD ||preCompile() ) {start();}… }主要做:
關鍵步驟為new ApplicationClasses(),執行computeCodeHashe(),后者觸發目錄掃描,搜索.java文件。
相關過程簡化代碼如下:
public ApplicationClassloader() { super(ApplicationClassloader.class.getClassLoader()); // Clean the existing classes for (ApplicationClass applicationClass : Play.classes.all()) {applicationClass.uncompile(); } pathHash = computePathHash();… }int computePathHash() { StringBuffer buf = new StringBuffer(); for (VirtualFile virtualFile : Play.javaPath) {scan(buf, virtualFile); } return buf.toString().hashCode(); }void scan(StringBuffer buf, VirtualFile current) {if (!current.isDirectory()) {if (current.getName().endsWith(".java")) {Matcher matcher = Pattern.compile("\\s+class\\s([a-zA-Z0-9_]+)\\s+").matcher(current.contentAsString());buf.append(current.getName());buf.append("(");while (matcher.find()) {buf.append(matcher.group(1));buf.append(",");}buf.append(")");}} else if (!current.getName().startsWith(".")) {for (VirtualFile virtualFile : current.list()) {scan(buf, virtualFile);}} }11.2 Start流程
簡化代碼如下:
public static synchronized void start() { try { ... // Reload configuration readConfiguration();...// Try to load all classes Play.classloader.getAllClasses();// Routes Router.detectChanges(ctxPath);// Cache Cache.init();// Plugins for (PlayPlugin plugin : plugins) {try {plugin.onApplicationStart();} catch(Exception e) {if(Play.mode.isProd()) {Logger.error(e, "Can't start in PROD mode with errors");}if(e instanceof RuntimeException) {throw (RuntimeException)e;}throw new UnexpectedException(e);} }...// Plugins for (PlayPlugin plugin : plugins) {plugin.afterApplicationStart(); }} catch (PlayException e) {started = false;throw e; } catch (Exception e) {started = false;throw new UnexpectedException(e); } }關鍵步驟為執行Play.classloader.getAllClasses()加載app目錄中的類型。
簡化代碼如下:
public List<Class> getAllClasses() { if (allClasses == null) { allClasses = new ArrayList<Class>();if (Play.usePrecompiled) { ... } else { List<ApplicationClass> all = new ArrayList<ApplicationClass>();// Let's plugins play for (PlayPlugin plugin : Play.plugins) { plugin.compileAll(all); }for (VirtualFile virtualFile : Play.javaPath) { all.addAll(getAllClasses(virtualFile)); } List<String> classNames = new ArrayList<String>(); for (int i = 0; i < all.size(); i++) { if (all.get(i) != null && !all.get(i).compiled) { classNames.add(all.get(i).name); } }Play.classes.compiler.compile(classNames.toArray(new String[classNames.size()]));for (ApplicationClass applicationClass : Play.classes.all()) { Class clazz = loadApplicationClass(applicationClass.name); if (clazz != null) { allClasses.add(clazz); } } ... } } return allClasses; }主要步驟:
**到此完成.java的加載。**相關對象關系如下圖:
11.3 啟動HTTP服務
? 接著new Server()啟動HTTP服務,監聽請求
簡化代碼如下:
public Server() { ... if (httpPort == -1 && httpsPort == -1) {httpPort = 9000; } ... InetAddress address = null; try {if (p.getProperty("http.address") != null) {address = InetAddress.getByName(p.getProperty("http.address"));} else if (System.getProperties().containsKey("http.address")) {address = InetAddress.getByName(System.getProperty("http.address"));}} catch (Exception e) {Logger.error(e, "Could not understand http.address");System.exit(-1); }ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool()) ); try { if (httpPort != -1) {bootstrap.setPipelineFactory(new HttpServerPipelineFactory());bootstrap.bind(new InetSocketAddress(address, httpPort));bootstrap.setOption("child.tcpNoDelay", true);if (Play.mode == Mode.DEV) {if (address == null) {Logger.info("Listening for HTTP on port %s (Waiting a first request to start) ...", httpPort);} else {Logger.info("Listening for HTTP at %2$s:%1$s (Waiting a first request to start) ...", httpPort, address);} } else {if (address == null) {Logger.info("Listening for HTTP on port %s ...", httpPort);} else {Logger.info("Listening for HTTP at %2$s:%1$s ...", httpPort, address);} }}} catch (ChannelException e) {Logger.error("Could not bind on port " + httpPort, e);System.exit(-1); } ... }主要步驟:
總結
以上是生活随笔為你收集整理的26Play框架教程2学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 时间计算器
- 下一篇: Google镜像代理地址: