Привет, хабр! Я думаю, что зачастую вы видели задачи, которые могли бы быть с легкостью решены, если генерировать и выполнять код сразу в рантайме. Этот подход может облегчить жизнь, когда есть желание оптимизировать место с помощью кодогенерации. Однако из-за того, что вы поставляете библиотеку, готовый код станет вам известен только при запуске приложения (чтобы процедура сборки не усложнялась). В этой статье я хочу показать способ генерировать код в процессе выполнения программы, а потом сразу же запускать его. По сути, мы будем использовать компилятор Kotlin для jsr233. В качестве задачи можно взять относительно популярную вещь — мы сделаем некоторое подобие AOP для разбора ответов от базы данных. Таким образом, программист сможет разметить атрибутами свой код, а мы сгенерим и скомпилируем рабочий оптимизированный код сразу в runtime. Однако на самом деле, область применения подобной тактики намного шире: можно делать программируемую конфигурацию, можно оптимизировать существующий код (за счет замены условий на заранее подсчитанные значения, которые уже не изменятся). Ну и, конечно же, можно избежать копипаста даже в тех случаях, когда выразительности языка недостаточно, чтобы выделить обобщенный кусок кода. Весь код доступен на GitHub, для запуска необходима Java 11. В статье я привожу сокращенные варианты кода, без логов, диагностики и т.д. Прежде всего: если вы хотите решить именно эту задачу самостоятельно у себя в проекте, то это будет, скорее всего, велосипедом. Сначала лучше всего присмотреться к Hibernate или Spring Data, которые уже умеют всё это делать. Что хочется получить: возможность обвесить класс аттрибутами таким образом, чтобы «некий преобразователь результата из SQL в наш класс» смог бы вытащить результаты из запроса. Как пример таких аттрибутов: Как известно, чтобы разобрать ответ из базы, в Spring JDBC рекомендуется использовать ResultSet. Если вытаскивать строку из колонки, в интерфейсе есть как минимум два метода: А потому усложним задачу: Как я уже говорил, хотелось бы выделить решение в библиотеку, а значит в момент компиляции мы не можем знать обо всех классах. А хотелось бы делать разбор ответа из базы в стиле: Еще раз напоминаю: не стоит это решение сразу использовать в своем проекте. Даже если у вас есть задача разбора значений из базы, даже если есть желание работать напрямую с Spring JDBC, полученного результата можно добиться без кодогенерации, выразительности языков Java/Kotlin полностью хватает для этого. Домашнее задание — по-быстрому представить, как это сделать (естественно, без reflection и т.д.). Сейчас есть два наиболее простых способа скомпилировать Kotlin в процессе выполнения: это использовать напрямую Kotlin Compiler или же воспользоваться оберткой для jsr233. Первый способ позволяет компилировать сразу несколько файлов, имеет больше возможностей для расширения, однако он немного сложнее. Второй способ позволяет просто выполнить один файл, добавив типов в текущий Class Loader. Очевидно, что он не до конца безопасный, то есть запускать необходимо только доверенный код (кстати, Kotlin Script Compiler наоборот запускает код в отдельном Class Loader'е, правда, при настройках по-умолчанию ему никто не мешает запустить подпроцесс с произвольным кодом). Перво-наперво определимся с интерфейсом. Не стоит генерировать новый код на каждый Sql запрос, поэтому сгенерируем его для каждого объекта, который мы будем читать из базы. Как вариант, интерфейс может быть: inline метод необходим для того, чтобы иметь возможность работать с методом так, как будто это настоящий generic. С помощью такого дополнения создавать ResultSetMapper можно в виде кода: return mapperFactory.createMapper<MyClass>(). ResultSetMapper наследует стандартный интерфейс из Spring: Реализация фабрики отвечает за то, чтобы по аннотациям класса сгенерировать код, а потом отправить его в компилятор. Получаем исходник следующего рода: Нам необходимо вернуть ResultSetMapper<TMappingType>. Желательно, чтобы результирующий тип был не generic'ом и содержал все необходимые типы. Тогда у JVM будет больше знаний о коде, а значит и больше возможных оптимизаций. И, следовательно, компилировать мы будет код вида: Для реализации компилятора нам необходимы три вещи: Для первого пункта добавляем следующие зависимости в gradle: Для второго пункта необходимо добавить файл "src/main/resources/META-INF/services/javax.script.ScriptEngineFactory" cо следующим содержимым: Теперь остался последний пункт — запускать скрипт в runtime: Как я описал выше, мы хотим усложнить себе задачу и генерировать код не только для примитивных типов, но и для самописных. А значит, сначала лучше разобраться с моделью данных. Представим, что мы стараемся писать строго типизированный код, который постарается не допускать некорректных данных как можно раньше. Следовательно: Как один из способов, реализовать подобное поведение можно следующим образом: Создаем класс NonEmptyText, который содержит необходимое поле и проверки в конструкторе: Создаем также способ создания этого класса: С помощью этих примитивов мы можем приступить к классу UserName: Итак, здесь у нас есть UserName, который строго типизирован. И заодно companion object содержит в себе еще способ создания экземпляров этого типа, т.е. кроме конструктора, создать UserName можно теперь как: Теперь мы можем передавать конструктор типа в другие функции, то есть для метода вида fun <TValue> createText(input: String?, constructor: NonEmptyTextConstructor<TValue>): TValue? вызов будет createText("123", UserName), что довольно интуитивно. В некотором смысле, это похоже на type classes, однако с учетом возможностей JVM. Email мы определим следующим образом: Разделение его на два типа довольно искусственно, чаще всего в реальной жизни это не имеет смысла. В статье основной смысл подобного в том, чтобы читать из базы данных один объект (то есть — электронную почту), который хранится в двух колонках. Далеко не каждый ORM такое умеет. Далее создаем тип пользователя. Именно его мы будем получать из базы данных: Для того, чтобы сгенерировать код работы с базой данных, необходимо: Если у нас есть соотношение «одна колонка — один тип», то чтение данных из базы можно выразить в виде простого интерфейса: Таким образом, в процессе разбора результата запроса можно сделать следующее: Как я уже говорил выше, допустим, что в проекте много типов, которые представляются как «непустая строка». А значит для них можно сделать общий mapper: Как видно, мы как бы передали конструктор внутрь этого класса. Дальше мы можем сравнительно легко и просто штамповать уже mapper'ы для конкретных классов: К сожалению, в Kotlin я не придумал, как выразить mapper в виде метода-расширения, то есть extension type. В Scala этого можно добиться с помощью implicit, хоть они и не самые явные. Однако, как я уже говорил выше, у нас есть сложный тип — Email. И он требует две колонки. А значит интерфейс выше для него не подходит. Как вариант — можно создать еще один: Здесь на вход приходят две колонки, однако результирующий объект только один. Это как раз то, что нам и надо, хотя таких интерфейсов придется нагенерить на каждую разную численность параметров. Теперь можно сделать комбинированный mapper, который будет что-то вроде: И далее Email можно читать следующим образом: Осталось только сделать самую малость — определиться с аннотациями и сделать генерацию кода: Сначала определимся, какой код мы хотим видеть. Я пришёл к следующему, который соответствует всем изначальным критериям: Так как код генерится монолитом (переменные сначала определяются, а потом используются), разобьем на блоки хотя бы идею: В итоге получается код вида: Как видно, генерить код для выполнения сразу в runtime не просто, а очень просто. На создание проекта у меня ушло всего лишь несколько часов (плюс на статью еще пара), однако у нас уже вполне рабочий код, который читает данные из базы достаточно эффективно. Более того, за счет того, что мы контролируем кодогенерацию, мы можем добавить интернирование объектов, замеры производительности и еще море улучшений и оптимизаций. И, что самое главное, мы всегда знаем результирующий код, который в итоге выполнится. Kotlin DSL также может быть применен для программируемой конфигурации: если вы достаточно любите своих пользователей, чтобы не заставлять их делать конфигурации в виде json/xml/yaml, вы можете дать DSL, в котором и будете определять конфигурацию. Посмотрите, например, на TeamCity Build DSL — вы можете натурально запрограммировать билд, сделать условие/цикл (чтобы не копировать 10 раз один и тот же шаг), у вас есть подсказки в IDE. Ведь в программе всё равно нужна только модель конфигурации, и далеко не важно, как она создавалась. Не все задумки можно выразить в самом языке. И часто не хочется заниматься копипастом, который к тому же трудно проверить. И здесь тоже нам на помощь может прийти генерация кода: если реализацию можно выразить с помощью аннотаций/конфигурации, так почему бы её не сделать в общем виде, скрыв за интерфейсом? Подобный подход будет удобен и для JIT, который получит код со всеми готовыми типами, вместо обобщенного, где нет возможностей сделать оптимизации из-за недостатка знаний о вызовах. Однако самое важное я уже написал в начале статьи: несмотря на все возможные преимущества, пожалуйста, сначала оцените, насколько будет действительно полезно решение с генерацией и выполнением кода в runtime. Вполне может так получиться, что код с reflection работает с достаточной производительностью, а значит нет никакого смысла использовать не самые популярные техники и усложнять проект. Ссылки: User