1 play!的内置服务器

其内置服务器是采用了netty编写,所以开发阶段play无须借助第三方的server,如tomcat;需要注意的是,部署项目的时候,这个server就没啥用的,因为项目最终是需要放到我们的tomcat下的。

但是看看play的项目目录:

image.png

有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();
    }
}

看起来并没有很复杂的样子。简单说来,做了以下几件事情:

  1. 将请求封装成Request对象;
  2. Invoker执行具体的业务逻辑;
  3. catch相关异常;
  4. 清理资源;

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;
            }
        }
    }

大致上可以知道,基本流程是这样的:

  1. invocation.run()——执行业务逻辑;
  2. 请求如果需要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接口,四者之间关系如下。

image.png

来看看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 {
            ...
        }
    }

}

简单分析一下:

  1. Controller中的注解@Before和@After 、@Finnaly 都是在这个方法处理的。
  2. 整个流程的控制是基于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相关几个类之间的关系:

image.png

实际上,一个请求的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,大家可以自己去研究一下。