Play框架的请求处理流程1
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);
}
在Invocation中实现了模板方法模式(Template method pattern),定义了run()方法的执行步骤,而将各个步骤的实现方式交由其子类实现,以此方式来完成命令模式中的自定义命令。模板方法模式原型如下:
图3-6 模板方法模式原型
在Play框架的模板方法模式具体实现中,Invocation实现了Runnable接口,实现了run()方法,代码如下:
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();
}
}
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