实现 Play Framework 的增量编译

Play 的源码一般在 framework\src 目录下面。Java 文件的编译在 play.classloading.ApplicationClassloader 类中实现,模板的编译在 play.templates.TemplateLoader 类中实现。修改这两个文件就可以实现 Java 文件和模板文件的增量编译。

这里以 play 1.4.5 为例。

一、思路

  1. 个 play 项目,将 play 源码中需要修改的文件复制到项目中。
  2. 用 IDEA 打开项目,修改文件,让它们支持增量编译。
  3. 然后在 IDEA 中进行编译,得到 class 文件。
  4. 最后用编译好的 class 文件,替换 play 安装目录的 framework/play-1.4.5.jar 文件中对应的 class 文件。

:jar 文件其实就是一个 zip 文件,使用压缩软件就可以对其进行修改,添加新文件或覆盖已有的文件。

二、新建项目

使用命令新建一个 play 项目:

play new play-1.4.5

将 play 源码中的 ApplicationClassloader.java 和 TemplateLoader.java 文件复制到 app 目录中的对应路径:

三、修改源码

1、Java 文件的增量编译

主要修改 play.classloading.ApplicationClassloader.getAllClasses() 方法。

1)获取文件最后一次的编译时间

先添加一个方法,用于获取类的最后一次编译时间:

/**
 * 根据名称,获取文件最后一次编译时间
 *
 * @param name
 * @return
 */
private long getCompiledTime(String name) {
    File file = Play.getFile("precompiled/java/" + name.replace(".", "/") + ".class");
    if (!file.exists()) {
        return 0L;
    }

    return file.lastModified();
}

2)修改编译条件

进入 getAllClasses() 方法,找到以下代码:

if (applicationClass != null && !applicationClass.compiled && applicationClass.isClass()) {
    classNames.add(applicationClass.name);
}

修改这个条件:

if (getCompiledTime(applicationClass.name) < applicationClass.javaFile.lastModified()) {
    classNames.add(applicationClass.name);
}

classNames 是需要编译的文件的列表。上面的条件表示,只将修改过的文件添加到这个列表中。

3)只编译修改过的文件

在 getAllClasses() 方法中,找到以下代码:

for (ApplicationClass applicationClass : Play.classes.all()) {
    Class clazz = loadApplicationClass(applicationClass.name);
    if (clazz != null) {
        result.add(clazz);
    }
}

修改成如下:

// 保存原始数据
boolean oldValue = Play.usePrecompiled;
for (ApplicationClass applicationClass : Play.classes.all()) {
    // 只预编译修改过的文件
    Play.usePrecompiled = !classNames.contains(applicationClass.name);
    Class clazz = loadApplicationClass(applicationClass.name);
    if (clazz != null) {
        result.add(clazz);
    }
}
Play.usePrecompiled = oldValue;

其中添加了 3 行代码。第1行和第3行是为了保存 Play.usePrecompiled 原始值,第2行的意思是,将不再 classNames 中的类标记为已编译,即不参与 loadApplicationClass() 方法中的编译过程。

还有一步不能忘了!!!

classNames 变量的定义要提到外面一层来,因为 loadApplicationClass() 方法是在 classNames.add 代码的外面一层。

下面给出一段较为完整的代码:

List<String> classNames = new ArrayList<>();
if (!Play.pluginCollection.compileSources()) {

    List<ApplicationClass> all = new ArrayList<>();

    for (VirtualFile virtualFile : Play.javaPath) {
        all.addAll(getAllClasses(virtualFile));
    }

    for (ApplicationClass applicationClass : all) {
        if (getCompiledTime(applicationClass.name) < applicationClass.javaFile.lastModified()) {
            classNames.add(applicationClass.name);
        }
    }

    StopWatch watch = new StopWatch();
    watch.start();
    Logger.info("[ java ] compile %d java files", classNames.size());
    Play.classes.compiler.compile(classNames.toArray(new String[classNames.size()]));
    watch.stop();
    Logger.info("[ java ] compile finished! used %d ms", watch.getTime());
}

// 保存原始数据
boolean oldValue = Play.usePrecompiled;
for (ApplicationClass applicationClass : Play.classes.all()) {
    // 只预编译修改过的文件
    Play.usePrecompiled = !classNames.contains(applicationClass.name);
    Class clazz = loadApplicationClass(applicationClass.name);
    if (clazz != null) {
        result.add(clazz);
    }
}
Play.usePrecompiled = oldValue;

Collections.sort(result, new Comparator<Class>() {

    @Override
    public int compare(Class o1, Class o2) {
        return o1.getName().compareTo(o2.getName());
    }
});

4)计算编译消耗的时间

找到执行编译的代码:

Play.classes.compiler.compile(classNames.toArray(new String[classNames.size()]));

计算编译的耗时:

StopWatch watch = new StopWatch();
watch.start();
Logger.info("[ java ] compile %d java files", classNames.size());
Play.classes.compiler.compile(classNames.toArray(new String[classNames.size()]));
watch.stop();
Logger.info("[ java ] compile finished! used %d ms", watch.getTime());

2、模板文件的增量编译

主要修改 play.templates.TemplateLoader.scan() 方法。

1)获取文件最后一次的编译时间

先添加一个方法,用于获取类的最后一次编译时间:

/**
 * 根据名称,获取文件最后一次编译时间
 *
 * @param name
 * @return
 */
