说到阅读源码,阅读的方法很重要。如果逐个类逐个类的看,看到有关联的类,就跳进去看,这样效率非常低,根本看不出整个框架的逻辑思想,重要的是容易磨灭那股看代码的冲劲。看源码,起码该知道框架是从哪里启动,先从自己熟悉、感兴趣的模块下手,结合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框架从启动 -> 接受请求 -> 执行业务代码 -> 渲染页面模板 -> 返回数据到浏览器的大致流程。