1. 程式人生 > >給Java開發者的Play Framework(2.4)介紹 Part1:Play的優缺點以及適用場景

給Java開發者的Play Framework(2.4)介紹 Part1:Play的優缺點以及適用場景

1. 關於這篇系列

這篇系列不是Play框架的Hello World,因為這樣的文章網上已經有很多。

這篇系列會首先結合實際程式碼介紹Play的特點以及適用場景,然後會有幾篇文章介紹Play與Spring,JPA(Hibernate)的整合,以及一些Play應用的最佳實踐, 這期間會在Github上提供一個腳手架專案,方便感興趣的朋友直接動手嘗試。最後會簡單分析Play的部分原始碼,幫助大家理解黑盒子的內部機制。

我水平有限,有錯誤歡迎指出。

2. Play介紹

Play Framework是一個開源的Web框架,背後商業公司是Typesafe。要介紹Play之前,首先理清Play的兩個不同的分支。 Play 1.x 使用Java開發,最新版本是1.3.1,只支援Java專案。從11年開始就進入了維護階段,新專案一般不考慮使用Play1。 Play 2.x 使用Scala和Java開發,同時支援Java和Scala專案。 這裡主要介紹最新的Play2.4 for Java。有一點需要提前說明,雖然Play2主要由Scala開發,但是對於專案中的一般開發人員而言, 使用Play可以完全不懂Scala,具體情況後面會說明。

3. 為什麼要了解Play

現在的Web框架或者類庫可以說是浩如煙海。近十年來,在Web開發領域,JVM陣營的佔有率一直不高, 資料來源(http://hotframeworks.com/#rankings)
這是國外開源專案的資料,相對來說國內Java框架的使用率會高一些。而最近幾年,Ruby和Python在國內的開發群體也在不斷壯大。 Java框架在Web領域不那麼受歡迎,主要原因在於開發速度遠落後於其他的開發框架。對於初創公司而言,快速開發出產品投入市場試錯比花半年打磨出一款功能效能齊備的 應用更加重要,而對於成熟產品,也需要快速響應頻繁的需求變化,這方面動態語言又更勝一籌。所以說到Web後端框架的技術選型,除非技術團隊有比較深的JVM背景, 否則會傾向於選擇RoR,Django這些框架。

JVM陣營在Web領域逐漸落後主要有三個原因:編譯的鍋,技術棧的鍋和語言的鍋。

大家都知道Java原始碼需要編譯之後才能執行,直接結果是每次修改原始碼都需要重啟Web伺服器才能看到效果。如果專案比較小類也少,重啟時間還勉強能接受。 我以前參與的一個專案,使用的是WebLogic伺服器,Spring容器裡大概有上千個Bean,重啟一次至少得花5分鐘,還是優化後的結果。工作時間至少有20%花在重啟上了。 雖然現在有JRebel之類的熱載入技術,但是國內使用的相對較少。

Servlet規範在1997年出現,在當時可以說是很先進的技術,加上Tomcat的橫空出世,直接促成了JSP的崛起。然而時過境遷,Servlet風光不再, Web容器存在的必要性也

被越來越多的人質疑。原因就在於人為的將應用與容器剝離, 雖然這種做法本意是好的,但是結果就是給開發測試部署帶來一系列整合的問題,現在越來越多的專案開始使用內嵌的Jetty或Tomcat就是一個現實的例子。 Servlet還帶來一個問題,就是有狀態的伺服器。一旦使用了Session,伺服器就無法享受到水平擴充套件的好處了,由此不得不採用Session複製或者粘性Session(Sticky Session)的 方案來解決這個問題,無論採取哪種方案都會有效能損耗,並且推高了技術成本。Servlet說到底是Java EE家族的一員,由於Sun的領導(Oracle背鍋), 從Java EE 5開始,Java EE的角色已經從技術創新者轉換為跟隨者,這些年基本上可以說是跟著開源社群的步子在走的,除了政府大單和跨國企業,你很難再看見它的身影了。

至於語言,其實從JDK8開始,Java已經很好用了。不過從JDK5到JDK8,十年太長,尤其是在Web。

之前Java陣營受累於沒有成熟的快速開發框架,Spring熱衷於提供各種整合方案,可是配置和使用還是相當的麻煩,直到Spring Boot的出現才有改善。 不過近幾年出現了一些相當優秀的框架,如DropwizardPlayVert.x。 這篇系列要介紹的Play,通過ClassLoader在原始碼修改的時候動態載入類,解決了修改程式碼需要重啟伺服器的問題,完全拋棄了Servlet技術棧,基於Netty實現了自己的 請求響應介面(Request/Result),基於Play的應用就是無狀態的,另外Play處理請求的方式是無阻塞的(Non-Blocking)。Play2在設計的時候借鑑了RoR的許多優點, 學習Play能夠讓你瞭解一些現代化框架的特點,同時能夠為你開啟非同步程式設計世界的大門。Promise已經被Scala,JavaScript等語言大量使用,Actor模型也已經遍地開花, 這些你都可以直接在Play中使用,或者你想保持原來的程式設計風格也完全沒有問題。

4. Play的特性

1. Play2的模板引擎

Play2的模板是很強大並且容易上手的. 相對於Java領域其他模板引擎(Freemarker, Velocity, JSP, Groovy, etc), 主要有三個特點.
1) 簡單易上手, 沒有JSP裡面繁雜的內建物件和指令, 所有功能都通過方法呼叫完成.
2) 主流IDE中都支援Play模板的靜態型別檢查, 類似JSP.
3) 支援反向路由.
舉個例子, 一般系統都會有一個固定的頁面佈局, 比如分出頁頭頁尾。如果用JSP或者Velocity之類的模板, 一般都是通過sitemesh+filter或者在每個頁面include來完成佈局。使用Play模板, 完成這個功能非常容易。 首先定義一個main頁面 main.scala.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@(title: String = "預設標題")(staticFile: Html = Html(""))(content: Html)
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
    <meta charset="utf-8" />
    <title>@title</title>
