排除模板,编译分离
我们知道,项目真正需要编译的是我们的.java文件,成百上千个java文件的编译是需要耗费一定的时间的,但是一些静态文件,如图片,网页这些资源,其实是不需要编译的.对于一个java项目来说,编译的过程可以说就是对.java文件的处理,那么结合我们的play项目,我们实际上除了类文件外的其他所有资源都可以排除掉,不让他参与编译和输出,既我们只需要让其输出precompiled中的java文件夹即可.
尝试着排除所有文件打包,看看项目是否如我们所想
play war youapplication -o outpath --exclude app:conf:lib:test:public
上边我们将所有文件夹都排除,观察输出的内容
结果仅留下了precompiled目录,且速度上又快了几十秒.
但是,我们将这个包替换原先的项目后放入tomcat中运行,项目会提示错误!报错内容会提示templates not find,缺少了模板文件?我们观察对比一下,看看究竟少了啥
第一张是报错的打包内容,第二张是正常的内容,我们发现少了app和conf 2个文件夹,app中包含了views文件夹,里面是所有模板的缓存文件,conf中有一个routes文件,是路由的缓存文件.这几个文件在前面介绍过,也是属于框架运行时会去查找的目录,原来我们把目录排出后,连缓存文件也不生成了,play在运行时为了加速模板文件的读取和执行,就会尝试去寻找缓存的模板和路由文件,找不到就报错了.
缓存的逻辑思路其实很简单,先去判断是否存在缓存,不存在则去读取原文件,若存在,则我们读取缓存.大都的缓存逻辑应该是这样的,play的缓存机制也应该如此.我们可以尝试将这些缓存文件中的乱码清空,仅保留文件的结构,相当于创建了一个空的模板缓存,发现项目可以正常运行.
通过上面的实验,我们知道不能将框架需要的模板文件也排除,且play框架在运行时会去找寻这些模板缓存文件.
这让我们的优化思路遇到了点阻碍,使用play自带的命令无法生成缓存文件.但是仔细想,我们仅仅只是生成一个和原文件相同的目录结构和文件结构,实际上不是什么太大的难题,并且既然缓存可以清除,那么我们实际上只要让java文件全部编译打包就可以了,其余的这些文件我们都可以看成是静态资源处理.
有了上面的思路,我们接下来要解决的问题就变成了:如何只做java文件的编译?并且根据上面排除指令的效果和缓存文件的存在,我们可以肯定的知道,play在打包时还会对模板文件进行编译缓存的操作,这一步也是我们需要分离出来的.查了半天帮助命令也没有找到相关分离打包的操作,遇到了框架层面的问题,我们唯一且快速有效的方式即是查看源码,从源码上找寻到底是如何编译这些文件.带着问题查看源码,可以加速我们的查询时间.
查询源码,找到编译类的方法
我们找到play1.2.7的play包,找到Play.java查看源码,我们可以搜索关键字,例如我们在输入打包命令时控制台会输出一句日志Precompiling ...
,那么我们就可以搜索这个关键,搜索后我们在427行找到了打包的代码:
//play打包
static boolean preCompile() {
if (usePrecompiled) {
if (getFile("precompiled").exists()) {
classloader.getAllClasses();
Logger.info("Application is precompiled", new Object[0]);
return true;
} else {
Logger.error("Precompiled classes are missing!!", new Object[0]);
fatalServerErrorOccurred();
return false;
}
} else {
try {
Logger.info("Precompiling ...", new Object[0]);
Thread.currentThread().setContextClassLoader(classloader);
long start = System.currentTimeMillis();
classloader.getAllClasses();
if (Logger.isTraceEnabled()) {
Logger.trace("%sms to precompile the Java stuff", new Object[]{System.currentTimeMillis() - start});
}
if (!lazyLoadTemplates) {
start = System.currentTimeMillis();
TemplateLoader.getAllTemplate();
if (Logger.isTraceEnabled()) {
Logger.trace("%sms to precompile the templates", new Object[]{System.currentTimeMillis() - start});
}
}
return true;
} catch (Throwable var2) {
Logger.error(var2, "Cannot start in PROD mode with errors", new Object[0]);
fatalServerErrorOccurred();
return false;
}
}
}
这段源码逻辑很简单,注意观察到440行至454行,里面是打包的过程classloader.getAllClasses();
TemplateLoader.getAllTemplate();
这2句明显是编译类和编译模板文件的操作,我们可以看到,play在编译时的确同时编译了java和模板文件,并且,在编译时是没有忽略需要排除的目录的,排除指令(–exclude)是在输出的时候发生的作用,这样就可以解释了,为什么排除所有的文件后,编译的速度并没有加速特别多,因为实际上play任然是对整个项目中的文件进行了遍历.
我们只需要他编译类文件即可,既只需要play执行打包命令中的
classloader.getAllClasses();
一种简单粗暴的方法是直接改源码,在源码中删除编译模板文件的逻辑.但秉承开闭原则,我们肯定是不应该去修改源码的,那么我们只能提取我们需要的逻辑自己组合成一个新的打包方法.
我们继续使用搜索关键字的形式查找具体的编译方法.因为最后的calss文件都输出到了precompiled/java文件夹中,我们再次搜索"precompiled/java/"关键字,可以在play.classloading.ApplicationClasses.java
中找到一个方法:
//将字节码写入到.class文件中
public byte[] enhance() {
this.enhancedByteCode = this.javaByteCode;
if (this.isClass()) {
boolean shouldEnhance = true;
try {
CtClass ctClass = enhanceChecker_classPool.makeClass(new ByteArrayInputStream(this.enhancedByteCode));
if (ctClass.subclassOf(ctPlayPluginClass)) {
shouldEnhance = false;
}
} catch (Exception var4) {
}
if (shouldEnhance) {
Play.pluginCollection.enhance(this);
}
}
if (System.getProperty("precompile") != null) {
try {
File f = Play.getFile("precompiled/java/" + this.name.replace(".", "/") + ".class");
f.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(f);
fos.write(this.enhancedByteCode);
fos.close();
} catch (Exception var3) {
var3.printStackTrace();
}
}
return this.enhancedByteCode;
}
这个方法既是将编译好的字节码输出到.calss文件的方法,我们再搜索一下哪些地方调用到了这个方法,查看一下官方的调用逻辑.
在play.classloading.ApplicationClassloader.java
中的158行我们找到了打包时调用该方法的代码
OK,基本方法我们已经找齐了,我们可以精简和参考源码的逻辑,写出一个test,来执行我们的优化打包逻辑:
import org.junit.Test;
import play.Play;
import play.classloading.ApplicationClasses.ApplicationClass;
import play.test.FunctionalTest;
import java.io.File;
/**
* 单个文件打包编译
*
* @author 子牙
*/
public class SinglePrecompild extends FunctionalTest {
@Test
public void test() {
File precompiledFile = Play.getFile("/precompiled/");
if (precompiledFile.exists()) {
deleteDir(precompiledFile);// 删除打包文件
}
System.setProperty("precompile", "true");// 进入编译状态
//需要打包的类名
String[] path = {"jobs.EveryDayJob"};
compileAndenhance(path);
}
/**
* 编译并输出
*
* @param fileArray
*/
private void compileAndenhance(String[] fileArray) {
for (ApplicationClass applicationClass : Play.classes.all()) {
for (String name : fileArray) {
if (name.contains(applicationClass.name)) {
applicationClass.compile();// 编译
applicationClass.enhance();// 输出
break;
}
}
}
}
/**
* 递归删除目录下的所有文件及子目录下所有文件
*
* @param dir 将要删除的文件目录
* @return boolean Returns "true" if all deletions were successful. If a
* deletion fails, the method stops attempting to delete and returns
* "false".
*/
private boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
// 递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}
}
稍微说明一下几个关键点:
-
若需要调用框架中的方法,我们毕需要运行框架的单元测试(目前没有找到单独运行的方式)
-
根据打包的代码逻辑,我们需要设置系统参数
System.setProperty("precompile", "true")
使其进入编译的状态
3.play启动时,其实compile()
编译这一步操作就已经完成了,这一步是可以注释掉的
这边的关键代码在compileAndenhance
方法中,先编译(这一步其实可以省略),然后将编译好的字节码写入文件中,并且可以实现打个文件的编译打包操作.若需要全部文件打包,我们只需要去除if判断,让所有的文件都编译即可.执行一下上方的test,编译单个文件,时间仅需5s!感觉速度还是很快的!
我们再试一下全部文件的打包,看看需要花多久
2m23s!!我们的项目可是有上千个文件的,对于这个时间,优化的效果已经显而易见了,从原先的4~5分钟,缩短到了2分半内!
至此我们已经用源码中的方法,取代了原先的打包命令,成功编译出了.class文件,已经可以应付所有的代码变动的更新需求,并且可以实现单个文件的编译了,我们完全的把编译分离了出来. 那么接下去我们就需要将重新组合我们的输出,使其在结构上和原先的打包输出一致了.