«1. Введение

В этом руководстве мы рассмотрим новейшие методы интеграции Groovy в приложение Java.

2. Несколько слов о Groovy

Язык программирования Groovy — это мощный динамический язык с опциональной типизацией. Он поддерживается Apache Software Foundation и сообществом Groovy при участии более 200 разработчиков.

Его можно использовать для сборки всего приложения, для создания модуля или дополнительной библиотеки, взаимодействующей с нашим кодом Java, или для запуска оцениваемых и компилируемых скриптов на лету.

Для получения дополнительной информации прочитайте Введение в язык Groovy или перейдите к официальной документации.

3. Зависимости Maven

На момент написания последним стабильным выпуском была версия 2.5.7, а Groovy 2.6 и 3.0 (оба выпущены осенью 2017 года) все еще находятся в стадии альфа-тестирования.

Подобно Spring Boot, нам просто нужно включить pom groovy-all, чтобы добавить все зависимости, которые могут нам понадобиться, не беспокоясь об их версиях:

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>${groovy.version}</version>
    <type>pom</type>
</dependency>

4. Совместная компиляция

Прежде чем перейти к подробности того, как настроить Maven, нам нужно понимать, с чем мы имеем дело.

Наш код будет содержать файлы Java и Groovy. У Groovy не возникнет проблем с поиском классов Java, но что, если мы хотим, чтобы Java нашла классы и методы Groovy?

На помощь приходит совместная компиляция!

Совместная компиляция — это процесс, предназначенный для компиляции файлов Java и Groovy в одном проекте с помощью одной команды Maven.

При совместной компиляции компилятор Groovy будет:

    анализировать исходные файлы в зависимости от реализации, создавать заглушки, совместимые с компилятором Java, вызывать компилятор Java для компиляции заготовок вместе с исходными кодами Java – таким образом Классы Java могут находить зависимости Groovy, компилировать исходные коды Groovy — теперь наши исходные коды Groovy могут находить свои зависимости Java. найдите их.

Без совместной компиляции исходные файлы Java будут скомпилированы, как если бы они были исходными кодами Groovy. Иногда это может работать, поскольку большая часть синтаксиса Java 1.7 совместима с Groovy, но семантика будет другой.

5. Плагины компилятора Maven

Существует несколько плагинов компилятора, которые поддерживают совместную компиляцию, каждый со своими сильными и слабыми сторонами.

Двумя наиболее часто используемыми с Maven являются Groovy-Eclipse Maven и GMaven+.

5.1. Плагин Groovy-Eclipse Maven

Плагин Groovy-Eclipse Maven упрощает совместную компиляцию, избегая создания заглушек, что по-прежнему является обязательным шагом для других компиляторов, таких как GMaven+, но имеет некоторые особенности конфигурации.

Чтобы включить поиск новейших артефактов компилятора, мы должны добавить репозиторий Maven Bintray:

Затем в разделе подключаемых модулей мы сообщаем компилятору Maven, какую версию компилятора Groovy он должен использовать.

<pluginRepositories>
    <pluginRepository>
        <id>bintray</id>
        <name>Groovy Bintray</name>
        <url>https://dl.bintray.com/groovy/maven</url>
        <releases>
            <!-- avoid automatic updates -->
            <updatePolicy>never</updatePolicy>
        </releases>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </pluginRepository>
</pluginRepositories>

На самом деле подключаемый модуль, который мы будем использовать, — подключаемый модуль компилятора Maven — на самом деле не компилирует, а вместо этого делегирует задание артефакту groovy-eclipse-batch: -все версии зависимостей должны соответствовать версии компилятора.

Наконец, нам нужно настроить автообнаружение нашего исходного кода: по умолчанию компилятор будет искать в таких папках, как src/main/java и src/main/groovy, но если наша папка java пуста, компилятор не будет искать для наших заводных источников.

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <compilerId>groovy-eclipse-compiler</compilerId>
        <source>${java.version}</source>
        <target>${java.version}</target>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-compiler</artifactId>
            <version>3.3.0-01</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-batch</artifactId>
            <version>${groovy.version}-01</version>
        </dependency>
    </dependencies>
