Search code examples
javagroovyclassloader

GroovyClassLoader - Isolate from parent class loader


I'm trying to run Groovy scripts inside my Java Application using the GroovyClassLoader and the GroovyScriptEngineImpl, and I want to isolate the Groovy script from the parent application context.

What I mean is that when I'm running my Groovy script, I don't want it to inherit from the dependencies loaded in my Java application in order to be able to, for example, load Gson 2.5.5 in my script, even if my Java application is using Gson 3.4.1.

// Replacing the context class loader with the system one
ClassLoader initialCL = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader().getParent());

// Creating my GroovyClassLoader and GroovyScriptEngine
GroovyClassLoader groovyCL = new GroovyClassLoader();
GroovyScriptEngineImpl scriptEngine = new GroovyScriptEngineImpl(groovyCL);

// Compiling and running my Groovy script
CompiledScript compiledScript = scriptEngine.compile("println \"hello\"");
compiledScript.eval();

// Going back to my initial classloader
Thread.currentThread().setContextClassLoader(initialCL);

This way, the isolation is indeed working, but the content of the script is not executed at all (even printing a line in the console for example), and I'm getting no error anywhere.

If I don't update my context classloader before creating the new GroovyClassLoader, the script is working fine, but it's inheriting from the parent dependencies.

Do you have any idea?

Thanks :)

UPDATE : After a bit more testing, It seems like the compilation is working properly, but the evaluation of the compiled script isn't doing anything. Indeed, I'm getting an error when trying to compile a script that doesn't have all the dependencies needed even tho they're present in my Java application classpath.


Solution

  • Okay, finally figured it out, and this is an interesting one.

    I said that there was no error, and while it wasn't printed, I actually had one (quite obvious since it wasn't working...). I managed to get the error by changing slightly my method to execute Groovy scripts using a GroovyShell instead of my GroovyScriptEngineImpl and GroovyClassLoader:

    Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader().getParent());
    GroovyClassLoader groovyCL = new GroovyClassLoader();
    
    new GroovyShell(groovyCL).evaluate("println(\"test\")");
    

    And here's the error I'm finally getting and that was, for some reason, hidden during my previous executions, not using the GroovyShell:

    groovy.lang.GroovyRuntimeException: Failed to create Script instance for class: class Script1. Reason: java.lang.ClassCastException: Script1 cannot be cast to groovy.lang.GroovyObject
    

    So what's the problem?

    Well actually, the classloader that will be used to compile the script and the one used to evaluate the compiled script are not the same: groovyCL is used to compile the script, and the "current thread" classloader is used to create the GroovyShell object and evaluate the script.

    This means that the groovy.lang.GroovyObject from both classloaders are not compatible, since the class is defined in both classloaders (even tho they're the exact same class codewise).

    Then, when trying to cast the Script1 object created by groovyCL, the GroovyShell (or the mechanism I used before with GroovyScriptEngineImpl etc...) will encounter ClassCastException.

    This whole thing can lead to funny errors such as:

    java.lang.ClassCastException: groovy.lang.GroovyShell cannot be cast to groovy.lang.GroovyShell
    

    The solution

    So, what you want to do is to create your GroovyClassLoader instance, and, using this classloader, create all the objects of the workflow using reflection:

    GroovyClassLoader groovyCL = new GroovyClassLoader();
    
    Class groovyShellClazz = groovyCL.loadClass(GroovyShell.class.getName());
    Object groovyShellObj = groovyShellClazz.newInstance();
    Method evaluateMethod = groovyShellClazz.getMethod("evaluate", String.class);
    evaluateMethod.invoke(groovyShellObj, "println(\"test\")");
    

    This requires a bit more work, but the script will be compiled and evaluated by the same classloader and the problem will be fixed.

    I still have to work on adapting this solution to my initial situation using the GroovyScriptEngineImpl but it's the exact same method :)