Используем Xtend для прикладной кодогенерации: сеанс чёрной магии с разоблачением

Привет Хабр! Меня зовут Когунь Андрей. В КРОК я руковожу группой разработчиков Java (у нас большая распределённая по всей стране команда). Ещё я провожу встречи московского сообщества Java разработчиков JUG.MSK. Делаю это исключительно в корыстных целях: фотографируюсь там со всеми докладчиками, и однажды открою галерею с самыми интересными людьми в мире Java-разработки. Также помогаю делать конференции для разработчиков: JPoint, Joker и DevOops — в качестве члена программного комитета. Ну и для души, так сказать, преподаю Java-технологии студентам.

В КРОК мы с коллегами в основном занимаемся заказной разработкой. Одно из наших направлений — так называемые учётные системы. Их надо делать по возможности быстро. Они типовые, различия обычно наблюдаются только в доменной модели. Поэтому мы постоянно боремся за то, чтобы писать меньше бойлерплейт-кода, будь то тривиальные геттеры-сеттеры, конструкторы и т.п. или CRUD-репозитории и контроллеры. Мы для этого активно пользуем кодогенерацию.

Про неё сейчас и расскажу: покажу как взять кодогенерацию под контроль, чтобы она стала реально полезным инструментом разработчика. Попутно расскажу про технологию Xtend и покажу практические приёмы как с ней можно работать. Покажу как создавать собственные активные аннотации, писать и отлаживать код процессора аннотаций.

j5ctbvwuborkuqr5uhxec-kspqe.jpeg

За много лет работы с Java мы перепробовали много чего интересного:


  • поддержка генерации в IDE,
  • генерация байт-кода при помощи Lombok,
  • процессоры аннотаций, порождающие новый код,
  • фреймворки, позволяющие по описанию модели получить готовое (почти) приложение,
  • и много чего ещё, в том числе новые и не очень JVM-языки, которые позволяют писать более лаконичный код и реализовывать DSL для решения прикладных задач.

В какой-то момент, проанализировав сильные и слабые стороны всех этих подходов, их ограничения и практическую применимость, мы пришли к тому, что в нашем собственном фреймворке для быстрой разработки (jXFW) будем использовать Xtend. Использовать для кодогенерации исходного Java-кода по доменной модели и для аккумулирования того опыта, который мы накопили в работе с различными технологиями. Сейчас расскажу, как в jXFW это всё работает и покажу, как вы можете сделать то же самое для своих нужд. Причём первую версию вы сможете изготовить буквально за пару дней и дальше начать применять подход «know-how как код».

Рассказывать буду на примере упрощённого демо-проекта, который был реализован в рамках доклада на JPoint.

Ремарка: чего в статье не будет:


  1. Я не хочу, чтобы мы в итоге делали выводы про то, что «технология А» лучше «технологи Б». Или что там Eclipse лучше IDEA или наоборот. Поэтому я не буду напрямую сравнивать какие-то языки, технологии. Всё что упоминаю, это лишь для того, чтобы какую-то аналогию объяснить на понятных примерах.
  2. Я не буду делать введение в Spring и Spring Boot. Исхожу из того, что вы имеете хотя бы какой-то опыт работы с этими технологиями. Мне кажется, сейчас сложно найти джависта, который не работал с ними. Но если вы вдруг слышите о Spring и Spring Boot впервые, вам срочно надо посмотреть доклады и тренинги Евгения Борисова и Кирилла Толкачева, — там мои коллеги рассказали об этих технологиях очень подробно.
  3. Не буду очень сильно погружаться в Xtend. Но поскольку, как показывает мой опыт выступления на Java-конференциях, эта технология мало кем используется, сделаю небольшой ликбез. Чтобы вы уже дальше могли для себя решить, нужен вам Xtend или нет.

Xtend — это статически типизированный язык программирования, приемник Xpand, построенный с использованием Xtext и компилирующийся в исходный код Java. Технология Xtext нужна для того, чтобы реализовывать всевозможные DSL. По сути, Xtend это такой своеобразный DSL.

