Java REPL вам не ScriptEngine +11




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

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

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

Критерии



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

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

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

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

JShell



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

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

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

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

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



Beanshell



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

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

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



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

Kotlin


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

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

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

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

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



Groovy



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

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

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



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

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


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

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




К сожалению, не доступен сервер mySQL