Привет, меня зовут Григорий Мясоедов, ранее я имел опыт работы в JetBrains в команде build tools, а конкретно занимался Maven-plugin. В этой статье я хочу поговорить о том как устроен плагин под капотом, его сильных и слабых местах, и о том, что я в итоге со всем этим сделал.
Одна из самых частых проблем, которыми я занимался в JetBrains, звучала так — “через командную строку Maven проект собирает, но в IDEA он не импортируется (импортируется с ошибками)”. Как будет показано ниже большинство этих проблем связаны с архитектурой тут. Код изобилует Java Reflection и проверками на версию Maven. Недавний пример — не работала поддержка Maven 4. Т.к. в Maven 4 было много изменений, то это потребовало создание нового процесса для данной версии. Получаем не малый объем кода и его дублирование. Что сказывается на сложности проекта и его поддержке и стабильности работы плагина.
Также есть open source реализация «демон» процесса для Maven — проект Mvnd (статья на Habr). Если посмотреть на его исходники, то можно заметить аналогичные проблемы. Там тоже появился модуль daemon-m40, вдобавок к daemon-m39. Это показывает насколько не тривиальная задача — создание и поддержка своего «демон» процесса для Maven и на сколько легко там можно допустить ошибку. И постоянно требуется «догонять» Maven.
Обзор GMaven plugin
Еще в плагин непосредственно для самого Maven. Суть которого разрешить все зависимости проекта. Он почти не содержит логики. Всего три класса — один из которых DTO, другой утилитный и основной Mojo класс.
Рассмотрим пример простейшего Maven plugin:
@Mojo(name = "my_task_name", defaultPhase = NONE, aggregator = true, requiresDependencyResolution = TEST) public class ResolveProjectMojo extends AbstractMojo { }
-
name — имя «таска» плагина;
-
defaultPhase — фаза жизненного цикла, к которой по умолчанию привязан плагин;
-
aggregator — значение true означает что плагин выполняется один раз для всего агрегатора, а не для каждого подпроекта в отдельности;
-
requiresDependencyResolution — требуемый scope для разрешения зависимостей.
Для запуска через командную строку плагина, нужно выполнить: mvn <groupId>:artifactId:<version>:my_task_name. Даже такой простой плагин, благодаря параметру requiresDependencyResolution = TEST, загрузит все зависимости если надо и разрешит их, добавив попутно resolvedArtifacts в проектную модель Maven — MavenProject (TEST это самый верхнеуровневый scope). А это именно то, что нам и надо.
Код моего Maven-плагина не многим сложнее, чем этот пример. Класс всего на 200 строк и суть этой логики — мэппинг данных для извлечения конфигураций ряда плагинов (настраиваются через точку расширения основного GMaven плагина), необходимых для корректного импорта проектной модели в IDEA. (Как пример: maven-compiler-plugin, откуда получаем параметры компилятора, чтобы передать в IDEA и проект мог собираться через среду разработки). Далее готовая проектная модель, со всеми разрешенными зависимостями, через листенер событий сборки Maven, возвращается как результат работы процесса. Maven-плагин добавляется в локальный m2 репозиторий пользователя в процессе работы основного плагина для IDE.
Тут следует чуть подробнее остановиться на том, как я получаю проектную модель из Maven, т.к. его процесс не подразумевает возврата какого-то результата кроме кода процесса. В RMI и сразу получают готовую модель проекта, как результат вызова метода, который отвечает за ее получение.
У меня было два пути:
-
возвращать результат через Maven output в виде строк и сериализовать/десериализовать его в какой либо формат (например JSON);
-
либо также обернуть процесс в RMI и возвращать Java объекты.
Я выбрал второй путь, такой механизм используется в листенер событий сборки Maven я сохраняю проектную модель в static переменную, результат которой и забираю в конце вызова RMI метода, который запускает обычный Maven процесс.
Затем, полученную проектную модель Maven, импортируем в IDEA через ExternalSystem API. В результате почти все заработало “из коробки” и плагин GMaven это также в основном просто мэппинг из проектной модели Maven в структуру ExternalSystem, которая далее “сама” ложится в структуру проекта IDEA (Project Structure… ctrl+alt+shift + s). Про более детальную работу с ExternalSystem API и другими точками расширения IDEA, необходимыми для написания подобного рода плагинов, планирую рассказать в следующей статье, если эта тема будет кому-либо интересна.
В итоге мы получаем:
-
очень простой процесс взаимодействия с Maven, который заключается в запуске плагина;
-
полноценный жизненный цикл Maven со всеми текущими фичами, что исключает баги из разряда, что мы чего-то не учли при получении проектной модели.
Результаты
|
|
GMaven |
IDEA Maven |
|
|
(~1100 модулей) |
ошибки импорта |
— |
+ |
|
ошибки сборки |
+/- |
+ |
|
|
время импорта (сек) |
110 |
60 |
|
|
(~150 модулей) |
ошибки импорта |
— |
+ |
|
ошибки сборки |
— |
+ |
|
|
время импорта (сек) |
60 |
|
|
|
(~100 модулей) |
ошибки импорта |
— |
— |
|
ошибки сборки |
+/- |
+/- |
|
|
время импорта (сек) |
20 |
12 |
|
|
(15 модулей) |
ошибки импорта |
— |
— |
|
ошибки сборки |
— |
— |
|
|
время импорта (сек) |
2 |
2 |
-
все зависимости на момент измерений, уже были в локальном репозитории;
-
В проекте Spring-Boot ошибки сборки в обоих плагинах вызваны модулем gradle plugin, если его отключить, то сборка проходит успешно;
-
Dbeaver IDEA Maven plugin не смог импортировать вообще;
-
сравнения проводились на версии IDEA 2023.2, -Xmx4g, i7-10875H, 32gb.
В целом можно сказать, что время импорта проекта, как в оригинальном mvnd и делегирование выполнение моего maven-плагина ему, чтобы не писать свой «демон» процесс и не заниматься его поддержкой;
не реализован инкрементальный апдейт билд скриптов, но это заметно только на проектах с большим числом Maven модулей — Quarkus/Spring;
проект не покрыт тестами, т.к. главной задачей на данный момент, было скорее закончить разработку и донести свою мысль, не расплескав ее, и выкатить прототип.
Ближайшая цель — это собрать обратную связь и понять, будет ли это кому-то полезно. И исправление багов, которые находятся в процессе работы плагина.
Что есть из интересного:
-
я отказался от dependency tree в build-tool окне. Т.к. в реальных проектах тысячи зависимостей, и искать в них нужную нет возможности. Также это сильно тормозит отрисовку — build-tool окна и потребляет много памяти. Вместо этого есть один элемент — Dependencies. Двойной клик по которому открывает Dependency Analyzer с возможностью поиска, фильтрации и отображение “путей” зависимостей (полное дерево зависимостей получаются лениво от Maven по требованию и постоянно не хранится).