</head>
<body>
@header()  <-- 頁頭 -->
@navigator() <!-- 導航 -->
@content
<script src="@routes.Assets.versioned("js/jquery-1.11.2.min.js")"></script>
@staticFile
1
@(title: String = "預設標題")(staticFile: Html = Html(""))(content: Html)

這一部分是引數宣告,這裡聲明瞭三個引數:title標題, 有預設值;staticFile為html程式碼塊, 可以傳js等;content為頁面內容。

1
2
3
@header()  <-- 頁頭 -->
@navigator() <!-- 導航 -->

這一部分是引用同目錄下的另外兩個頁面:header.scala.html和navigator.scala.html。為什麼能這樣引用,因為這些頁面(main,header,navigator)都會被自動 編譯成一個方法(準確地說是一個Scala object,不過這裡先當做方法),所以這裡相當於方法呼叫。同樣,這個main也會被編譯成方法,其他頁面可以呼叫main來完成佈局, 例如 login.scala.html

1
2
3
4
5
6
7
8
9
@main() {
    <script type="text/javascript">
        FG.user.login();
    </script>
} {
    <div class="login width1200">
    <!-- login -->
    </div>
}

這就是一個簡單的登入頁面。登入頁面呼叫main頁面的方法,第一個引數不傳使用預設標題,第二個引數傳入登入頁面的js程式碼,第三個引數傳入登入頁面的html程式碼。 這樣就完成了頁面佈局, 沒有隨處可見的include, 也沒有暗箱操作的filter, 所有的一切都是方法呼叫, 是不是很簡單清晰?

靜態型別檢查就不說了, 本來Java的一大優點(Que Dian)就是型別檢查,所以在Java裡用Freemarker或者Velocity這種模板的做法值得商榷。

反向路由的意思是, 在Play中, 所有的Controller url都配置在一個routes檔案中, 例如

1
GET         /register                           @controllers.user.LoginController.registerPage

之後無論是在Controller裡還是模板中, 都不用硬編碼url。而是使用routes檔案。例如在Controller中使用redirect(routes.LoginController.registerPage())就能實現重定向。 而在模板中使用 <a href="@controllers.routes.LoginController.registerPage()">來指向連結。這種風格就是REST裡的URI模板。

2. 熱部署

這個上面介紹過,不用重啟伺服器。

3. 內建dev/prod環境,內建部署指令碼

平常開發的時候使用run啟動Play,是跑在dev模式。 Play會定時掃描原始碼目錄進行熱更新,並且類都是訪問的時候再載入,提高啟動速度。 使用start啟動專案就執行在prod模式。Play內建dist命令,可以把所有的檔案打包成一個zip,解壓之後直接執行bin目錄下的可執行檔案即可啟動專案,除了JDK之外無須任何其他外部依賴。 這大大減輕了運維成本,同時也能夠很方便的進行持續整合(CI)。

4. 使用Play開發的Server大部分能做到Stateless

這個之前也說過,Play拋棄了Servlet/JSP裡Session等概念, 內建沒有提供方法將物件與伺服器例項進行繫結(你要使用HashMap存的話Play也沒辦法)。 推薦的做法是使用外部快取, 比如Redis, Memcached等。可能有人會覺得沒有Session是Play的一個缺點(Play裡的Session和Servlet Session不是一回事), 但是隻要你開發過流量大一點的應用, 你就會理解這點。

5. 好用的配置庫

如果你之前開發過Java專案, 肯定寫過**.properties或者管理過一大堆的xml。Java內建庫對properties檔案的處理是很弱的,你不得不自己寫一些工具類去進行處理, 而且properties檔案還不支援更復雜的語法。Play使用Typesafe Config庫,配置檔案使用HOCON格式,預設配置檔案為application.conf。 你能很容易讀取裡面的配置, 並且你也可以把自己的配置寫在裡面。所以專案中基本不需要使用properties或者xml檔案了,除了第三方庫需要的。

