Скомпилируйте класс Groovy во время выполнения в Java

Я успешно могу скомпилировать Groovy на Java во время выполнения, сохранить его в базе данных и извлечь. Я не могу скомпилировать класс Groovy, если он имеет внутренние классы или внутреннее перечисление. Кто-нибудь успешно скомпилировал такой код Groovy, включил внутренние классы/перечисления и смог вытащить скрипт по имени класса?

Например, я хочу загрузить показанный ниже сценарий «Тест», который содержит внутренние классы, и запустить сценарий во время выполнения.

Код компилятора:

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());

Крутой скрипт:

import com.groovy.groovy.TestScript

class Test implements TestScript {

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

person ColinMc    schedule 15.04.2014    source источник
comment
Вы перебираете классы изcompileUnit, но возвращаете только байты из последнего класса compiledScriptBytes = groovyClass.getBytes(); Я не знаю, так ли это, но это похоже на потенциальную ошибку.   -  person airborn    schedule 16.04.2014
comment
Что ж, я попытался перебрать все классы и сохранить их в одном байте [], но это не сработало при получении класса groovy и приведении его к моему интерфейсу Java.   -  person ColinMc    schedule 16.04.2014


Ответы (5)


Из описания непонятно, почему вы сами компилируете. Если вы можете просто позволить Groovy сделать это за вас, то все это можно просто упростить до чего-то вроде этого:

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

GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
Class theParsedClass = groovyClassLoader.parseClass(script);
person Jeff Scott Brown    schedule 16.04.2014
comment
Я упомянул, что хочу скомпилировать сценарии Groovy и поместить их в базу данных. Причина этого в производительности, поэтому, когда я хочу запустить скрипт, мне не нужно компилировать его несколько раз. Скрипты запускаются каждую минуту, и ежеминутная компиляция скрипта кажется неэффективной. - person ColinMc; 17.04.2014
comment
Если бы мне не нужно было компилировать скрипты и хранить их, это сработало бы. - person ColinMc; 17.04.2014
comment
Если каждую минуту вы извлекаете байты из базы данных, а затем вызываете defineClass(), чтобы превратить эти байты в класс, вы, вероятно, можете улучшить это. Ваше приложение таково, что по какой-то причине вы не можете получить доступ к загрузчику классов, который доступен каждый раз, когда вам нужен доступ к классу скрипта? Пока вы можете это делать, вы можете избежать необходимости выполнять функцию defineClass(), чтобы превращать ваши байты в класс каждый раз, когда вам нужно его запустить. Вы можете сделать это только один раз, а затем каждый раз, когда вам нужно будет запускать этот скрипт, класс уже будет доступен в загрузчике классов. - person Jeff Scott Brown; 17.04.2014
comment
Компиляция сценариев выполняется с помощью инструмента веб-поддержки для инженеров, который запускается на отдельной JVM, чем приложение, которое фактически запускает сценарии. - person ColinMc; 17.04.2014
comment
Это по-прежнему звучит так, как будто вы просите загрузчик классов перезагружать класс из byte[] каждый раз, когда вам нужно запустить скрипт. В любом случае, это не та проблема, о которой вы спрашивали, так что я не буду вас больше беспокоить. Извините за отвлечение. Устраняет ли какое-либо из упомянутых выше решений проблему, о которой вы изначально спрашивали? - person Jeff Scott Brown; 17.04.2014
comment
Ну, это сработало, если бы я не компилировал сценарии заранее. Другие потерпели неудачу, поскольку внутренние классы все еще не скомпилировались. Мне нужно скомпилировать весь сценарий Groovy, даже если он имеет внутренние классы, и сохранить его в базе данных в виде массива байтов. Затем мне нужно извлечь этот массив байтов из базы данных и запустить скрипт. - person ColinMc; 17.04.2014
comment
Ваш первоначальный вопрос гласит: Кто-нибудь успешно скомпилировал такой код Groovy и включил внутренние классы/перечисления? Код, который я показал, действительно компилирует код Groovy, который включает внутренние классы. Если вы хотите скомпилировать внутренние классы, выполните итерацию по всем классам, возвращенным из compileUnit.getClasses(), и обработайте каждый из этих byte[]. Проблема с вашим исходным кодом заключается в том, что вы повторяете все из них, но имеете дело только с последним. Посмотрите на цикл for в вашем compileGroovyScript. - person Jeff Scott Brown; 17.04.2014
comment
Оглядываясь назад, мы обнаружили недостатки в хранении скомпилированных скриптов Groovy в базе данных. Если Groovy будет обновлен в нашем проекте, то все скрипты в базе данных должны быть перекомпилированы. Другим случаем была реализация, которую я написал, что было невозможно легко обрабатывать внутренние классы, перечисления и т. д. Это лучший способ компилировать скрипты во время выполнения. Единственная проблема, которая у меня есть прямо сейчас, это то, что у меня есть тонна мертвых GroovyClassLoaders в Perm Gen, которые не очищаются полным GC (stackoverflow.com/questions/27451058/). - person ColinMc; 18.12.2014

Хорошо, это может быть немного поздно, но, надеюсь, это поможет следующему человеку. Я думаю, вам нужно сохранить список для каждого классного класса, а затем cl.defineClass и, наконец, cl.loadClass. Я думаю, что groovy иногда компилируется в список классов, как показано ниже, когда я addSource(), я добавляю один класс, а затем перебираю все сгенерированные классы из этого одного файла.

Это код, который я сейчас запускаю (хотя я не пробовал сохранять и перезагружать позже)

    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, пока он не понадобится с вашим собственным загрузчиком классов, который может быть полезным и более производительным).

person Dean Hiller    schedule 02.07.2016

Здесь для простоты здесь отсутствует какая-либо обработка ошибок, но, вероятно, это то, что вам нужно:

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;
}
person Jeff Scott Brown    schedule 16.04.2014

В зависимости от ваших требований вы можете захотеть предоставить доступ к внутренним классам, и вы можете сделать это с помощью чего-то вроде этого, который находит класс с соответствующим именем вместо того, чтобы предполагать первый класс:

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;
}
person Jeff Scott Brown    schedule 16.04.2014

Я сталкиваюсь с этим сам, но только что выполнил компилятор 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, чтобы вы может пройти через это.

Это очень очень похоже. Я пытаюсь заставить заводную версию работать дальше.

person Dean Hiller    schedule 01.07.2016