说到阅读源码,阅读的方法很重要。如果逐个类逐个类的看,看到有关联的类,就跳进去看,这样效率非常低,根本看不出整个框架的逻辑思想,重要的是容易磨灭那股看代码的冲劲。看源码,起码该知道框架是从哪里启动,先从自己熟悉、感兴趣的模块下手,结合IDE一步一步debug下去,然后逐个模块攻破。
首先,简单介绍一下普通web框架大致的工作流程:
- http请求到达,从容器(tomcat, netty等)获取到相应的request和response 
- 从request中解析用户数据 
- 结合框架本身的路由规则,执行相应的filter和action 
- 把action处理后的数据,结合页面模板来生成返回数据 
- 把返回的数据写入response,交给容器输出给用户 
play的大致流程也跟上面的差不多。另外,play启动有两种模式,一个是dev模式,用于本地开发;一个是prod模式,用于线上生产环境使用。两者的最大的区别就是dev会实时动态编译被更改过的文件,大大加快开发速度;而prod会把所有的文件预编译放到precompiled目录。后面的内容是基于dev模式来讲解。
首先,play是从play.server.Server的main方法开始。
    public static void main(String[] args) throws Exception {
        File root = new File(System.getProperty("application.path"));
        if (System.getProperty("precompiled", "false").equals("true")) {
            Play.usePrecompiled = true;
        }
        
        if (System.getProperty("writepid", "false").equals("true")) {
            writePID(root);
        }
        
        Play.init(root, System.getProperty("play.id", "")); // *系统初始化*
        if (System.getProperty("precompile") == null) {
            new Server(args);
        } else {
            Logger.info("Done.");
        }
    }
进去init方法查看,首先是看readConfiguration,这里主要是根据play自己的规范,读取application.conf,play应用主要的配置项都在此文件里面,包括应用端口、数据库配置,缓存配置等等,把所有的配置都加载到内存中。
    public static void readConfiguration() {
        confs = new HashSet<VirtualFile>();
        //读取配置文件,把配置文件里面的配置都读取到内存
        configuration = readOneConfigurationFile("application.conf");
        extractHttpPort();
        pluginCollection.onConfigurationRead();
     }
然后读取相应的目录下的文件:app, conf, public....这些目录中是存放着我们写的java,css,js和其他的配置信息(例如国际化等)。
        VirtualFile appRoot = VirtualFile.open(applicationPath);
        roots.add(appRoot);
        javaPath = new CopyOnWriteArrayList<VirtualFile>();
        javaPath.add(appRoot.child("app"));
        javaPath.add(appRoot.child("conf"));
        if (appRoot.child("app/views").exists()) {
            templatesPath = new ArrayList<VirtualFile>(2);
            templatesPath.add(appRoot.child("app/views"));
        } else {
            templatesPath = new ArrayList<VirtualFile>(1);
        }
        // Main route file
        routes = appRoot.child("conf/routes");
这里面的所有文件,都用play本身对File封装的VirtualFile来存放管理,VirtualFile提供了对文件操作的方便接口。注意,这里只是把所有文件的路径给保存到内存内存中,还没到编译。
下一步,初始化play本身的classloader、插件;根据application.conf文件的内容,来初始化cookie的域。初始化操作没什么可看,就简单带过。
初始化完毕后,最后直接启动netty,监听制定的端口,等待第一个http请求到来。play从netty获取数据是在play.server.PlayHandler的messageReceived方法,这里就是play从netty的ChannelHandlerContext里解析出play自身的request。以下代码只列request的部分属性:
         //根据uri来解析
        String uri = nettyRequest.getUri();
        // Remove domain and port from URI if it's present.
        if (uri.startsWith("http://") || uri.startsWith("https://")) {
            // Begins searching / after 9th character (last / of https://)
            uri = uri.substring(uri.indexOf("/", 9));
        }
        
        //获取远程IP地址
        String remoteAddress = getRemoteIPAddress(messageEvent);
        //获取请求的方式
        String method = nettyRequest.getMethod().getName();
        if (nettyRequest.getHeader("X-HTTP-Method-Override") != null) {
            method = nettyRequest.getHeader("X-HTTP-Method-Override").intern();
        }
        ......
        //获取host
        String host = nettyRequest.getHeader(HOST);
解析完需要的request之后,在play自己分析出对应的action和method等等,所有的元素解析完并组合成自身需要的request后,就新开一个线程去跑具体的业务代码。该线程对应的工作分两种情况:
- 若是第一次请求,则加载所有的java文件到ApplicationClass,并且通过ecplise的jdt编译,把编译后的字节码存到对应的ApplicationClass。 
- 若不是第一次请求,则扫描对应的目录,对被更改过文件重新编译或删除。对文件的扫描规则是计算该文件的hashcode和lastModifyTime;对目录的扫描规则:把该目录下的所有的类名按顺序加到字符串中,再hashcode比较。 - boolean raw = Play.pluginCollection.rawInvocation(request, response); if (raw) { ..... } else { //开新的线程去跑业务代码 Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, messageEvent)); } 
invoke 实质上是新开一个线程跑Invocation,Invocation实现了Runnable,说到这里你大概懂了吧。然后去看Invoction的run
        public void run() {
            if (waitInQueue != null) {
                waitInQueue.stop();
            }
            try {
                preInit();
                if (init()) {
                    //执行相应的filter
                    before();
                    //执行业务代码
                    execute();
                    //收尾
                    after();
                    //将处理完的数据返回到浏览器
                    onSuccess();
                }
            } catch (Suspend e) {
                ...
            } catch (Throwable e) {
               ...
            } finally {
                ...
            }
        }
具体业务处理完后,play就将数据写回到response中,具体就在onSuccess里处理,进入onSuccess里,可以看到writeResponse:
         byte[] content = null;
        final boolean keepAlive = isKeepAlive(nettyRequest);
        if (nettyRequest.getMethod().equals(HttpMethod.HEAD)) {
            content = new byte[0];
        } else {
            content = response.out.toByteArray();
        }
        ChannelBuffer buf = ChannelBuffers.copiedBuffer(content);
        nettyResponse.setContent(buf);
        if (Logger.isTraceEnabled()) {
            Logger.trace("writeResponse: content length [" + response.out.size() + "]");
        }
        setContentLength(nettyResponse, response.out.size());
        //把数据写进response
        ChannelFuture f = ctx.getChannel().write(nettyResponse);
这里有一点值得注意一下,当具体的业务代码执行完毕,准备把处理后的数据传给模板渲染,我们是一般是直接调用play的render方法,而仔细看一下render方法,实质上是向上抛出了一个RenderTemplate,RenderTemplate继承FastRuntimeException,FastRuntimeException继承RuntimeException。意思就是play通知模板渲染的机制是通过抛出异常来实现的。
总所周知,Java处理Exeception性能差,根本原因在于:异常基类Throwable.java的public synchronized native Throwable fillInStackTrace()方法。性能开销在于:
1. 是一个synchronized方法(主因)
2. 需要填充线程运行堆栈信息
那就是说,Exception的性能是差,原因在于ThrowablefillInStackTrace()方法。而我们再看看FastRuntimeException的fillInStackTrace方法:
    /**
     * Since we override this method, no stacktrace is generated - much faster
     * @return always null
     */
    public Throwable fillInStackTrace() {
        return null;
    }
这下明白了吧,具体看作者自己的注释好了。
以上就是play框架从启动 -> 接受请求 -> 执行业务代码 -> 渲染页面模板 -> 返回数据到浏览器的大致流程。
 
  
  
  
 
 
  
 
 
 