Господа разработчики java приложений. Сегодня вашему вниманию представляется простой способ использования code evaluation, реализация которого позволит исполнять произвольный код в работающем приложении, что в свою очередь позволит сэкономить массу времени на CI/CD.

Зачем мне это нужно?

Если вам приходится разрабатывать в условиях микросервисной архитектуры, особенно в большой компании, то скорее всего вам знакома ситуация, когда посмотреть как "по-боевому" работает приложение можно только на стендах на которых есть интеграции с другими микросервисами. И поскольку далеко не все вещи можно проверить локально на заглушках, для того чтобы проверить ту или иную гипотезу, приходится пушить новый код (не факт что работающий корректно) в репозиторий... прогонять его через CI/CD... и только потом по логам понять что где-то что-то пошло не так. И хорошо если логи сразу покажут в чём вы ошиблись, потому как иначе этот процесс пуша и прогона по пайплайнам может стать вашим круговоротом сансары.

Исполнение динамически введённого кода поможет решить эту проблему.

Как это работает?

Как мы с вами знаем, groovy это полностью совместимый с Java язык программирования с динамической компиляцией. Эти две особенности groovy и помогут нам в том, что бы реализовать code evaluation для java приложений. О том как добавить поддержку groovy в java проект вы сможете легко найти сами. А я приведу пример, как реализовать code evaluation (в некотором смысле аналогичный тому, который вы можете видеть во время дебага в вашей IDE).

1) Создадим groovy класс, а в нём шаблонную строку в которую поместим класс и плейсхолдер для динамически введённого кода. Пример такой строки:

def EXPRESSION_CLASS_TEMPLATE = """
package dev.toliyansky.eval.service
class ExpressionClass implements java.util.function.Supplier<Object> {
	def get() {
		%s
	}
}
"""

Примечание: package должен быть такой же как и у класса в котором вы этот код будете писать.

Почему мы имплементируем Supplier будет описано ниже.

2) Динамически скомпилируем и загрузим этот класс.

Кусок кода ниже может быть размещён например в REST контроллере, который получает code как тело запроса.

def finalClassCode = String.format(EXPRESSION_CLASS_TEMPLATE, code)
def supplier = groovyClassLoader.parseClass(finalClassCode)
                                .getDeclaredConstructor()
                                .newInstance() as Supplier<Object>
def result = supplier.get()

В первой строчке заменяем %s на код который хотим динамически ввести и исполнить в рантайме.

Во второй строке компилируем и загружаем класс из строки, которую получили на предыдущем шаге. Тут важно заметить что созданный инстанс класса приводим к Supplier для того, чтобы далее в третьей строчке можно было сделать вызов метода в котором должен исполниться динамически введённый код. Supplier<Object> в данном случае идеально нам подходит.

Вот собственно и всё.

Пример использования code evaluation

Допустим у вас web приложение в kubernetes. Вы написали какую-то новую фичу, закоммитились, прошли код ревью, прогнали код через CI/CD и вот POD с вашим приложением наконец поднимается, вы заходите в логи и видите что фича работает не так как ожидалось. Допустим, для примера, что вы забыли в конструкторе что-то проинициализировать и поэтому остальная бизнес логика не отрабатывает с банальным NullPointerException.

Имея в своём арсенале HTTP роут с исполнением динамического кода, можно спокойно обратиться к applicationContext, вытащить нужный бин и ручками проинициализировать забытую переменную. После чего потыкать в инициирование отработки бизнес кода и проверить результат, минуя весь CI/CD. Таким банальным примером всё не ограничивается. При желании можно даже в райнтайме переопределить метод класса с бизнес логикой и играться до тех пор пока не отладите код и потом уже зная как оно себя ведёт коммититься и прогонять пайплайны.

Готовое решение для web приложений на spring boot

Если вы не хотите возиться с добавлением groovy в ваш java проект и писать все эти контроллеры, обёртки, разбираться с динамикой, а просто хотите добавить в одну строчку зависимость в ваш проект и чтобы работало из коробки, то тогда специально для вас презентую маленький проект который всё это умеет - evaluator-spring-boot-starter

Это, как можно догадаться из названия, spring boot starter. Подключение данного стартера добавляет в ваше web приложение роут http://host:port/eval отдающий WEB-UI, в котором можно ввести код который хотите динамически исполнить, а результат выведется в окне рядом. Всё это приправлено подсветкой синтаксиса, и прочими удобствами. Если же, например, ваше приложение не имеет выходного роута из кластера и вы можете только лишь использовать curl или wget из терминала POD, то тогда можно использовать роут http://host:port/eval/groovy и отправлять код как параметр GET запроса или как тело POST запроса.

Впрочем, всё это более-менее подробно описано в readme проекта.

Скриншот WEB-UI evaluator-spring-boot-starter
Скриншот WEB-UI evaluator-spring-boot-starter

В итоге

  • Продемонстрировано как code evaluation может сэкономить время на отладке приложения

  • Продемонстрировано как реализовать code evaluation в java проекте

  • Продемонстрировано готовое решение в виде spring boot starter.