private static long getCompiledTime(String name) {
    if(name.indexOf("/") != 0) name = "/" + name;
    String filename = "precompiled/templates" + name;
    File file = Play.getFile(filename);
    if (file == null) return 0L;
    return file.lastModified();
}

2)修改编译条件,只编译修改过的文件

进入 scan() 方法,编译模板文件的代码:

Template template = load(current);
if (template != null) {
    try {
        template.compile();
        if (Logger.isTraceEnabled()) {
            Logger.trace("%sms to load %s", System.currentTimeMillis() - start, current.getName());
        }
    } catch (TemplateCompilationException e) {
        Logger.error("Template %s does not compile at line %d", e.getTemplate().name, e.getLineNumber());
        throw e;
    }
    templates.add(template);
}

在这段代码的外层添加一个条件:

// 计算模板文件名
String name = current.relativePath().replaceAll("\\{(.*)\\}", "from_$1").replace(":", "_").replace("..", "parent");

// 只编译修改过的文件
long lastModified =  getCompiledTime(name);
if ( lastModified < current.lastModified()) {
    // 打印正在编译的文件名
    Logger.info("[ template ] compile %s", name);
    Template template = load(current);
    if (template != null) {
        try {
            template.compile();
            if (Logger.isTraceEnabled()) {
                Logger.trace("%sms to load %s", System.currentTimeMillis() - start, current.getName());
            }
        } catch (TemplateCompilationException e) {
            Logger.error("Template %s does not compile at line %d", e.getTemplate().name, e.getLineNumber());
            throw e;
        }
        templates.add(template);
    }
}

3)计算编译消耗的时间

进入 getAllTemplate() 方法,找到 scan() 方法的循环代码:

for (VirtualFile virtualFile : Play.templatesPath) {
    scan(res, virtualFile);
}

在其中添加耗时计算代码:

// 用于计算编译时间
StopWatch watch = new StopWatch();
Logger.info("[ template ] compile template files");
for (VirtualFile virtualFile : Play.templatesPath) {
    Logger.info("[ template ] scan %s", virtualFile.getRealFile().getAbsolutePath());
    watch.reset();
    watch.start();
    scan(res, virtualFile);
    watch.stop();
    Logger.info("[ template ] compile finished! used %s ms", watch.getTime());
}
Logger.info("[ template ] compiled template files");

四、编译项目

在 IDEA 中编译项目,编译后的文件在 tmp 目录中:

五、修改 play 的 jar 包

用 修改 play 安装目录的 framework/play-1.4.5.jar

这里以 Bandizip 压缩工具为例。

右键点击 play-1.4.5.jar 文件,选择“压缩文件预览”:

在打开的窗口中先进入 play/classloading 目录,点击“添加”按钮:

根据提示选择在 IDEA 中编译好的,以“ApplicationClassloader”开头的 3 个 class 文件,覆盖 jar 包中对应的 3 个文件。

按照此方法再覆盖 play/templates 目录下的 TemplateLoader.class 文件。

然后关闭窗口,完成 jar 文件的修改。

六、禁止删除 precompiled 目录

本方案采用比较待编译的文件与 precompiled 目录下对应的已编译文件的修改时间,来判断是否需要编译。而 play 1.4.5 在执行 play precompile 命令时会首先删除 precompiled 目录,这会导致增量编译无法实现。所以要用删除 precompiled 目录的代码。

打开 play 下面的 framework/pym/play/commands/precomplie.py 文件,注释以下代码:

#    if os.path.exists(os.path.join(app.path, 'precompiled')):
#        shutil.rmtree(os.path.join(app.path, 'precompiled'))

到这里就可以正常使用增量编译的功能了。只要像平常一样使用简单 play precompile 命令即可,它会只编译修改过的 java 文件和模板文件:

$ play precompile
~        _            _
~  _ __ | | __ _ _  _| |
~ | '_ \| |/ _' | || |_|
~ |  __/|_|\____|\__ (_)
~ |_|            |__/
~
~ play! 1.4.5, https://www.playframework.com
~
~ using java version "1.8.0_181"
Listening for transport dt_socket at address: 8000
11:32:53,551 INFO  ~ Starting F:\Myspace\play-1.4.5
11:32:53,624 INFO  ~ Precompiling ...
11:32:53,629 INFO  ~ [ java ] compile 2 java files
11:32:54,197 INFO  ~ [ java ] compile finished! used 569 ms
11:32:54,412 INFO  ~ [ template ] compile template files
11:32:54,413 INFO  ~ [ template ] scan F:\Myspace\play-1.4.5\app\views
11:32:54,415 INFO  ~ [ template ] compile /app/views/Application/index.html
11:32:54,896 INFO  ~ [ template ] compile finished! used 483 ms
11:32:54,896 INFO  ~ [ template ] scan D:\play\play-1.4.5\framework\templates
11:32:54,900 INFO  ~ [ template ] compile finished! used 4 ms
11:32:54,901 INFO  ~ [ template ] compiled template files
11:32:54,919 INFO  ~ Done.

上面的日志显示,有 2 个 java 文件被编译,有一个“/app/views/Application/index.html”的模板文件被编译。

附录

最后,提供修改后的源码和 play-1.4.5.jar。

下载地址:https://pan.baidu.com/s/1ZiytMmKLGRj_CViPlviW1w


前一篇:
后一篇:

发表评论