</plugin>

Тот же механизм работает и для наших тестов.

Чтобы принудительно обнаружить файл, мы можем добавить любой файл в src/main/java и src/test/java или просто добавить плагин groovy-eclipse-compiler:

Раздел \u003cextension\u003e обязательно, чтобы подключаемый модуль добавлял дополнительную фазу сборки и цели, содержащие две исходные папки Groovy.

5.2. Плагин GMavenPlus

<plugin>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-eclipse-compiler</artifactId>
    <version>3.3.0-01</version>
    <extensions>true</extensions>
</plugin>

Плагин GMavenPlus может иметь имя, похожее на старый плагин GMaven, но вместо создания простого патча автор попытался упростить компилятор и отделить его от конкретной версии Groovy.

«Для этого плагин отделяется от стандартных рекомендаций для плагинов компилятора.

Компилятор GMavenPlus добавляет поддержку функций, которых в то время не было в других компиляторах, например, invokedynamic, консоль интерактивной оболочки и Android.

С другой стороны, это представляет некоторые сложности:

он модифицирует исходные каталоги Maven, чтобы они содержали как исходники Java, так и исходники Groovy, но не заглушки Java, он требует от нас управления заглушками, если мы не удаляем их с помощью правильные цели

Чтобы настроить наш проект, нам нужно добавить gmavenplus-plugin:

    Чтобы можно было протестировать этот плагин, мы создали второй файл pom с именем gmavenplus-pom.xml в образце.

5.3. Компиляция с помощью подключаемого модуля Eclipse-Maven

<plugin>
    <groupId>org.codehaus.gmavenplus</groupId>
    <artifactId>gmavenplus-plugin</artifactId>
    <version>1.7.0</version>
    <executions>
        <execution>
            <goals>
                <goal>execute</goal>
                <goal>addSources</goal>
                <goal>addTestSources</goal>
                <goal>generateStubs</goal>
                <goal>compile</goal>
                <goal>generateTestStubs</goal>
                <goal>compileTests</goal>
                <goal>removeStubs</goal>
                <goal>removeTestStubs</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <!-- any version of Groovy \>= 1.5.0 should work here -->
            <version>2.5.6</version>
            <scope>runtime</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</plugin>

Теперь, когда все настроено, мы наконец можем создать наши классы.

В приведенном нами примере мы создали простое приложение Java в исходной папке src/main/java и несколько сценариев Groovy в папке src/main/groovy, где мы можем создавать классы и сценарии Groovy.

Давайте соберем все с помощью плагина Eclipse-Maven:

Здесь мы видим, что Groovy все компилирует.

5.4. Компиляция с помощью GMavenPlus

$ mvn clean compile
...
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ core-groovy-2 ---
[INFO] Changes detected - recompiling the module!
[INFO] Using Groovy-Eclipse compiler to compile both Java and Groovy files
...

GMavenPlus показывает некоторые отличия:

Мы сразу замечаем, что GMavenPlus проходит следующие дополнительные этапы:

Генерируя заглушки, GMavenPlus наследует слабость, которая доставляла много головной боли разработчикам в прошлом. лет, при работе с совместной компиляцией.

$ mvn -f gmavenplus-pom.xml clean compile
...
[INFO] --- gmavenplus-plugin:1.7.0:generateStubs (default) @ core-groovy-2 ---
[INFO] Using Groovy 2.5.7 to perform generateStubs.
[INFO] Generated 2 stubs.
[INFO]
...
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ core-groovy-2 ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 3 source files to XXX\Baeldung\TutorialsRepo\core-groovy-2\target\classes
[INFO]
...
[INFO] --- gmavenplus-plugin:1.7.0:compile (default) @ core-groovy-2 ---
[INFO] Using Groovy 2.5.7 to perform compile.
[INFO] Compiled 2 files.
[INFO]
...
[INFO] --- gmavenplus-plugin:1.7.0:removeStubs (default) @ core-groovy-2 ---
[INFO]
...