6. Play外掛

RoR框架之所以好用,主要原因之一就是圍繞RoR有相當豐富的外掛可供選擇,很多業務功能甚至都不需要開發就能實現。Play的外掛數量當然相對於RoR還是要少一些, 不過你遇到的需求基本都有現成的外掛可以使用。比如發郵件, 授權和驗證, sitemap生成,第三方登入等等。自己寫一個外掛也很簡單。

7. 優秀的測試支援

因為Play誕生的時候TDD已經很火熱,所以Play對測試的支援非常好。 例如下面的幾行程式碼就能對Controller進行測試。

1
2
3
Http.RequestBuilder request = new Http.RequestBuilder().method(POST).uri(routes.LoginController.requestPhoneCode(phone).url());
Result result = route(request);
assertThat(result.status(), is(OK));

Play還內建了對 Selenium WebDriver的支援,可以模擬瀏覽器進行測試。以下是官方的例子:

1
2
3
4
5
6
7
8
9
public class BrowserFunctionalTest extends WithBrowser {
    @Test
    public void runInBrowser() {
        browser.goTo("/");
        assertNotNull(browser.$("title").getText());
    }
}

8. 優秀的REST支援

Play2從誕生起就能很容易的支援RESTful風格的架構(因為Play2在設計的時候REST就已經大行其道), 在Play2中實現RESTful API的示例可以參考Stackoverflow上的這個回答

5. 使用Play過程中遇到的坑

1. 首次編譯速度過慢

這是Scala的鍋。Scala在編譯過程中要經歷至少30個步驟, 導致編譯速度相當慢。在我的機器上(Core™ i5-4590 CPU @ 3.30GHz,RAM 8GB),編譯100多個Scala類大約需要1到2分鐘。好在sbt可以增量編譯, 即首次編譯之後,你再修改程式碼,編譯器只會編譯那些它認為需要編譯的類,編譯幾個類的時候速度很快,基本重新整理頁面就能完成。

2. IDE的Scala外掛偶爾會誤報錯誤

首先得說明,最適合開發Play專案的IDE是IntelliJ IDEA。現在IDEA最新的Scala外掛相比之前的版本,已經有很大的提升。 不過偶爾還是會出現誤報的情況,這個問題隨著新版本外掛的釋出應該會慢慢解決。

3. Scala和Sbt的學習成本較高

這可能是初次接觸Play的使用者遇到的最大障礙。其實對於大多數業務開發人員來說,這不是問題。使用Play for Java版本,專案程式碼99%都是Java程式碼, 而Sbt類似於Maven,一旦專案搭建好後不需要過多接觸,只要學會幾個常用的命令就可以了,例如project root(切換專案), run(啟動伺服器在dev模式)。 我們團隊大部分成員之前都沒有接觸過Scala和Play,經過一兩週的磨合期之後都能很順利的使用Play進行開發了。

4. Play的API變化速度比較快

Play的版本號遵循Semantic Versioning,不同主版本的API變化非常大,比如Play1和Play2就是兩個不同的框架。 而副版本之間API也會有一些變化,而且不一定完全向後相容。例如使用Play2.3.x的專案在升級到2.4的時候,需要按照官方提供的遷移手冊進行程式碼修改, 不然是執行不了的。這對於其他背景的開發者來說可能比較容易理解,但是如果是一直習慣於使用Spring MVC或Struts2的話,可能會對這點感到不適。

6.總結

Play和Spring MVC的定位有些相似,但是比Spring MVC提供更豐富的功能,和Web有關的專案都可以使用Play。但是如果要用好Play,對團隊有一定的要求。

首先,你的團隊應該不是墨守成規的團隊。大部分人都害怕變化,這是不爭的事實。JDK的發展緩慢加上國內的技術氛圍,著實讓Java開發人員過了幾年的舒服日子。 你如果是05年學會了ibatis和Spring,然後這十年去環遊世界了,在15年你照樣能輕鬆找到一份待遇還算可以的工作。然而事情已經開始發生變化,不會學習可能會被淘汰。

其次,你的團隊應該重視工作效率和質量,並且有時間做出改進。國內很多團隊信奉的是人海戰術。以低薪聘請大量不合格的開發人員來開發業務功能, 而不是注重單人的工作效率和質量,很多專案的加班和延期都源於此。這樣的團隊就不適合用Play。很難想象每天都要加班去應付工作的團隊有時間打磨升級自己的工具和技能。 但是反過來低效率的工具和技能又拖累了自己的工作效率,這是一個惡性迴圈。

最後,團隊中需要有人對Scala和Sbt有一定的瞭解。雖然Play有Java版本可以使用,但是如果不會Scala和Sbt,在搭建環境,使用一些高階功能(如Filter)的時候可能會遇到麻煩。

下篇我會介紹Play和Spring還有JPA(Hibernate)的整合,畢竟Spring在大部分Java專案還是主流。有問題和建議歡迎指出。