- PVSM.RU - https://www.pvsm.ru -

Java REPL вам не ScriptEngine

Java REPL вам не ScriptEngine - 1

Привет! Меня зовут Дима, я разработчик в команде “Архитектура” в hh.ru. Среди прочего, я занимаюсь тем, что делаю разработку проще для коллег. Выполнение кода в продакшене является типовой задачей. Поэтому когда я услышал, что с этим есть проблемы, я решил заняться их устранением.

Не всегда изменения данных можно сделать простым UPDATE/INSERT — иногда нужно задействовать валидацию, шины событий и прочее. В таких случаях самым оптимальным решением является выполнение произвольного кода прямо в приложении. У нас Java, поэтому когда появился JEP-222 [1], я сразу подумал, что JShell, возможно, сможет снова сделать написание скриптов удобным. Чуда не произошло, а потому под катом вы найдете не очень глубокое сравнение самых известных интерпретаторов Java (и «около-Java») для jvm с примерами. Всех интересующихся приглашаю под кат.

Для запуска скриптов мы используем BeanShell [2], и для 2019-го он ужасен: последний релиз от 2016 года [3], отсутствие поддержки лямбд и даже дженериков — все это заставляет писать код, который никто не писал со времен Java 1.4 [4].

Критерии

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

  1. поддержка актуального java синтаксиса;
  2. возможность передать в интерпретатор внешний контекст;
  3. возможность прервать выполнение;
  4. возможность перенаправить I/O;
  5. информативная обратная связь.

Чем больше язык, на котором мы пишем скрипты, напоминает тот, который мы разрабатываем, тем меньше ошибок — руки помнят. Но когда мы допускаем ошибки, которые были выявлены на этапе компиляции, они должны позволить разработчику их пофиксить — это указания на имена отсутствующих переменных, строчки, стейктрейсы etc.
Далее скрипты должны работать в определенном контексте, с доступом к Spring-овому контексту, к логгеру, который будет обслуживать именно скрипты. Без такой возможности передачи контекста, его получение превращается в квест.
Если ошибка все же просочилась в рантайм, то рестартить весь инстанс, чтобы остановить выполнение, — плохая идея, поэтому нужно иметь возможность просто прервать выполнение скрипта в произвольный момент времени.
И последнее — любые сообщения в системный вывод в процессе работы скрипта имеют смысл только в контексте этого скрипта. В системных логах от такого вывода толку мало. Поэтому хочется иметь возможность эти сообщения перенаправить в ответ.

Итак, поехали

JShell [5]

  • поддержка актуального java синтаксиса — да
  • возможность передать контекст — нет
  • возможность прервать выполнение — да
  • возможность перенаправить I/O — нет
  • информативная обратная связь — да

Сразу скажу, JEP-222 [1] не преследует своей целью создать встраиваемый интерпретатор — его цель именно REPL, то есть, возможность быстрого прототипирования кода. Это чревато рядом последствий.

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

Во-вторых, REPL вполне может исполняться не локально, а где-то на удаленной машине, поэтому API сделан с учетом таких особенностей. Я думаю, это основная причина, по которой в API нет возможности передать в интерпретатор внешний контекст и перенаправить I/O.
Кроме того, возникают разные режимы запуска — удаленный [6], когда shell подключается к машине по JDI, и локальный [7]. Так как передать контекст программно возможности нет, а нам все равно очень хочется, то надежда остается только на локальный режим и на то, что мы умеем пользоваться кодогенерацией [8]
Но, к сожалению, локальный режим явно не задумывался как основной — вот такой скрипт [9] вызывает дедлок [10] на компиляторе. При том, что этот же код в режиме JDI работает без проблем.

Таким образом, от использования JShell пришлось отказаться, хотя в целом API странноват, но понятен — отдаем скрипт на вход, получаем поток событий, для каждого из них можно проверить статус, получить ошибки и дебажную информацию. Ошибки позволяют идентифицировать выражение, в котором её допустили:

Java REPL вам не ScriptEngine - 2

Beanshell [11]

  • поддержка актуального java синтаксиса — нет
  • возможность передать контекст — да
  • возможность прервать выполнение — да
  • возможность перенаправить I/O — да(но требует использования спецметодов)
  • информативная обратная связь — да

Неудача заставила обратить внимание на то, что мы используем сейчас. После продолжительного перерыва, проект вроде бы ожил, и судя по roadmap'у [11], уверенно движется к релизу, который решит все наши проблемы — уже сейчас должно работать много фич.

На момент написания статьи в beanshell действительно появилась поддержка дженериков, но лямбды по-прежнему не работают. Возможно, к выходу релиза ситуация изменится.
Зато в плане интеграции движок вполне дружелюбен — поддержка стандартного javax.scripting, ошибки выполнения достаточно вербозны:

Java REPL вам не ScriptEngine - 3

Тем не менее, использование стримов без лямбд — это ад, который горит в аду. Возможно, проще даже писать на другом языке. Поэтому я решил присмотреться к сегменту «около-java». И первый кандидат на роль скриптового интерпретатора тут, конечно же