Xtend совсем не новый язык программирования. Его создали ещё в 2011, примерно тогда же, когда появлялось большинство JVM-языков. Интересно, что у Xtend был слоган: «Java 10 сегодня!» Да, сегодня Java 10 у нас уже есть, слоган морально устарел. Но, похоже, люди что-то знали про Java, когда создавали Xtend, и некоторые фичи, заложенные в Xtend, они вот как раз прямо в Java 10 и появились. В частности, вывод типа локальной переменной (var). Но есть в Xtend и такие фичи, которых у Java пока ещё нет:


  • активные аннотации,
  • шаблонные выражения,
  • Switch Expressions.

Теперь расскажу про кодогенерацию в нашем фреймворке jXFW, чтобы наглядно показать, чем она полезна. Расскажу на примере простейшего приложения.

Запускаю Eclipse.

ce4fvwym9batnmvlwim19aikmfo.png

Как видите, здесь практически ничего нет. Только application.java (конфигурация для Spring Boot) и собственно исходник на Xtend, — в нём реализована доменная модель.

1ouixdl_3n5kaotkgue4v1awrsc.png

Как видите, Xtend-исходник очень похож на Java. Здесь нет ничего особенного. Просто класс с полями и несколько аннотаций. А что в итоге? jXFW генерирует два приложения (см. рисунок ниже): одно выполняется на сервере (тот самый Spring Boot) и даёт нам апишечку, а другое — на клиенте.

molgrsqydjeu2as66fc6laq-37y.png

Если мы что-нибудь введём в клиентской части (например, как зовут спикера) и сохраним…

xmzsvjjgabk1ydhqhun4njw0yy8.png

…то получим соответствующую запись и на клиенте, и на сервере.

_xklooeqt9mkgb0mxsazhhi5hwm.png

То есть всё по-честному.

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

Что за магия здесь под капотом? И как в ней замешан Xtend? Рассказываю. У нас есть класс, на нём проставлены аннотации, вернее активные аннотации. Вся магия скрывается в них. Аннотации в Xtend очень похожи на аннотации в Java. Просто в Xtend для них есть отдельное ключевое слово: annotation.

l5oikothbxlrtbfsyel6faxtgks.png

Активной аннотация становятся, если её, в свою очередь, пометить другой аннотацией: @Active, а в ней указать класс процессора, который активируется, когда эта аннотация поставлена над каким-то элементом.

yxyiithseabxu2c2xllpve9oqr4.png

Дальше всё как обычно.

Xtend из коробки имеет некоторое количество таких аннотаций.

sk6z_ogipkvjk4-yu_pghqpxvh0.png

Идея здесь примерно такая же, что и в библиотечке Lombok: пишешь только необходимый текст, ставишь аннотацию, и в итоге получаешь полный код.

Если вдруг вам ещё пока не понятно, о чём тут идёт речь, то теперь я буду рассказывать про кодогенерацию медленно и подробно. Результат, как уже сказал, доступен тут.

Открываем проект jp-boot-xtend-demo. Я его получил при помощи Spring Initializr.

haoovb1jqcgspfl99mu5f0pcgks.png

Дополнительных зависимостей здесь практически нет (см. файл pom.xml). Есть только spring-boot-starter-data-jpa и spring-boot-starter-data-rest. Плюс, подключен модуль jp-boot-xtend-demo-compile, в котором реализована наша активная аннотация. Если вам доводилось работать с процессорами аннотаций, вы наверно в курсе, что сам процессор определяется в отдельном модуле. Xtend в этом смысле не исключение.

И уже здесь, в jp-boot-xtend-demo-compile (см. файл pom.xml), мы подключаем все Xtend-зависимости, которые нам нужны: org.eclipse.xtend.lib, org.eclipse.xtend.lib.macro. Плюс, подключаем плагин xtend-maven-plugin. На случай если захотим тестировать наш Xtend-код, нам понадобится ещё несколько зависимостей: org.eclipse.xtend.core, org.eclipse.xtext.testing, org.eclipse.xtext.xbase.testing.

Кроме того, в Eclipse, я соответственно подключил плагин, который называется Xtend IDE. Актуальная инструкция как установить плагин — тут. Ещё один вариант: сразу взять дистрибутив, в котором этот плагин предустановлен — Eclipse for Java and DSL Developers.

Давайте смотреть как тут всё работает. Как и в случае с jXFW здесь есть приложение (см. файл DemoApplication.java), а также Java-класс, который будет нашей Entity, на базе которой мы будем всё строить (см. файл Country.xtend).

oxa2dzsckfafgiwtbmvikxqdb9q.png