В идеальном сценарии все будет работать просто отлично, но если добавить больше шагов, у нас будет больше точек отказа: например, сборка может завершиться ошибкой до того, как удастся очистить заглушки.

  1. Generating stubs, one for each groovy file
  2. Compiling the Java files – stubs and Java code alike
  3. Compiling the Groovy files

Если это произойдет, старые заглушки могут запутать нашу IDE, что приведет к ошибкам компиляции, хотя мы знаем, что все должно быть правильно.

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

5.5. Упаковка зависимостей в JAR-файле

Чтобы запустить программу как JAR-файл из командной строки, мы добавили maven-assembly-plugin, который будет включать все зависимости Groovy в «толстый JAR-файл» с постфиксом, определенным в свойство descriptorRef:

После завершения компиляции мы можем запустить наш код с помощью этой команды:

6. Загрузка кода Groovy на лету

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <!-- get all project dependencies -->
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <!-- MainClass in mainfest make a executable jar -->
        <archive>
            <manifest>
                <mainClass>com.baeldung.MyJointCompilationApp</mainClass>
            </manifest>
        </archive>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <!-- bind to the packaging phase -->
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Компиляция Maven позволяет нам включать файлы Groovy в наш проект и ссылки на их классы и методы из Java.

$ java -jar target/core-groovy-2-1.0-SNAPSHOT-jar-with-dependencies.jar com.baeldung.MyJointCompilationApp

Хотя этого недостаточно, если мы хотим изменить логику во время выполнения: компиляция выполняется вне этапа выполнения, поэтому нам все равно нужно перезапустить наше приложение, чтобы увидеть наши изменения.

Чтобы воспользоваться динамической мощью (и рисками) Groovy, нам нужно изучить методы, доступные для загрузки наших файлов, когда наше приложение уже запущено.

6.1. GroovyClassLoader

Для этого нам нужен GroovyClassLoader, который может анализировать исходный код в текстовом или файловом формате и генерировать результирующие объекты класса.

Когда источником является файл, результат компиляции также кэшируется, чтобы избежать накладных расходов, когда мы запрашиваем у загрузчика несколько экземпляров одного и того же класса.

Скрипт, поступающий непосредственно из объекта String, вместо этого не будет кэшироваться, поэтому вызов одного и того же скрипта несколько раз может привести к утечке памяти.

GroovyClassLoader — это основа, на которой строятся другие системы интеграции.

Реализация относительно проста:

6.2. GroovyShell

Метод parse() загрузчика сценариев оболочки принимает исходные тексты в текстовом или файловом формате и создает экземпляр класса Script.

private final GroovyClassLoader loader;

private Double addWithGroovyClassLoader(int x, int y) 
  throws IllegalAccessException, InstantiationException, IOException {
    Class calcClass = loader.parseClass(
      new File("src/main/groovy/com/baeldung/", "CalcMath.groovy"));
    GroovyObject calc = (GroovyObject) calcClass.newInstance();
    return (Double) calc.invokeMethod("calcSum", new Object[] { x, y });
}

public MyJointCompilationApp() {
    loader = new GroovyClassLoader(this.getClass().getClassLoader());
    // ...
}

Этот экземпляр наследует метод run() от Script, который выполняет весь файл сверху донизу и возвращает результат, заданный последней выполненной строкой.

Если мы хотим, мы также можем расширить Script в нашем коде и переопределить реализацию по умолчанию, чтобы напрямую вызывать нашу внутреннюю логику.

Реализация вызова Script.run() выглядит так:

«

«Обратите внимание, что run() не принимает параметры, поэтому нам нужно будет добавить в наш файл некоторые глобальные переменные, чтобы инициализировать их через объект Binding.

private Double addWithGroovyShellRun(int x, int y) throws IOException {
    Script script = shell.parse(new File("src/main/groovy/com/baeldung/", "CalcScript.groovy"));
    return (Double) script.run();
}

public MyJointCompilationApp() {
    // ...
    shell = new GroovyShell(loader, new Binding());
    // ...
}

Поскольку этот объект передается при инициализации GroovyShell, переменные совместно используются всеми экземплярами сценария.

