在Java中运行时编译Groovy类

11

我成功地能够在Java中运行时编译Groovy并将其存储在数据库中,并提取出来。如果一个Groovy类有内部类或内部枚举,我就不能编译它。有人能够成功地编译包含内部类/枚举的Groovy代码,并能够按类名提取脚本吗?

例如,我想要加载下面显示的包含内部类的"Test"脚本,并在运行时运行该脚本。

编译器代码:

public byte[] compileGroovyScript(final String className, final String script) {
    byte[] compiledScriptBytes = null;
    CompilationUnit compileUnit = new CompilationUnit();
    compileUnit.addSource(className, script);
    compileUnit.compile(Phases.CLASS_GENERATION);

    for (Object compileClass : compileUnit.getClasses()) {
        GroovyClass groovyClass = (GroovyClass) compileClass;
        compiledScriptBytes = groovyClass.getBytes();
    }

    return compiledScriptBytes;
}

提取脚本的代码:

public Class getGroovyScript(final String className, final byte[] script) {
    Class clazz = null;

    try (GroovyClassLoader classLoader = new GroovyClassLoader(this.getClass().getClassLoader())) {
        clazz = classLoader.defineClass(className, script);
    } catch (IOException e) {
    } catch (Exception e) {
    }

    return clazz;
}

运行脚本的代码:

Class groovyClass = app.getGroovyScript(className, compiledScript);
TestScript script = (TestScript) groovyClass.newInstance();
System.out.println(script.getMessage());

Groovy脚本:

import com.groovy.groovy.TestScript

class Test implements TestScript {

    String getMessage() {
        [1..10].each(){
            println it
        }
        return "Jello"
    }
}

你从compilationUnit迭代类,但是你只返回最后一个类的字节 compiledScriptBytes = groovyClass.getBytes(); 我不知道是否如此,但这看起来像是一个潜在的错误。 - airborn
我尝试迭代所有类并将它们存储在一个byte[]中,但是当获取Groovy类并将其转换为我的Java接口时,这种方法并不起作用。 - ColinMc
5个回答

14

从描述中并不清楚为什么您要自己进行编译。如果您可以让Groovy代劳,那么整个过程可以简化为以下内容:

String script = // string containing the script you want to parse

GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
Class theParsedClass = groovyClassLoader.parseClass(script);