При необходимости мы можем сразу посмотреть на то как выглядит Java-файл, сгенерированный из этого Xtend-исходника. Он нам сразу же доступен, и мы можем им пользоваться во всём остальном коде.

adptol4qu6lkif5vlr1epoudwpi.png

Например, в нашем DemoApplication есть кусок кода, который пытается вызывать метод setName. Но пока он красненький.

ncy7-8lr4tldanybkxqeyxb0vx4.png

Я добавляю в Xtend-исходник активную аннотацию @Accessors, и у меня в сгенерированном Java-коде автоматически появляются геттеры и сеттеры, в том числе setName.

dsnsosh-usuin1cxnzkvtwuuzbc.png

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

ufc9frdhf9hagk7qfz6fslvtmne.png

Тут я ещё вписал в Xtend-файл аннотации @ToString и @EqualsHashCode, и в итоге получил Java-исходник прямо такой, как и хотел.

Небольшой лайфхак, который избавит вас от необходимости после каждой правки Xtend-исходника отыскивать в target сгенерированный Java-файл. В Eclipse есть специальная оснастка: Generated Code. Что она делает? Встаньте на любую строчку в Xtend-исходнике, и увидите в окне Generated Code Java-код, который для неё сгенерирован. А оттуда при необходимости уже можете пойти непосредственно в Java-исходник. Вот такая удобная штука.

В принципе, всё хорошо работает. Но как только мы начинаем работать с кодогенерацией, тут же возникает вопрос: «А можно такой же, но только с перламутровыми пуговицами?» Так… Что бы я ещё хотел? Я бы хотел наверно, чтобы у меня сеттеры мои вызывались в цепочке — т.е. не просто устанавливалось значение, но ещё, чтобы и сам объект возвращался из этого сеттера, и я мог на нём следующий позвать.

«Из коробки» в Xtend такой аннотации нет. Поэтому нам придётся её делать ручками. И какие тут есть варианты?

В принципе, мы знаем, что существует аннотация @Accessors — мы посмотрим на её исходный код, увидим, что там есть Accessors Processor, специально написанный. И вот мы уже смотрим на Xtend-код и пытаемся понять, а в каком месте мы могли бы здесь что-то подкрутить, чтобы у нас работало так, как надо. Но это не очень продуктивный путь. Мы по нему не пойдём.

Мы будем писать полностью новую аннотацию. И вот почему. Дело в том, что в активных аннотациях, которые применяются в Xtend, есть возможность привязаться к конкретной фазе компиляции. Т.е. в тот момент, когда AST у нас уже есть, а исходных файлов ещё нет — мы можем как угодно этот наш AST менять. И это очень удобно.

Соответственно, вот эта наша аннотация (это я уже зашёл в проект jp-boot-xtend-demo-compile; см. файл EntityProcessor.xtend) @Active — она нам говорит про те самые четыре фазы, к которым мы можем привязываться. На каждой фазе работает свой собственный Participant-вариант, и мы можем реализовать тот, который нам нужен.

В принципе, есть базовый специальный класс —AbstractClassProcessor, в котором мы оверрайдим метод doTransform. В него нам будут приходить соответственно описания наших классов, помеченных этой аннотацией. И дальше мы, соответственно, что-то делаем.

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

ii4la3ifpwwfsftyzhdmhooazkk.png

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

Что здесь делает Xtend? У него есть вот эти самые шаблонные выражения. Мы ставим три одинарные кавычки, и дальше пишем то, что хотим получить на выходе. И при этом форматируем так, как нам удобно.

peboghjkrsn4ykntjn1wrgckzl4.png

А когда ставим фигурные кавычки, — и пользуем simpleName от нашего филда в данном случае, который по совместительству является аргументом, — то сюда подставляется нужное значение.

Код написан на Xtend. Мне кажется читать его, с одной стороны легко, потому что его мало. С другой стороны, он может мне быть понятен сходу. Например, почему мы позвали метод modifySetter, который я определил чуть ниже, и передали в него всего один аргумент?

Дело в том, что в Xtend есть такая вещь как Extension-методы. И у объекта того типа, которым является первый аргумент, можно этот Extension-метод позвать. Хорошо, а почему мы тогда его здесь не указали? Да потому что мы внутри лямбды, а в ней есть переменная it. Когда у нас есть переменная it, к лямбде можно обращаться, не указывая её. То же самое вот с it, который мы указали в качестве аргумента. Поэтому declaredFields-property у MutableClassDeclaration — мы зовём напрямую, безо всяких префиксов.