Kotlin [12]

  • поддержка актуального java синтаксиса — нет
  • возможность передать контекст — нет
  • возможность прервать выполнение — да
  • возможность перенаправить I/O — нет
  • информативная обратная связь — да

Java-код, если очень повезет, будет валидным kotlin-кодом. Но запустить что-то хоть сколько-нибудь адекватное на java в kotlin у меня не вышло, но тем не менее давайте попробуем.
Котлин уже пару лет как анонсировал поддержку javax.scripting [13].

Первая проблема, с которой приходится столкнуться, — это dependency-hell.
Kotlin-compiler включает в себя классы org.jdom, которые стали драться с org.jdom в приложении и заверте… Итак, у нас есть kotlin-compiler-embeddable, где все эти классы переложены в кастомные пакеты.

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

Ошибки же тоже вполне вербозны:

Java REPL вам не ScriptEngine - 4

Groovy [14]

  • поддержка актуального java синтаксиса — нет, но есть аналоги
  • возможность передать контекст — да
  • возможность прервать выполнение — да
  • возможность перенаправить I/O — да
  • информативная обратная связь — да

Груви, помимо поддержки javax.scripting, предоставляет свой, более расширенный API для интеграции интерпретатора. Например, есть возможность передать AST-трансформацию, которая позволяет добавить условное прерывание после каждого выражения [15]. Штука такая мощная, что аж страшно [16].

Более того, Java-(а особенно beanshell)-код может быть вполне валидным груви-кодом.
Интеграция и тестовая эксплуатация прошла успешно, за исключением инициализации листов и синтаксиса лямбд (их приходится заворачивать в фигурные скобки), существующие биншелл-скрипты отработали без проблем. Ошибки более чем вербозны:

Java REPL вам не ScriptEngine - 5

Пожалуй, на сегодняшний день это единственный интерпретатор, который, с одной стороны, позволяет писать код образца 2019 года, а с другой — соответствует всем требованиям, которые разумно предъявить к интерпретатору.

Какие мы можем сделать выводы?

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

Второй — на сегодняшний день нет ни одного инструмента для исполнения скриптов на Java, поэтому если вам требуется такой инструмент, будьте готовы осваивать новый синтаксис.

Автор: Jorixxx

Источник [17]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/324934

Ссылки в тексте:

[1] JEP-222: https://openjdk.java.net/jeps/222

[2] BeanShell: https://github.com/beanshell/beanshell

[3] 2016 года: https://github.com/beanshell/beanshell/releases/tag/2.0b6

[4] 1.4: https://jcp.org/en/jsr/detail?id=14

[5] JShell: https://github.com/dzharikhin/java-interpreter-comparison/blob/master/jshell-example/src/main/java/org/jrx/interpreter/jshell/JShellExample.java

[6] удаленный: https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.jshell/share/classes/jdk/jshell/execution/JdiExecutionControlProvider.java

[7] локальный: https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/jdk.jshell/share/classes/jdk/jshell/execution/LocalExecutionControlProvider.java

[8] кодогенерацией: https://github.com/dzharikhin/java-interpreter-comparison/blob/master/jshell-example/src/main/java/org/jrx/interpreter/jshell/JShellExample.java#L141-L154

[9] вот такой скрипт: https://github.com/dzharikhin/java-interpreter-comparison/blob/master/jshell-example/src/main/resources/Script.java

[10] дедлок: https://github.com/dzharikhin/java-interpreter-comparison/blob/master/jshell-example/src/main/java/org/jrx/interpreter/jshell/JShellExample.java#L72

[11] Beanshell: https://github.com/dzharikhin/java-interpreter-comparison/blob/master/beanshell-example-parent/beanshell-example/src/main/java/org/jrx/interpreter/beanshell/BeanshellExample.java

[12] Kotlin: https://github.com/dzharikhin/java-interpreter-comparison/blob/master/kotlin-example/src/main/java/org/jrx/interpreter/kotlin/KotlinInterpreterExample.java

[13] анонсировал поддержку javax.scripting: https://discuss.kotlinlang.org/t/embedding-kotlin-as-scripting-language-in-java-apps/2211

[14] Groovy: https://github.com/dzharikhin/java-interpreter-comparison/tree/master/groovy-example/src/main/java/org/jrx/interpreter/groovy

[15] условное прерывание после каждого выражения: https://github.com/dzharikhin/java-interpreter-comparison/blob/master/groovy-example/src/main/java/org/jrx/interpreter/groovy/GroovyNativeApiExample.java#L49-L54

[16] страшно: https://github.com/dzharikhin/java-interpreter-comparison/blob/master/groovy-example/src/main/java/org/jrx/interpreter/groovy/GroovyNativeApiExample.java#L71-L97

[17] Источник: https://habr.com/ru/post/461027/?utm_campaign=461027&utm_source=habrahabr&utm_medium=rss