本文由 Shaw 發表在 ScalaCool 團隊部落格。
Play! 是一種高效率的 Java 和 Scala Web 應用程式框架,它能夠用來開發「響應式」 Web 應用,同時它也整合了現代 Web 應用程式開發所需的元件和 API。本文將介紹一下 Play! 的基本性質以及利用該框架開發 Web 程式的優勢。
響應式 Web 開發框架
![Image of play](https://i.iter01.com/images/17282d64e87e1a2ab397f29adcbebd69163c9b874440a4806cef25d73d661ba1.png)
Play! 拋棄了傳統的 Java Web 框架的模式,而是選擇擁抱「響應式」(Reactive)應用的理念,從頭開始設計,這使得 Play! 可以夠構建出即使在高負載下也能夠對使用者行為進行實時響應的 Web 應用。 Play! 作為一個全棧「響應式框架」主要有如下特點:
響應式(Responsive)—— 在使用者層面,Play! 能夠快速響應使用者的行為
可伸縮(Scalable)—— 在負載層面,Play! 能實現良好的水平擴充套件
事件驅動(Event-driven)—— Play! 的 HTTP Server 就是基於事件模型實現的
接下來我們就 Play!的這幾個主要的特點進行介紹。
事件模型 Web 伺服器(Evented Web Application Server)
在 Play 2.6.x 之前,Play! 的 HTTP Server 是基於 Netty 實現的,最新版的 Play 2.6.x 是基於 Akka HTTP 實現的,在介紹 Play! 的 HTTP Server 的優勢之前,我們先看一下傳統的 Java Web 框架所採用的 HTTP Server 是怎樣的。
當前比較主流的實現 HTTP Server 的模型主要有兩類——「執行緒模型」以及「事件模型」。
執行緒模型伺服器(THREADED SERVERS)
傳統的 Java Web 框架所採用的伺服器就是基於執行緒模型來實現的,比如非常流行的 Apache Tomcat,該模型的工作方式如下圖所示:
![Image of Thread](https://i.iter01.com/images/6c4b533041a18f8330a779ddf767ac7af9a897bc9a008f6458001c33506ea1d1.png)
「接收者執行緒」(Accpter thread)接受客戶端的 HTTP 請求,然後將這些請求分配給「請求處理執行緒」進行處理。
這種模型的弊端就是,「工作執行緒」(也就是上面提到的「請求處理執行緒」)是有限的,而客戶端發來的請求數量往往會大於「工作執行緒」的數量。當此種情況發生時,那些沒有得到處理的執行緒就會一直處於阻塞和等待狀態,反映到使用者層面就是頁面遲遲得不到響應,如果等待時間過長,耐心的使用者最終會看到請求超時(Request timeout)的資訊,急性子的使用者就會關掉這個頁面。
另外採用執行緒這種方式也非常地耗費資源,如果某個請求很耗費時間,那麼處理該請求的工作執行緒大概是這樣工作的:
![Image of thread-handle](https://i.iter01.com/images/aaddd08e1c89f91e7fd22719a48cfd8952b55880f74afffca7f68c5f46320ed3.png)
其中綠色是程式執行時間,紅色是等待時間,可以看到由於 I/O 操作比較慢,所以這個執行緒的工作時間大部分都在等待,極大地消耗了資源,如果是採用多執行緒,那消耗的資源將會多倍增加。
事件模型伺服器(EVENTED SERVERS)
Play! 的 HTTP Server 是基於 Netty 或者 Akka HTTP 實現的,這兩個框架都具有非同步非阻塞的優點。我們先看一下 Play! 的事件模型伺服器是如何工作的:
![Image of Event](https://i.iter01.com/images/0cd82b52f6ad5328d4c208a6b1f7b13035539aaca985dd967a2367f5ae1f7e27.png)
我們知道,當使用者傳送一個請求的時候,往往這個請求包含了許多操作,而 Play! 則能將這些請求分割為一個一個的事件,然後非同步去處理這些事件。例如,當某一個事件正在被作業系統處理的時候,這個過程可能會花費一些時間,之前說過,如果執行緒一直等待這個事件執行完然後再去執行下一個事件就有點浪費資源了,所以在這個等待時間裡,event loop(訊息執行緒)可以去執行事件佇列中的其他事件。當某個事件執行完之後,就會發出一箇中斷,這個中斷也算一個事件,然後加入到事件佇列中,等待執行。這種非同步非阻塞的模式使得 Play! 能夠以較少的資源應對大流量的訪問。反映在使用者層面就是,Play! 能夠快速地對使用者的行為作出響應。
為了與「執行緒模型」進行對比,我們畫一個類似的圖來解釋為什麼「事件模型」消耗的資源更少而處理的請求更多:
![Image of Event handle](https://i.iter01.com/images/c855397b65e8f9a4ce2ce8ab8b26a168fbd20bfd390d228c83011435aa774558.png)
圖中綠色的部分為事件的執行時間,橙色部分為「空閒時間」,注意這裡是「空閒時間」而非前面所說的「等待時間」,在這個空閒時間內,event loop 可以去執行其他事件而不必等待前面某個事件執行完成,當某個事件執行完成之後,會發出中斷,這個中斷也會產生一個新的事件,最終 event loop 也會執行這個事件。這就是「事件模型」處理某個請求的流程,可以看到,沒有了等待時間,大大提高了程式執行的效率,也使得系統能夠以較少的資源處理大量的請求。
非同步非阻塞
Play! 通過重新設計並實現了自己 HTTP Server 這使得 Play! 能夠以「非同步」的方式去處理每一個請求。在利用 Play! 進行開發的時候,Play! 預設配置的 controller 就是非同步的,所以我們可以利用 Play! 很方便地寫出非同步非阻塞的程式碼。我們知道,在 Java8 之前,要編寫非同步非阻塞的程式碼往往需要使用回撥,但是當業務邏輯變得複雜,回撥變多的時候就會出現傳說中的 “回撥地獄”,這使得程式碼的可讀性極差。而 Scala 語言引入了 Future ,極大地簡化了多個回撥的處理,使程式碼看上去優雅很多(關於如何在 Play! 中利用Future實現非同步邏輯,我們將會在後面的文章中進一步的介紹)。所以在 Play! 中利用 scala 編寫非同步程式碼將會變得非常高效。
無狀態(Stateless)
Play! 框架拋棄了 Servlet/JSP 裡 Session 等概念,內建沒有提供方法將物件與伺服器例項進行繫結,在每次 HTTP Request 之間不會在 Server 端儲存狀態,所需的狀態都需要在 HTTP Request 之間傳遞,這樣做的好處就是使得應用在負載層面實現了良好的水平擴充套件,接下來我們分別介紹一些有狀態的部署方式與無狀態的部署方式。
有狀態部署
![Image of state](https://i.iter01.com/images/6dd340619539118ce768e67857968cebcb9da4f03254275072bf011852b1d416.png)
如果我們在 session 中儲存了大量與客戶端的「狀態資訊」的話,為了防止某臺機器當機而導致使用者與伺服器儲存的會話狀態丟失,我們需要在各個節點之間共享這些「狀態資訊」。比較常見的做法是採用叢集,比如採用 tomcat 或者 jboss 的叢集功能,採用此種方式並不能通過增加節點來解決系統負載過大的問題,因為隨著節點增加,各個節點之間 session 的通訊會增加,從而使系統開銷增大。所以採用有狀態的部署方式不能使系統具有良好的伸縮性。
無狀態部署
![Image of stateless](https://i.iter01.com/images/a4e592c1f0b8cfbe2d3baf80d768d3f37df69ff0e2983a04e6d91be037fd7627.png)
如圖所示,採用無狀態的部署方式,每個節點不儲存諸如 session 之類的狀態資訊,各個節點之間也沒有共享狀態,它們彼此都是獨立的。當系統的負載增加時,我們只需要增加一個節點,然後在前端通過均衡負載就可以使系統的效能提高。這樣就使得系統具有良好的伸縮性。當然,沒有了 session,那 Play! 如何來儲存狀態呢,我們可以使用 Play! 中基於 Cookie 的客戶端使用者會話以及「外部快取」(這些在之後的文章中會介紹)。
ROR風格
對於很多公司而言,快速地開發出一款產品並上線非常重要,由於 ROR (Ruby on Rails) 風格的框架在開發效率上面非常高,所以很多公司在快速構建應用時往往會選擇這類框架,而不是傳統的 Java 框架。
![Image of play and java layer](https://i.iter01.com/images/9fa50751d79fada8e9e19da6cdfc07ac6f2256c5637249800a85ebbd7994b6f8.png)
通過上圖可以對比一下 Play! 與傳統的 Java EE 框架的區別,可以看到 Play! 在架構上更加清晰簡潔。在 Play! 之前, 相比於 ROR 風格的框架,傳統的 Java Web 框架在開發網頁應用的時候往往耗時比較長,原因主要有兩個:
1、依賴 Servlet
傳統的 Java Web 框架都是基於 Servlet 來構建的,開發人員開發的應用也需要在 Servlet 容器中執行,但是這就帶來了一個後果,開發人員每次修改完程式碼之後,都需要重新啟動 Web 伺服器才能看到修改後的效果。如果某一個專案規模較小,那重啟以及編譯的時間還能接受,但是如果專案很大,那開發過程中所花的大部分時間都浪費在重啟以及編譯上面了。
而 Play! 框架通過 ClassLoader 在原始碼修改的時候動態載入類,解決了修改程式碼需要重啟伺服器的問題,使得開發效率變高。
2、 複雜的 XML 配置檔案
傳統的 Java Web 框架在開發某個 Web 應用的時候需要引入大量的 XML 配置檔案,這些檔案在配置起來比較麻煩,如果數量很多且分散在不同的檔案下面會使得維護成本增加。
Play! 框架深諳 ROR 之道,採用 約定優於配置,只有一個全域性的配置檔案 application.conf,其他大部分配置都是預設的,我們只需要按照它約定的去做好了。
RESTFul
傳統的 Java Web 框架利用 Servlet 將 Http協議隱藏了起來,也就是說開發者不能很直觀地看到某一個請求對應的某個操作。而 Play! 在設計上擁抱了 Http 協議,比如我們要獲取一個使用者列表,我們就可以在 route 檔案中這樣寫:
GET /customer/list controllers.CustomerController.list複製程式碼
那麼 /customer/list 這個 URL 對應的就是 CustomerController 中的 list 方法。
這樣看上去更加直觀。
強型別模板
從 Play! 2 開始, Play! 的模板就全面擁抱了 Scala,所以 Play! 的模板都是可以編譯的 Scala 函式,這就意味著我們可以在編譯的時候直接在瀏覽器或者控制檯中看到模板的錯誤資訊,而不用等到將應用部署,呼叫頁面之後才能發現錯誤。