Это вот всё, что в принципе придётся знать про Xtend.

Давайте теперь посмотрим как это работает. Я определяю аннотацию @Entity. Затем иду вот в этот наш класс.

85oogcmd-ndwxrfqert7rp2d66c.png

Заменяю текущую @Entity с javax.persistence на свою — на активную аннотацию.

ymfmgw7jo06sbi_pnwbhvk5xohi.png

И вот теперь сеттер у нас такой как надо. Т.е. из Country возвращается this — мы возвращаемое значение поменяли с void на тип объекта, над которым стоит аннотация: @Id Long id.

Но, допустим, я хочу, чтобы айдишник сеттился немножко по-другому (всё к той же идее «хочу такое же, но с перламутровыми пуговицами»). Тогда я вписываю в свой класс setID. И оно даже отчасти работает. Ведь сеттер появился в нужном месте — сразу после id.

ksjojim1b5dbgattqi8dx8v4yaq.png

Вот только Eclipse мне здесь немножко подсвечивает и возмущается: «ты мне, вообще, о чём здесь говоришь?»

45dy8fdjxtilvf-kfttcxzwei-w.png

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

Поэтому нам надо внимательно следить за тем, чтобы таких казусов не возникало, — когда пишем что-то на Xtend. Как такое отследить? Например, можно у того Transformation-контекста, который сюда приходит, прописать метод isThePrimaryGeneratedJavaElement, и соответственно передать туда сеттер. Получается прямо в таком же стиле, как мы обычно пишем на Java.

p34lodtfkgnnpwxxpdkdbbpprce.png

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

splb1v25xlqb2p2imkbmgfn5--0.png

Теперь всё работает как надо. Ошибки компиляции больше нет, а сеттерный айдишник стал такой как я и хотел.

ilrsay20s37htt3jgn1hvzzxqlm.png

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

zgf53qqwnp1milrmwupvfjjsfjc.png

Не знаю, для чего в жизни это может пригодиться вам, но лично мне в работе такая штука нужна. Что мы тут указываем? Мы здесь указали имя филда. И дальше опять вот эта наша квадратная скобочка — открываем лямбду; здесь дальше соответственно указываем, что нас интересует. Причём, нам важно, чтобы поле было транзиентное.

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

vpxrr8bqe-clx5a5glm0sguhm5y.png

Давайте посмотрим, насколько это нам помогло.

sr7rtgjjkoadv1rpu3pscw5wlqk.png

Да, всё хорошо! Причём dirty написано ровно в том месте, где и должно. Нет никаких выкрутасов с отступами и т.д. Код выглядит хорошо и там, и там. Несмотря на то, что получился в результате кодогенерации. Плюс, как видите, код всё ещё остался простым для понимания.

@Entity больше мучить не будем. Убираю её в комментарии. И объявляю ту же самую аннотацию, но на Java (см. файл Entity.java). Здесь, как и на Xtend, всё просто, только чуть больше букв.

8vmdsh2rfnogeysyr9p4cyzpteq.png

Процессор тоже можно писать на Java (см. файл JavaEntityProcessor.java).

mm1ybwbxjgwrgbf7b0qkqz_blda.png

Что я тут сделал? Я добавил обработчик для ещё одной фазы: doRegisterGlobals и докинул в контекст классы, которые мне понадобятся: Service и Repository. Плюс, заоверрайдил метод doTransform — тот самый doTransform, который написал чуть раньше на Xtend. Причём я тут нормально навигируюсь по коду. Могу попадать в Xtend-код…

wawq0jit2ae61cr7uxetnqo7sh0.png

…и обратно в Java-код.

v3gaju8qryk0nybj9ayirmewl4a.png

Дальше (см. метод doTransform) я добавляю к нашей entity аннотацию. Обратите внимание, здесь, в отличие от Xtend все методы надо вызывать явно — через context.

Затем идёт метод, который создаёт репозиторий: createRepository. Важный момент: для всего того что мы генерируем, важно указывать PrimarySource: context.setPrimarySourceElement(repositoryType, entity);. Зачем? Чтобы при кодогенерации, — когда у нас появляется Java-файл, — он был связан со своим Xtend-источником.

