Search code examples
javagroovyclassloadergroovyclassloader

GroovyClassLoader call to parseClass is successful, even when code does not compile


I'm trying to dynamically load a Groovy script as a class but the class object is created even when the script's code does not compile.

For example, a simplified version of my Groovy code to load the Groovy script is as follows:

GroovyCodeSource src = new GroovyCodeSource(
    "blah blah blah",
    "Foo.groovy",
    GroovyShell.DEFAULT_CODE_BASE
)
new GroovyClassLoader().parseClass(src, true)

Clearly, the code blah blah blah isn't a legitimate Groovy script. And yet, a class object is successfully created for this dynamic code. According to GroovyClassLoader's Javadoc for the parseClass method a CompilationFailedException should be thrown for cases such as this.

How is it possible that the class is still created for broken code and how can I successfully create a class from dynamic Groovy source code conditionally on whether or not the code will compile? I've done a lot of research and experimentation but to no avail.


Solution

  • That's because groovy provides dynamic access to methods and properties and in terms of Groovy, the code blah blah blah, is valid. Actually you are providing code for Script (there is no class declaration). After compilation, you will get a class that extends groovy.lang.Script.

    So, let me continue your code and show you how it could be valid...

    GroovyCodeSource src = new GroovyCodeSource(
        'blah blah blah',
        "Foo.groovy",
        GroovyShell.DEFAULT_CODE_BASE
    )
    def c = new GroovyClassLoader().parseClass(src, true)
    println c                     //class Foo
    println c.getSuperclass()     //class groovy.lang.Script
    
    def i = c.newInstance()
    //i.run()                     //MissingPropertyException: No such property: blah for class: Foo
    i.setBinding([
        blah: { x-> return [blah: "x.class =${x.getClass()}"] }
    ] as Binding)
    i.run()                       //SUCCESS
    

    I would also advise you to run groovyconsole, enter blah blah blah, press Ctrl+T, and check what class was generated for your script. Note that you could switch between different compilation/parsing phases. enter image description here


    A possible workaround is to use the CompileStatic annotation on the methods or the class:

    //compilation of this code will fail with message
    //[Static type checking] - The variable [blah] is undeclared.
    @groovy.transform.CompileStatic
    def f(){
        blah blah blah
    }
    f()
    

    You could force GroovyClassLoader to make static validation for the whole script.

    Let's imagine you want your scripts to access only some pre-defined variables/methods and you want to check this at compile step and not at runtime.

    The following example shows how to do that and it will fail the blah blah blah code during compilation:

    import org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder
    import org.codehaus.groovy.control.CompilerConfiguration
    import groovy.transform.CompileStatic
    
    //your base Script class that declares only valid members
    //for example `log`
    abstract class MyScript extends groovy.lang.Script{
        PrintStream log
    }
    
    //create compiler config with base script class 
    CompilerConfiguration cc = new CompilerConfiguration()
    cc.setScriptBaseClass(MyScript.class.getName())
    //make static compilation set for class loader
    cc = CompilerCustomizationBuilder.withConfig(cc){ 
        ast(CompileStatic) 
    }
    //create classloader with compile config
    GroovyClassLoader gcl = new GroovyClassLoader(this.getClass().getClassLoader(),cc)
    
    
    GroovyCodeSource src = new GroovyCodeSource(
        "log.println 'hello world'",
        "Foo.groovy",
        GroovyShell.DEFAULT_CODE_BASE
    )
    def c = gcl.parseClass(src, true)  //this will fail for 'blah blah blah' source
    def i = c.newInstance(log:System.out)
    i.run()
    

    P.S. There are other code transformers available in Groovy.