Java вместо Groovy

в 14:06, , рубрики: groovy, java, maven classloader, runtime comilation, scripting, Программирование, разработка

Вдруг оказывается, что в проекте нужны скрипты и возникает вопрос что лучше эволюция или революция?
Но даже попытка внедрить груви может провалиться в легаси проекте с консервативным коллективом. И руководство может найти еще десяток причин не пропустить груви в проект. Хоть groovy гораздо проще и ближе программисту знающему java, чем та же scala.

Java вместо Groovy - 1

Но даже в этом случае можно использовать динамически компилируемые скрипты в проекте. Научимся компилировать java код динамически в памяти и запускать его в jvm, использовать в нем динамически загружаемыме библиотеки из maven. Хотелось бы написать как можно меньше кода для этого и чтобы процесс использования был максимально прост. Да и еще бы не хотелось надеяться на доступность tools.jar нашей пограмме.

Предупреждая негодование со стороны Groovy специалистов, признаюсь что я и сам люблю и использую этот динамический язык программирования и внес свою скромную лепту в Groovy Grape. Не умаляя достоинств Groovy, все же попробуем применить java в той области где груви на jvm вне конкуренции — динамической компиляции, взаимодействии с существующим java кодом и динамическим импортом зависимостей (то что делает Grape).

О компиляции в Java. JSR 199

Стандарт JSR 199 — java Compiler API, существует довольно давно. Интерфейсы API присутствуют в java пакетах javax.tools.*. Но чтобы компилировать java код из памяти в память и потом запустить его, надо изрядно написать кода и побить в бубен. Реализация компилятора не идет в составе JRE и tools.jar нет в maven репозитариях.

Как писать меньше с JSR 199

Хотелось бы что-нибудь готовое, не велосипедить каждый раз и коллега подсказал проект Janino. Сам janino содержит свой компилятор подмножества java и хорошо подходит лишь для вычисления выраженией. Есть org.codehaus.janino:commons-compiler-jdk который использует JSR 199, но вот только сильно зависит от oracle/openjdk tools.jar. После вечера работы напильником на свет появился janino-commons-compiler-ecj (2,3 МБ) который включает в себя eclipse java compiler и доработанный под него commons-compiler-jdk. Он самодостаточен и позволяет компилировать и загружать код даже в JRE. Если же добавить к нему mvn-classloader, то в скриптах можно делать такую же магию с динамическими зависимостями, как и в Groovy Grape.

Для сравнения библиотека для динамического языка mvel2 (989 КБ) занимает всего в пару раз меньше места, но не позволяет делать такие простые вещи как реализация интерфейса, определение внутреннего и инстанцирование анонимного класса, отсутствует подобие конструкции try/catch/finally да и отладка скриптов на нем может показаться адом.

Пример

Для компиляции скрипта на java нужна только зависимость com.github.igor-suhorukov:janino-commons-compiler-ecj:1.0 и лишь 3 строчки кода:

        SimpleClassPathCompiler simpleCompiler = new SimpleClassPathCompiler(dependenciesUrls);
        simpleCompiler.cook(SCRIPT_NAME+".java", scriptSourceText);
        Class<?> clazz   = simpleCompiler.getClassLoader().loadClass(SCRIPT_NAME);

Чтобы не быть голословным, есть пример всего про что рассказываю на github. Для его запуска нам потребуется JVM которая поддерживает java8, так как в примере скрипта будет Stream API.

Итак, начнем:

git clone https://github.com/igor-suhorukov/janino-commons-compiler-ecj-example.git
cd janino-commons-compiler-ecj-example
mvn test

Для того чтобы импортировать класс PhantomJsDowloader из maven зависимости com.github.igor-suhorukov:phantomjs-runner:1.1 вызовем MavenClassLoader и создадим classpath компилятору на основе этого maven артефакта:

List<URL> urlsCollection = MavenClassLoader.usingCentralRepo().getArtifactUrlsCollection("com.github.igor-suhorukov:phantomjs-runner:1.1", null);
new SimpleClassPathCompiler(urlsCollection);

Далее привожу текст основной java программы, которая загружает зависимости из репозитария, компилирует скрипт и выполняет его.
janino-commons-compiler-ecj-example/src/test/java/org.codehaus.commons.compiler.jdk/SimpleClassPathCompilerTest.java