Дальше немного скучного кода: пользую типы из Spring Data, чтобы указать какой у репозитория должен быть интерфейс.

qqn9o5rfs1nt2fkstgaviinhhhc.png

Дальше прохожусь по всем филдам, и выбирай из них те, которые не транзиентные и не являются айдишниками.

gqal5heiizwoja0xtdv1-nus0m8.png

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

Дальше добавляем к нашим филдам всякие разные findBy-методы. Причём смотрим на аннотацию »@Column», которая стоит над филдом. Если она имеет установленный атрибут признака уникальности значения (isUnique), просто возвращаем entityType. Если нет, возвращаем List. В конце ставим аннотацию @Param, которая нужна для того чтобы работал Spring Data Rest.

ovxq71sdi3g7h9jvvlvnmbsw6ug.png

Всё! Для Repository генератор готов. Теперь если откроем Xtend-исходник, на основе которого будет генерироваться Java-код, и посмотрим на Gentrated Code, то здесь у нас добавился ещё и репозиторий. Мы можем смотреть на него, вот он такой.

_mmrxqrwrwop2zpx467jejthmsy.png

Дальше пишем генератор для Service. Там всё почти всё так же как и с Repository.

yzyp-dqv7znc6bp3mzdylyoxdw0.png

Вот и всё. Процессор готов. Можно запускать сгенерированное приложение.

Хорошо, сервис и репозиторий есть. Но как нам узнать, что у нас с моделью нашей всё хорошо? Добавим ещё одну фазу — фазу валидации. Я добавляю два валидатора.

d4ri1fuxgfqp1ebtzjhsb-l65tc.png

Теперь, если разработчик, который пишет Extend-код, вдруг забудет поставить перед своим классом аннотацию @ToString, валидатор выведет на экран Warning.

6jrsnglowa798byzmkkzwujnakk.png

Или если разработчик поставит аннотацию @ManyToOne, а под ней ещё и @Column, то это уже ошибка. А ошибиться-то очень легко. Мы же программируем очень часто на копи-пасте, особенно когда есть возможность всё в один и тот же файл писать, как в Xtend. Скопировали, вроде работает — успокоились. Но можно нарваться на коварную ошибку.

Допустим, у меня в Country.xtend у филда lastName прописано nullable = false, и я хочу, чтобы у Country тоже было nullable = false. Так неправильно. Поэтому Eclipse предупреждает меня. Но при этом генерируется Java код, в котором вроде как нет проблем.

79ozl89kpcdh-rbmokz-nbxln9y.png

Я меняю на @JoinColumn(nullable = false), и теперь всё хорошо. Можно запускать приложение.

tujk86pt-am9msu5ei6xtgxaqg0.png

Давайте наберём в браузере localhost:8080

aksauftnl9f3lb9lenk3x-nzkog.png

…затем localhost:8080/users/search.

ypnz7yfatfvmy3vhfyztpub63sq.png

Все наши findBy на месте. Приложение работает!

Ну вот и всё. Теперь вы тоже можете брать кодогенерацию под контроль, эффективно использовать её в своей работе. То есть проводить время с пользой: пару дней потерять на то, чтобы создать кодогенаратор, а потом «за 5 минут долететь». Будете писать меньше кода, будете делать меньше ошибок.

Вы теперь умеете создавать собственные активные аннотации, писать и отлаживать код процессора. Причём делать всё это на смешанном диалекте Java и Xtend, без необходимости переносить всю свою кодовую базу на Xtend.

Демо-проект, который мы с вами прямо в этой статье сейчас разработали, — я заопенсорсил на гитхабе. Скачивайте, изучайте, пользуйте. А если информацию легче воспринимаете на слух и с видео, вот мой доклад с конференции JPoint, где рассказываю всё то же самое, что и здесь в статье.
У меня всё. Пишите меньше скучного кода, делайте меньше ошибок, применяйте технологии осознанно. Буду рад ответить на ваши вопросы. Можете писать мне на akogun@croc.ru. Кстати, помните, я в начале статьи говорил, что участвую в подготовке конференций для джавистов? JPoint 2020 из-за известных причин будет проходить онлайн, но это даже совсем неплохо, у нас много отличных спикеров, которые не смогли бы приехать и выступить очно, а сама конференция будет идти целых 5 дней! С 29 июня по 3 июля — jpoint.ru. Приходите!

© Habrahabr.ru