1 play!的内置服务器
其内置服务器是采用了netty编写,所以开发阶段play无须借助第三方的server,如tomcat;需要注意的是,部署项目的时候,这个server就没啥用的,因为项目最终是需要放到我们的tomcat下的。
但是看看play的项目目录:
有app/test/public/conf,app下还会分为controllers、views。
我们知道,tomcat是要去项目下的WEB_INF下读取类和web.xml的;然而play并没有这些,所以,如果要把play不数字啊tomcat下运行,只能先“打包”了,把项目打包成符合tomcat的目录结构才行啊!
来看看打包后的web.xml吧:
<?xml version="1.0" ?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Play! </display-name>
<context-param>
<param-name>play.id</param-name>
<param-value>war</param-value>
</context-param>
<listener>
<listener-class>play.server.ServletWrapper</listener-class>
</listener>
<servlet>
<servlet-name>play</servlet-name>
<servlet-class>play.server.ServletWrapper</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>play</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Play实现了一个ServletWrapper,就是把所有的请求让这个servlet来接收即可。
2 一个请求的处理流程
继续上面的ServletWrapper看下去。ServletWrapper是一个servlet
/**
* Servlet implementation.
* Thanks to Lee Breisacher.
*/
public class ServletWrapper extends HttpServlet implements ServletContextListener {
...
}
Play!号称是抛弃了原有的servlet框架,但是无奈啊,servlet才是王道啊,最终部署的时候还是要自己去实现一个servlet。
Play请求的入口就这一个,所以,在ServletWrapper的service()中,我们可以看到play处理一个完整请求的框架。
它的实现大致上是这样的:
@Override
protected void service(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
...
Request request = null;
try {
Response response = new Response();
response.out = new ByteArrayOutputStream();
Response.current.set(response);
request = parseRequest(httpServletRequest);
...
Invoker.invokeInThread(new ServletInvocation(request, response, httpServletRequest, httpServletResponse));
} catch (NotFound e) {
if (Logger.isTraceEnabled()) {
Logger.trace("ServletWrapper>service, NotFound: " + e);
}
serve404(httpServletRequest, httpServletResponse, e);
return;
} catch (RenderStatic e) {
if (Logger.isTraceEnabled()) {
Logger.trace("ServletWrapper>service, RenderStatic: " + e);
}
serveStatic(httpServletResponse, httpServletRequest, e);
return;
} catch(URISyntaxException e) {
serve404(httpServletRequest, httpServletResponse, new NotFound(e.toString()));
return;
} catch (Throwable e) {
throw new ServletException(e);
} finally {
Request.current.remove();
Response.current.remove();
Scope.Session.current.remove();
Scope.Params.current.remove();
Scope.Flash.current.remove();
Scope.RenderArgs.current.remove();
Scope.RouteArgs.current.remove();
CachedBoundActionMethodArgs.clear();
}
}
看起来并没有很复杂的样子。简单说来,做了以下几件事情:
- 将请求封装成Request对象;
- Invoker执行具体的业务逻辑;
- catch相关异常;
- 清理资源;
3 请求的封装
和springMVC/Struts2框架一样,控制层首先要对所有的请求进行一次封装。
根据上述的分析,Play在ServletWrapper的service()中对请求进行了封装,封装的结果是Request。
来看一下Request的源码:
/**
* An HTTP Request
*/
public static class Request implements Serializable {
/**
* Server host
*/
public String host;
/**
* Request path
*/
public String path;
/**
* QueryString
*/
public String querystring;
/**
* Full url
*/
public String url;
/**
* HTTP method
*/
public String method;
......
public static ThreadLocal<Request> current = new ThreadLocal<Request>();
...
}
封装了一般的参数,需要注意的是current是一个静态变量,并且是一个全局变量,他是对整个线程可见的。
所以使用Play! 1.0+开发的时候,我们一般获得请求参数都是这样获得的:
Params ps = Request.current().params;
4 具体业务逻辑的执行
从第2步的源码中能够猜测得到
Invoker.invokeInThread(new ServletInvocation(request, response, httpServletRequest, httpServletResponse));
这一句就是具体业务逻辑的调用。
毫无疑问,这依赖于反射机制。
那么来看看invokeInThread()究竟在做些什么。
/**
* Run the code in the same thread than caller.
* @param invocation The code to run
*/
public static void invokeInThread(DirectInvocation invocation) {
boolean retry = true;
while (retry) {
invocation.run();
if (invocation.retry == null) {
retry = false;
} else {
try {
if (invocation.retry.task != null) {
invocation.retry.task.get();
} else {
Thread.sleep(invocation.retry.timeout);
}
} catch (Exception e) {
throw new UnexpectedException(e);
}
retry = true;
}
}
}
大致上可以知道,基本流程是这样的:
- invocation.run()——执行业务逻辑;
- 请求如果需要retry(是否需要重试),请求就一直处于阻塞状态,知道请求完成,或者抛出异常。
那么DirectInvocation.run()里面做了什么呢?
/**
* It's time to execute.
*/
public void 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();
}
}
}
可以猜到,核心的处理方法应该是execute()。那么execute()又做了什么呢?
DirectInvocation的初始化在ServletWrapper.service()中进行的。
也就是:
Invoker.invokeInThread(new ServletInvocation(request, response, httpServletRequest, httpServletResponse));
所以实现类其实是ServletInvocation,而DirectInvocation继承了Invocation,Invocation实现了Runnable接口,四者之间关系如下。
来看看ServletInvocation的execute()吧:
@Override
public void execute() throws Exception {
ActionInvoker.invoke(request, response);
copyResponse(request, response, httpServletRequest, httpServletResponse);
}
于是再看一下invoke()
public static void invoke(Http.Request request, Http.Response response) {
try {
// 3. Invoke the action
try {
// @Before
handleBefores(request);
// Action
if (actionResult == null) {
...
try {
inferResult(invokeControllerMethod(actionMethod));
} catch(Result result) {
...
} catch (InvocationTargetException ex) {
...
} else {
// @Catch
handleCatch();
}
}
}
// @After
handleAfters(request);
// OK, re-throw the original action result
if (actionResult != null) {
throw actionResult;
}
throw new NoResult();
} catch (IllegalAccessException ex) {
throw ex;
} catch (IllegalArgumentException ex) {
throw ex;
} catch (InvocationTargetException ex) {
...
}
} catch (Result result) {
// OK there is a result to apply
// @Finally
...
result.apply(request, response);
...
handleFinallies(request, null);
...
} catch (PlayException e) {
...
} catch (Throwable e) {
...
} finally {
...
}
}
}
简单分析一下:
- Controller中的注解@Before和@After 、@Finnaly 都是在这个方法处理的。
- 整个流程的控制是基于try/catch;
照上面的分析,response的返回应该也是在invoke()中完成的,但是并没有看到,所以猜测response的返回都是在catch块中完成的。
来看看Controller中的renderText()
/**
* Render a 200 OK application/json response
* @param jsonString The JSON string
*/
protected static void renderJSON(String jsonString) {
throw new RenderJson(jsonString);
}
它就是在抛出一个异常啊。这个异常在invoke()中被catch到。
来看看Render相关几个类之间的关系:
实际上,一个请求的response就是在apply()中完成的。
public void apply(Request request, Response response) {
try {
setContentTypeIfNotSet(response, "text/plain; charset=" + Http.Response.current().encoding);
response.out.write(text.getBytes(getEncoding()));
} catch(Exception e) {
throw new UnexpectedException(e);
}
}
5 资源的释放
我们知道,Play!中很多全局变量,比如Request、Response、Session等,都是全局变量,是对整个线程可见的。
所以问题来了,如果一个线程要同时处理多个请求,全局的Request、Response等变量会不会同时被多个请求修改,导致请求和返回错乱?
答案是肯定的。所以,Play!号称是短小请求,同一个线程,上一个请求没结束之前,下一个请求会一直处于阻塞状态。你只要再去看一下在Invoker.invokeInThread()中的实现,就大致能够明白。
那么一个线程处理完一个请求后,如果马上接受新的请求,全局变量还是上一个请求遗留下来的。这对于新的请求而言,是完全错误的。
所以,就需要对这个全局的资源进行清理,这个过程是在ServletWrapper.service()中完成的,可以参照一下第2节的这部分源码,可以发现,是在finnally完成的。
Request.current.remove();
Response.current.remove();
Scope.Session.current.remove();
Scope.Params.current.remove();
Scope.Flash.current.remove();
Scope.RenderArgs.current.remove();
Scope.RouteArgs.current.remove();
CachedBoundActionMethodArgs.clear();
Play的源码暂时就分析到这里了。平时用的比较多的是Controller,大家可以自己去研究一下。