Если мы предпочитаем более детальное управление, мы можем использовать invokeMethod(), который может обращаться к нашим собственным методам посредством отражения и напрямую передавать аргументы.

Давайте посмотрим на эту реализацию:

Под прикрытием GroovyShell полагается на GroovyClassLoader для компиляции и кэширования результирующих классов, поэтому те же самые правила, описанные ранее, применяются таким же образом.

private final GroovyShell shell;

private Double addWithGroovyShell(int x, int y) throws IOException {
    Script script = shell.parse(new File("src/main/groovy/com/baeldung/", "CalcScript.groovy"));
    return (Double) script.invokeMethod("calcSum", new Object[] { x, y });
}

public MyJointCompilationApp() {
    // ...
    shell = new GroovyShell(loader, new Binding());
    // ...
}

6.3. GroovyScriptEngine

Класс GroovyScriptEngine специально предназначен для тех приложений, которые полагаются на повторную загрузку скрипта и его зависимостей.

Несмотря на то, что у нас есть эти дополнительные функции, реализация имеет лишь несколько небольших отличий:

На этот раз нам нужно настроить корни исходного кода, и мы ссылаемся на скрипт только по его имени, что немного чище .

private final GroovyScriptEngine engine;

private void addWithGroovyScriptEngine(int x, int y) throws IllegalAccessException,
  InstantiationException, ResourceException, ScriptException {
    Class<GroovyObject> calcClass = engine.loadScriptByName("CalcMath.groovy");
    GroovyObject calc = calcClass.newInstance();
    Object result = calc.invokeMethod("calcSum", new Object[] { x, y });
    LOG.info("Result of CalcMath.calcSum() method is {}", result);
}

public MyJointCompilationApp() {
    ...
    URL url = null;
    try {
        url = new File("src/main/groovy/com/baeldung/").toURI().toURL();
    } catch (MalformedURLException e) {
        LOG.error("Exception while creating url", e);
    }
    engine = new GroovyScriptEngine(new URL[] {url}, this.getClass().getClassLoader());
    engineFromFactory = new GroovyScriptEngineFactory().getScriptEngine(); 
}

Глядя внутрь метода loadScriptByName, мы сразу видим проверку isSourceNewer, где движок проверяет, действителен ли источник, находящийся в данный момент в кеше.

Каждый раз, когда наш файл изменяется, GroovyScriptEngine автоматически перезагружает этот конкретный файл и все классы, зависящие от него.

Хотя это удобная и мощная функция, она может вызвать очень опасный побочный эффект: многократная перезагрузка огромного количества файлов приведет к перегрузке процессора без предупреждения.

Если это произойдет, нам может потребоваться реализовать собственный механизм кэширования для решения этой проблемы.

6.4. GroovyScriptEngineFactory (JSR-223)

JSR-223 предоставляет стандартный API для вызова фреймворков сценариев, начиная с Java 6.

Реализация выглядит похожей, хотя мы возвращаемся к загрузке через полные пути к файлам:

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

private final ScriptEngine engineFromFactory;

private void addWithEngineFactory(int x, int y) throws IllegalAccessException, 
  InstantiationException, javax.script.ScriptException, FileNotFoundException {
    Class calcClas = (Class) engineFromFactory.eval(
      new FileReader(new File("src/main/groovy/com/baeldung/", "CalcMath.groovy")));
    GroovyObject calc = (GroovyObject) calcClas.newInstance();
    Object result = calc.invokeMethod("calcSum", new Object[] { x, y });
    LOG.info("Result of CalcMath.calcSum() method is {}", result);
}

public MyJointCompilationApp() {
    // ...
    engineFromFactory = new GroovyScriptEngineFactory().getScriptEngine();
}

7. Подводные камни динамической компиляции

Используя любой из описанных выше методов, мы могли бы создать приложение, которое считывает сценарии или классы из определенной папки вне нашего файла jar.

Это дало бы нам возможность добавлять новые функции во время работы системы (если только нам не требуется новый код в части Java), тем самым достигая своего рода разработки с непрерывной доставкой.

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

8. Подводные камни при запуске Groovy в проекте Java