package org.codehaus.commons.compiler.jdk;

import com.github.igorsuhorukov.codehaus.plexus.util.IOUtil;
import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader;
import org.junit.Test;

import java.lang.reflect.Method;
import java.net.URL;
import java.util.List;

public class SimpleClassPathCompilerTest {

    @Test
    public void testClassloader() throws Exception {

        final String SCRIPT_NAME = "MyScript";
        List<URL> urlsCollection = MavenClassLoader.usingCentralRepo().getArtifactUrlsCollection("com.github.igor-suhorukov:phantomjs-runner:1.1", null);

        SimpleClassPathCompiler simpleCompiler = new SimpleClassPathCompiler(urlsCollection);
        simpleCompiler.setCompilerOptions("-8");
        simpleCompiler.setDebuggingInformation(true,true,true);

        String src = IOUtil.toString(getClass().getResourceAsStream(String.format("/%s.java", SCRIPT_NAME)));
        simpleCompiler.cook(SCRIPT_NAME+".java", src);

        Class<?> clazz   = simpleCompiler.getClassLoader().loadClass(SCRIPT_NAME);
        Method main = clazz.getMethod("main", String[].class);
        main.invoke(null, (Object) null);
    }

    public static void runIt(){
        System.out.println("DONE!");
    }
}

А это сам скрипт, который использует внешнюю библиотеку из maven и также вызывает метод runIt класса, который его скомпилировал.
janino-commons-compiler-ecj-example/src/test/resources/MyScript.java

import com.github.igorsuhorukov.phantomjs.PhantomJsDowloader;
import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader;
import org.codehaus.commons.compiler.jdk.SimpleClassPathCompilerTest;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MyScript{

    public static void main(String[] args)  throws Exception{

        class Wrapper{
            private String value;

            public Wrapper(String value) { this.value = value; }

            public String getValue() { return value; }
        }

        SimpleClassPathCompilerTest.runIt();

        List<String> res = Arrays.asList(new Wrapper("do"), new Wrapper("something"), new Wrapper("wrong")).stream().
                                            map(Wrapper::getValue).collect(Collectors.toList());
        System.out.println(String.join(" ",res));

        System.out.println("Classes from project classpath. For example "+MavenClassLoader.class.getName());

        System.out.println(PhantomJsDowloader.getPhantomJsPath());
    }
}

Для работы примера нужны следующие зависимости

  • com.github.igor-suhorukov:janino-commons-compiler-ecj:1.0 для компиляции java кода и динамической загрузки его.
  • com.github.igor-suhorukov:mvn-classloader:1.3 для динамической загрузки библиотек из maven и формирования classpath компилятора.
  • junit для теста.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.github.igor-suhorukov</groupId>
    <artifactId>janino-commons-compiler-ecj-example</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.github.igor-suhorukov</groupId>
            <artifactId>janino-commons-compiler-ecj</artifactId>
            <version>1.0</version>
            <exclusions><exclusion><groupId>*</groupId><artifactId>*</artifactId></exclusion></exclusions>
        </dependency>
        <dependency>
            <groupId>com.github.igor-suhorukov</groupId>
            <artifactId>mvn-classloader</artifactId>
            <version>1.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Debug скрипта на java

Отладка скрипта, как обычная отладка java программы. Расставляем точки останова и не забываем включить отладочную информацию при компиляции скрипта:

simpleCompiler.setDebuggingInformation(true,true,true);

Выводы

Мы научились компилировать java код из java программы, добавлением всего нескольких строчек. Также мы умеем включать в этот скрипт зависимости из maven репозитариев и проводить отладку кода в IDE.

Подход из статьи может заменить groovy для скриптов в проекте, если требования не позволяют использовать ничего кроме java или коллеги враждебно воспринимают груви и с этим ничего не получается сделать. Вы можете возразить про AST/метапрограммирование, что groovy впереди и будете правы, в java с этим не все просто. Про работу с AST java программы расказывал в статье "Разбор Java программы с помощью java программы.". Ситуацию с метропрограммированием попробуем решить в следующих публикациях.

Несмотря на то что статья описывает подход на «чистой» java и на выбор такого подхода в проекте могли повлиять политические мотивы, я считаю что лучше Java вместе с Groovy, чем «Java вместо Groovy».

Автор: igor_suhorukov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js