Play1.2.x源代码概读

Stella981
• 阅读 554

说到阅读源码,阅读的方法很重要。如果逐个类逐个类的看,看到有关联的类,就跳进去看,这样效率非常低,根本看不出整个框架的逻辑思想,重要的是容易磨灭那股看代码的冲劲。看源码,起码该知道框架是从哪里启动,先从自己熟悉、感兴趣的模块下手,结合IDE一步一步debug下去,然后逐个模块攻破。

首先,简单介绍一下普通web框架大致的工作流程:

  1. http请求到达,从容器(tomcat, netty等)获取到相应的request和response

  2. 从request中解析用户数据

  3. 结合框架本身的路由规则,执行相应的filter和action

  4. 把action处理后的数据,结合页面模板来生成返回数据

  5. 把返回的数据写入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框架从启动 -> 接受请求 -> 执行业务代码 -> 渲染页面模板 -> 返回数据到浏览器的大致流程。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
待兔 待兔
2个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Java修道之路,问鼎巅峰,我辈代码修仙法力齐天
<center<fontcolor00FF7Fsize5face"黑体"代码尽头谁为峰,一见秃头道成空。</font<center<fontcolor00FF00size5face"黑体"编程修真路破折,一步一劫渡飞升。</font众所周知,编程修真有八大境界:1.Javase练气筑基2.数据库结丹3.web前端元婴4.Jav
Wesley13 Wesley13
2年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Stella981 Stella981
2年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
2年前
Python中collections模块的使用
本文将详细讲解collections模块中的所有类,和每个类中的方法,从源码和性能的角度剖析。一个模块主要用来干嘛,有哪些类可以使用,看__init__.py就知道'''Thismoduleimplementsspecializedcontainerdatatypesprovidingalternativest
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。