Привет, Хабр! Меня зовут Дима, я разработчик в команде “Архитектура” в 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, поэтому если вам требуется такой инструмент, будьте готовы осваивать новый синтаксис.

Комментарии (12)


  1. Dr_XaoS
    24.07.2019 18:10

    Мы для удаленной консоли взяли CRaSH и дописали туда пару своих команд. Выглядит так: заходишь по ssh в приложение, запускаешь команду выполнения произвольного кода, вставляешь код на groovy, получаешь ответ.


    Скриншот


  1. usharik
    24.07.2019 18:28

    Код на Java, а для скриптов Python, Bash или иногда Groovy. До настоящего момента у меня все было примерно так. Не могу сказать, что я сильно этим не доволен и мне срочно нужен интерпретатор Java.


    1. bm13kk
      25.07.2019 11:53

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

      То есть перед началом выполнения скрипта нужно поднять (к примеру) DI приложения и извлечь нужные сервисы (сбилженые и запущеные).


  1. Beholder
    24.07.2019 19:18

    А не проще будет вместо всех этих мучений код скомпилировать и загрузить класс-файлы (возможно в jar)?


    1. Jorixxx Автор
      24.07.2019 20:45

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


      1. usharik
        24.07.2019 21:18

        М.б. какой-нибудь свой classloader написать для этого?..


        1. Jorixxx Автор
          25.07.2019 12:31

          classloader, который делает что?
          загрузить байтики в класс не проблема — вопрос откуда они возьмутся и что это будут за байтики
          Предлагаю просто прикинуть скольким критериям удовлетворяет выполнение скомпиленных классов — как скомпилить класс, чтобы перенаправить I/O, чтобы можно было снаружи в него контекст передать…


  1. sshikov
    24.07.2019 19:52

    Groovy один из самых адекватных? И почему я ни разу не удивлен?

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


  1. grinCo
    24.07.2019 23:29
    -1

    Я так и не понял, для чего код руками в проде нужно запускать?


    1. Jorixxx Автор
      25.07.2019 12:35

      Типичные кейсы:

      • нестандартное обращение пользователя в поддержку: ради одного случая писать боевой код — не хочется, а помочь человеку — хочется
      • ускорение запуска функционала: админка для фичи еще не готова, а сервисный слой уже вполне работает. можем сегодня запускать фичу в script-driven режиме, а завтра доделаем админку


      1. grinCo
        25.07.2019 19:23

        Спасибо за ответ.

        Я к тому что это все очень небезопасно выглядит. Вы по сути запускаете нетестированный код на проде, я подозреваю что в CVS тоже скрипт не добавляется до запуска.

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


        1. Jorixxx Автор
          26.07.2019 14:44

          Да, это угроза безопасности, но т.к. мы об этом знаем, то все не так страшно.
          Понятно, что функционал закрыт разрешениями и доступен только нескольким людям.
          А что до процесса, то ревьюить, тестировать и складывать скрипты в VCS — можно и нужно, так же как и весь остальной код.
          Просто тут все это работает на доверии. В кейсах, которые я описал, польза окупает этот риск