company_banner

Java REPL вам не ScriptEngine



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



    UPDATE: Запилили ScriptEngine из JShell. Но для этого пришлось написать свой режим запуска

    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, поэтому если вам требуется такой инструмент, будьте готовы осваивать новый синтаксис.
    HeadHunter
    HR Digital

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

      0

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


      Скриншот

        0
        Код на Java, а для скриптов Python, Bash или иногда Groovy. До настоящего момента у меня все было примерно так. Не могу сказать, что я сильно этим не доволен и мне срочно нужен интерпретатор Java.
          +1
          На сколько я понимаю проблему автора, Java нужна не потому что хочется на ней писать. А потому что для скрипта нужна функциональность из самого приложения.

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

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

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

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

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

            Впрочем, автор сознательно себя ограничивает одинаковым или похожим синтаксисом, в то время как часто (но не обязательно всегда) синтаксис удобно иметь как раз другой. Кложа, условно.
              –1
              Я так и не понял, для чего код руками в проде нужно запускать?
                0
                Типичные кейсы:
                • нестандартное обращение пользователя в поддержку: ради одного случая писать боевой код — не хочется, а помочь человеку — хочется
                • ускорение запуска функционала: админка для фичи еще не готова, а сервисный слой уже вполне работает. можем сегодня запускать фичу в script-driven режиме, а завтра доделаем админку
                  +3
                  Спасибо за ответ.

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

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

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

              Самое читаемое