Play框架使用事件驅動模型,以提供在不可預知的使用環境下的靈活的處理能力。
在一個web應用中,事件主要指用戶向服務器發起一次HTTP請求。對於Play框架,此類事件定義在routes文件中,play根據routes文件的內容以及用戶的請求,確定應該調用哪些過程。Play框架使用了Netty服務器,該服務器使用管道(pipeline),提供了在高併發情況下的優秀的異步處理能力。
當服務器接收到一個用戶請求的時候,將獲取一個管道,將請求相關信息傳入,之後交由Play框架處理。Play框架會根據該請求的內容,查找相應的路由規則,根據路由規則調用相應的事件處理流程,並(一般來說會)最終向用戶返回結果,完成一次事件處理:
圖1. 事件流向
用戶請求處理流程相關類
作爲一個web應用框架,Play框架的最基本的功能就是響應用戶請求。在本小節中,將概要講述當一個用戶請求(request)到來時,play將啓動怎樣的流程來對該請求進行處理,並最終返回相應給用戶。本小節的重點在於闡明流程,由於這一流程涉及到M-V-C三個方面,對於其具體實現細節,將在後文敘述。
一、相關類介紹
在介紹處理流程之前,需要介紹在這裏流程中涉及到的一些類。本小節將重點介紹這些類的類結構,以及它們在這個流程中發揮的主要作用。
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()方法執行過程將在後文敘述。
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的一個子類。
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頁面),框架在處理過程中將所有的處理結果歸到同一個地方並統一作判斷和處理,如果將結果作爲返回值,則需要更繁複的方法來對結果進行收集和處理。
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