-
Поддержка различных JDK level для main и test. В оригинальном плагине такая возможность тоже недавно появилась. Настройка — Create separate modules for production and test roots.
-
Понятие “контекста” при выполнение тасков. Допустим у нас есть проект с двумя модулями m1, m2 и m2 зависит от m1. Сейчас если в оригинальном плагине выполнить на модуле m2 task compile через build-tool окно, то будет ошибка — модуль m1 на найден. Можно конечно модуль m1 заинсталить в локальный репозиторий, но это лишнее действие. В моем плагине такой проблемы нет. При компиляции модуля m2, Maven сам поймет на основании графа проектов, что нужен также модуль m1 и в итоге скомпилируются оба модуля. За это отвечает настройка — Use whole project context for task execution (по умолчанию включена).

Итог
Плагин опубликован в IDEA Marketplace. Также его можно собрать самим — инструкция есть в README.
Далее его можно использовать для открытия существующих Java Maven проектов. Так и для создания новых, через стандартный wizard. Буду очень признателен, если сможете найти время и проверить мой плагин на вашем Maven проекте, и в случае обнаруженных проблем, дадите обратную связь. Можно не стесняться и писать мне в личку на Habr или завести issue. Также мои контакты для связи есть на домашней странице плагина.


ссылка на оригинал статьи https://habr.com/ru/articles/753828/


Добавить комментарий