2
我提到过我想编译Groovy脚本并将它们放入数据库中。这样做的原因是为了提高性能,这样当我想运行一个脚本时,就不必多次编译它。这些脚本每分钟运行一次,每分钟编译一次脚本似乎效率低下。 - ColinMc
如果我不需要编译脚本并存储它们,这个就可以工作。 - ColinMc
脚本的编译是在一个专门为工程师提供的 Web 支持工具上完成的,该工具运行在与实际运行脚本的应用程序不同的 JVM 上。 - ColinMc
听起来你仍然要求类加载器每次运行脚本时都从byte[]重新加载类。无论如何,这不是你所问的问题,所以我不会再打扰你了。抱歉分散了你的注意力。我提到的解决方案中有没有一个能解决你最初提出的问题? - Jeff Scott Brown
回顾后,我们发现将编译后的Groovy脚本存储在数据库中存在缺陷。如果我们的项目中更新了Groovy,则需要重新编译数据库中的所有脚本。另一个问题是我编写的实现无法轻松处理内部类、枚举等。这是一种更好的在运行时编译脚本的方法。我现在唯一的问题是,我有大量死亡的GroovyClassLoaders在Perm Gen中,而Full GC没有清理它们(http://stackoverflow.com/questions/27451058/groovy-update-causing-a-ton-of-dead-groovyclassloaders-in-permgen)。 - ColinMc
显示剩余3条评论

5

好的,可能有点晚,但希望对下一个人有所帮助。 我认为您需要为每个Groovy类保存一个列表,然后进行cl.defineClass,最后是cl.loadClass。 我认为Groovy有时会编译成一系列类,基本上就像下面的代码一样,当我添加Source()时,我添加一个类,然后遍历来自该文件的所有生成的类。

这是我正在运行的代码(虽然我尚未尝试在稍后保存和重新加载)

    GroovyClassLoader cl = new GroovyClassLoader();
    CompilationUnit compileUnit = new CompilationUnit();
    compileUnit.addSource(scriptCode.getClassName(), scriptCode.getScriptSourceCode());
    compileUnit.compile(Phases.CLASS_GENERATION);
    compileUnit.setClassLoader(cl);

    GroovyClass target = null;
    for (Object compileClass : compileUnit.getClasses()) {
        GroovyClass groovyClass = (GroovyClass) compileClass;
        cl.defineClass(groovyClass.getName(), groovyClass.getBytes());
        if(groovyClass.getName().equals(scriptCode.getClassName())) {
            target = groovyClass;
        }
    }

    if(target == null) 
        throw new IllegalStateException("Could not find proper class");

    return cl.loadClass(target.getName());

请注意cl.defineClass调用,它将类放入类加载器中,因此在查找时(枚举或内部类),它将存在。
因此,我认为您现在不需要创建自己的类加载器(虽然您可以通过自己的类加载器避免无用的defineClass,这可能很有用并且更高效)。

0

我自己也遇到了这个问题,但是我刚刚在运行时完成了一个按需Java编译器,我相信你遇到的是我在这段代码中解决的同样的问题

https://github.com/deanhiller/webpieces/tree/master/runtimecompile/src/main/java/org/webpieces/compiler/api

webpieces/runtimecompile是一个可重复使用的按需Java编译器,使用Eclipse编译器。

现在,对于Groovy,我认为你遇到了这种情况

1. you compile ONE script
2. this results in 'multiple' class file objects (I think) just like mine did
3. This is where you need to store EACH in the database SEPARATELY
4. Then you need a classloader that tries to lookup the 'inner classes' when jvm asks for it
5. finally you do a yourclassLoader.loadApplicationClass (much like the one in CompileOnDemandImpl.java in the project above
6. To be clear, step 5 causes step 4 to happen behind the scenes (and that is what is confusing).

如果你按照测试案例AnonymousByteCacheTest的步骤,它基本上做的就是这样。

你不需要安装任何东西来运行该项目的构建,只需克隆它并"./gradlew test",就会通过"./gradlew eclipse"或"./gradlew idea",它会生成IDE文件,以便你可以逐步执行它。

这非常非常相似。我接下来要尝试让Groovy版本的工作。


0

为了简单起见,这里放弃了任何错误处理,但这可能是您想要的:

public byte[] compileGroovyScript(final String className, final String script) {
    byte[] compiledScriptBytes = null;
    CompilationUnit compileUnit = new CompilationUnit();
    compileUnit.addSource(className, script);
    compileUnit.compile(Phases.CLASS_GENERATION);

    List classes = compileUnit.getClasses();
    GroovyClass firstClass = (GroovyClass)classes.get(0);
    compiledScriptBytes = firstClass.getBytes();

    return compiledScriptBytes;
}

0

根据您的需求,您可能希望提供对内部类的访问权限,您可以使用以下代码来查找与名称匹配的类,而不是假定第一个类:

public byte[] compileGroovyScript(final String className, final String script) {
    byte[] compiledScriptBytes = null;
    CompilationUnit compileUnit = new CompilationUnit();
    compileUnit.addSource(className, script);
    compileUnit.compile(Phases.CLASS_GENERATION);

    for (Object compileClass : compileUnit.getClasses()) {
        GroovyClass groovyClass = (GroovyClass) compileClass;
        if(className.equals(groovyClass.getName())) {
            compiledScriptBytes = groovyClass.getBytes();
            break;
         }

    }

    return compiledScriptBytes;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接