8.1. Производительность

Все мы знаем, что когда системе требуется высокая производительность, необходимо следовать некоторым золотым правилам.

Два из них могут иметь большее значение для нашего проекта:

избегать отражения минимизировать количество инструкций байт-кода

    Отражение, в частности, является дорогостоящей операцией из-за процесса проверки класса, полей, методов , параметры метода и так далее.

Если мы анализируем вызовы методов из Java в Groovy, например, при выполнении примера addWithCompiledClasses, стек операций между .calcSum и первой строкой фактического метода Groovy выглядит так:

Что такое соответствует Java. То же самое происходит, когда мы приводим объект, возвращенный загрузчиком, и вызываем его метод.

calcSum:4, CalcScript (com.baeldung)
addWithCompiledClasses:43, MyJointCompilationApp (com.baeldung)
addWithStaticCompiledClasses:95, MyJointCompilationApp (com.baeldung)
main:117, App (com.baeldung)

Однако вот что делает вызов invokeMethod:

В этом случае мы можем оценить, что на самом деле стоит за мощью Groovy: метакласс.

calcSum:4, CalcScript (com.baeldung)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:101, CachedMethod (org.codehaus.groovy.reflection)
doMethodInvoke:323, MetaMethod (groovy.lang)
invokeMethod:1217, MetaClassImpl (groovy.lang)
invokeMethod:1041, MetaClassImpl (groovy.lang)
invokeMethod:821, MetaClassImpl (groovy.lang)
invokeMethod:44, GroovyObjectSupport (groovy.lang)
invokeMethod:77, Script (groovy.lang)
addWithGroovyShell:52, MyJointCompilationApp (com.baeldung)
addWithDynamicCompiledClasses:99, MyJointCompilationApp (com.baeldung)
main:118, MyJointCompilationApp (com.baeldung)

Метакласс определяет поведение любого заданного класса Groovy или Java, поэтому Groovy просматривает его всякий раз, когда требуется выполнить динамическую операцию, чтобы найти целевой метод или поле. После обнаружения стандартный поток отражения выполняет его.

Два золотых правила нарушены одним методом вызова!

«Если нам нужно работать с сотнями динамических файлов Groovy, то, как мы вызываем наши методы, будет иметь огромное значение в производительности нашей системы.

8.2. Метод или свойство не найдено

Как упоминалось ранее, если мы хотим развернуть новые версии файлов Groovy в жизненном цикле компакт-диска, нам нужно обращаться с ними так, как будто они представляют собой API, отдельный от нашей основной системы.

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

Примеры каждого из них: наличие конвейера CI и использование устаревшего метода вместо удаления.

Что произойдет, если мы этого не сделаем? Мы получаем ужасные исключения из-за отсутствующих методов и неправильного количества и типов аргументов.

И если мы думаем, что компиляция нас спасет, давайте посмотрим на метод calcSum2() наших скриптов Groovy:

Просматривая весь файл, мы сразу видим две проблемы: метод calcSum3() и переменная z нигде не определены.

// this method will fail in runtime
def calcSum2(x, y) {
    // DANGER! The variable "log" may be undefined
    log.info "Executing $x + $y"
    // DANGER! This method doesn't exist!
    calcSum3()
    // DANGER! The logged variable "z" is undefined!
    log.info("Logging an undefined variable: $z")
}

Тем не менее, скрипт компилируется успешно, без единого предупреждения, как статически в Maven, так и динамически в GroovyClassLoader.

Это не удастся, только когда мы попытаемся вызвать его.

Статическая компиляция Maven покажет ошибку только в том случае, если наш Java-код ссылается непосредственно на calcSum3() после приведения GroovyObject, как мы делаем в методе addWithCompiledClasses(), но это все еще неэффективно, если вместо этого мы используем отражение.

9. Заключение

В этой статье мы рассмотрели, как мы можем интегрировать Groovy в наше Java-приложение, рассмотрели различные методы интеграции и некоторые проблемы, с которыми мы можем столкнуться при использовании смешанных языков.

Как обычно, исходный код, использованный в примерах, можно найти на GitHub.

«