» Articles » get rid of groovy recompilation

get rid of groovy recompilation

Compilation time for a groovy script adds a relatively high overhead to the running time of an application that is supposed to run very often. Granted, the GroovyScriptEngine does on-demand recompilation, but the compilation units don’t survive a restart.

I’m working on a project that contains a little command line application that is supposed to run very often. Every overhead adds up costs very quickly. I’ve measured more than a second overhead to compile two very small groovy scripts.

In this entry, we will create a mechanism that stores compiled groovy classes in a temporary directory and reuses them if none of the scripts have any changes.

None of the solution on the Embedding Groovy can be used as far as I know. None of the described methods have a way of storing the result of the compilation an reusing them.

The idea of the proposed solution is the following:

  • detect if there is a script that is newer than any of the compiled scripts
  • recompile all scripts if there are any changes
  • extend the classpath to use the compiled scripts

Let’s go through each step…

detect if there is a script that is newer than any of the compiled scripts

The first part is very easy. We just need a class that implements an API like this:

/**
 * returns true if file compareFrom (or all files in compareFrom, if
 * it points to a directory) is newer than file compareTo (or newer
 * than any file in compareTo if it points to a directory)
 * 
 * @param compareFrom
 * @param compareTo
 */
public static boolean newerThan(File compareFrom, File compareTo) {
// impl ...
}

The implementation is quite trivial.

recompile all scripts if there are any changes

The easiest way to compile groovy scripts into class files is to use ‘CompilationUnit’ directly.

public void compileScriptlets(File scriptDir, File cacheDir) {
    try {
        if (!ChangeChecker.newerThan(scriptDir, cacheDir)) {
            return;
        }
        CompilerConfiguration cc = new CompilerConfiguration();
        cc.setDebug(true);
        cc.setCacheDirDirectory(cacheDir);
        cc.setVerbose(true);
        //
        ClassLoader parent = getClass().getClassLoader();
        GroovyClassLoader gcl = new GroovyClassLoader(parent);
        //
        List<File> scriptlets = Utils.allfiles(scriptDir);
        for (File file : scriptlets) {
            CompilationUnit cu = new CompilationUnit();
            cu.addSource(new File(scriptDir, file.getName()));
            cu.setConfiguration(cc);
            cu.configure(cc);
            cu.setClassLoader(gcl);
            cu.compile();
        }
    }
    catch (Exception e) {
        LOG.error("exception caught: ", e);
    }
}

extend the classpath to use the compiled scripts

The previous code will create class files inside a given directory. We need to extend the current classloader by this directory. Fumbling around with the classloader is tricky and error prone and can lead to unforeseen problems if our extending code runs within an unexpected environment (a container, within ant or maven).

Please use the following code with caution!

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
/**
 * extend the given classloader with the given URL
 */
public static void addURL(URLClassLoader classloader, URL u) {
    try {
        Method method = URLClassLoader.class.getDeclaredMethod(
                    "addURL", new Class[]{URL.class});
        method.setAccessible(true);
        method.invoke(classloader, new Object[]{ u });
    }
    catch (Throwable t) {
        LOG.error("Classloader could not be extended!", t);
    }
}

Once we call this method, we can instantiate classes that have been previously compiled by the groovy compiler…

URLClassLoader uc = (URLClassLoader)getClass().getClassLoader();
ClassPathExtender.addURL(
        uc,
        target.toURI().toURL());
Class<?> forName = Class.forName("OneOfYourGroovyClasses", true, uc);
Object newInstance = forName.newInstance();

That’s all there is to it.