Новый взгляд на Maven-plugin для IDEA — GMaven
Привет, меня зовут Григорий Мясоедов, ранее я имел опыт работы в JetBrains в команде build tools, а конкретно занимался Maven-plugin. В этой статье я хочу поговорить о том как устроен плагин под капотом, его сильных и слабых местах, и о том, что я в итоге со всем этим сделал.
Одна из самых частых проблем, которыми я занимался в JetBrains, звучала так — «через командную строку Maven проект собирает, но в IDEA он не импортируется (импортируется с ошибками)». Как будет показано ниже большинство этих проблем связаны с архитектурой JB Maven плагина.
Обзор Maven plugin IDEA
Основная задача плагина для IDE это получить от билд-системы проектную модель, чтобы на основании этих данных сконфигурировать структуру проекта в самой IDE (модули, их директории — java/test/resources, зависимости и прочее).
Maven внутри использует Google Guice в качестве dependency injection фреймворка. На каждый запуск он создает новый процесс и поднимает с помощью Guice свой программный контекст. Основными компонентами которого являются:
ProjectBuilder — отвечает за построение проектной модели в памяти на основании build скриптов;
ModelInterpolator — заменяет выражения вида
${value}
их действующими значениями;ProjectDependenciesResolver — резолв зависимостей, включая транзитивные;
MavenSession — контекст сессии, содержащий все параметры процесса;
MavenProject — основной класс внутренней модели данных.
Текущая архитектура JetBrains Maven плагина, выглядит примерно следующим образом: из низкоуровневых Maven-компонентов, что приведены выше, построен кастомный легковесный процесс, который читает билд-файлы, разрешает все зависимости проекта и возвращает проектную модель. (Плагин для Eclipse, кстати, использует такой же подход). Данный процесс запускается в виде «демона», чтобы поднимать программный контекст один раз при первом запуске, а далее он переиспользуется.
Плюсы такого подхода:
он более легковесный и использует только то, что необходимо для конечного результата (получить проектную модель со всеми зависимостями);
переиспользует программный контекст;
как следствие работает быстрее;
за счет полной кастомизации процесса проще добавлять различные фичи для IDE;
Минусы:
из-за того, что трудно один к одному воспроизвести оригинальный Maven-процесс, постоянно возникают баги — что-то не учли/пропустили;
Maven на таком низком уровне часто меняется. Постоянно приходится играть с ним в догонялки, добавляя новый Maven-фичи в JB процесс;
отсюда также вытекает частая головная боль с выходом новых версий Maven и поддержкой совместимости (к примеру выход версий — 3.8.5, 4.0);
IDEA Maven «демон» хранит состояние в виде текущих настроек Maven. Это добавляет дополнительную сложность;
тяжело поддерживать.
Как итог: основная причина многих проблем, заключалась в том, что оригинальный Maven процесс отличается от JB процесса.
Как выглядит этот процесс для Maven 3.х можно посмотреть тут. Код изобилует Java Reflection и проверками на версию Maven. Недавний пример — не работала поддержка Maven 4. Т.к. в Maven 4 было много изменений, то это потребовало создание нового процесса для данной версии. Получаем не малый объем кода и его дублирование. Что сказывается на сложности проекта и его поддержке и стабильности работы плагина.
Также есть open source реализация «демон» процесса для Maven — проект Mvnd (статья на Habr). Если посмотреть на его исходники, то можно заметить там аналогичные проблемы. Там также появился модуль daemon-m40, вдобавок к daemon-m39. Это показывает насколько не тривиальная задача — создание и поддержка своего «демон» процесса для Maven и на сколько легко там можно допустить ошибку. И постоянно требуется «догонять» Maven.
Обзор GMaven plugin
Еще в JB, я предложил совсем другой подход — резолвить зависимости проекта через кастомный maven плагин (
Но ввиду известных событий, не успел начать это реализовывать в JB. И чтобы это не просто осталось на словах, но и показать на деле, что такой подход работает, решил написать свой Maven плагин для IDEA, который назвал — GMaven.
Основной модуль моего плагина для IDEA — это плагин непосредственно для самого 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
Код моего Maven-плагина не многим сложнее, чем этот пример. Класс всего на 200 строк и суть этой логики — мэппинг данных для извлечения конфигураций ряда плагинов (настраиваются через точку расширения основного GMaven плагина), необходимых для корректного импорта проектной модели в IDEA. (Как пример: maven-compiler-plugin, откуда получаем параметры компилятора, чтобы передать в IDEA и проект мог собираться через среду разработки). Далее готовая проектная модель, со всеми разрешенными зависимостями, через листенер событий сборки Maven, возвращается как результат работы процесса. Maven-плагин добавляется в локальный m2 репозиторий пользователя в процессе работы основного плагина для IDE.
Тут следует чуть подробнее остановиться на том, как я получаю проектную модель из Maven, т.к. его процесс не подразумевает возврата какого-то результата кроме кода процесс. В JB плагине такой проблемы нет, т.к. у них свой кастомный процесс, где они напрямую оперируют внутренними объектами Maven и могут с ним делать что угодно. Поэтому они свой «дэмон» процесс «завернули» в RMI и сразу получают готовую модель проекта, как результат вызова метода, который отвечает за ее получение.
У меня было два пути:
возвращать результат через Maven output в виде строк и сериализовать/десериализовать его в какой либо формат (например JSON);
либо также обернуть процесс в RMI и возвращать Java объекты.
Я выбрал второй путь, такой механизм используется в JB плагине и я хорошо был с ним знаком. И с точки зрения экономии времени, для меня было лучше переиспользовать уже готовый код. Хотя первый вариант более идеологически верный. Поэтому я тоже свой процесс «завернул» в RMI. И как уже писал выше, через листенер событий сборки 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 | ||
Quarkus (~1100 модулей) | ошибки импорта | - | + |
ошибки сборки | +/- | + | |
время импорта (сек) | 110 | 60 | |
Dbeaver (~150 модулей) | ошибки импорта | - | + |
ошибки сборки | - | + | |
время импорта (сек) | 60 | ||
Spring-Boot-2.1.x (~100 модулей) | ошибки импорта | - | - |
ошибки сборки | +/- | +/- | |
время импорта (сек) | 20 | 12 | |
Maven 3.8.x (15 модулей) | ошибки импорта | - | - |
ошибки сборки | - | - | |
время импорта (сек) | 2 | 2 |
все зависимости на момент измерений, уже были в локальном репозитории;
В проекте Spring-Boot ошибки сборки в обоих плагинах вызваны модулем gradle plugin, если его отключить, то сборка проходит успешно;
Dbeaver IDEA Maven plugin не смог импортировать вообще;
сравнения проводились на версии IDEA 2023.2, -Xmx4g, i7–10875H, 32gb.
В целом можно сказать, что время импорта проекта, как в оригинальном JB плагине так и в моем, на маленьких и средних проектах до ~50 модулей, примерно сопоставимое. На проектах с большим числом модулей, из-за полностью кастомного процесса получения проектной модели и ряда оптимизаций оригинальный плагин работает быстрее.
Текущее состояние проекта
На данном этапе это MVP c базовыми возможностями:
полный импорт проектной модели из Maven в IDEA;
выполнение Maven тасков;
работа с зависимостями + Dependency Analyzer;
создание Run Configurations для запуска;
открытие существующего проекта, создание нового проекта/модуля.
поддержка Maven 3.3.1 + (JDK 7+)
версия IDEA 2022.2+
В основу закладывается простота разработки и стабильность. При получении проектной модели, в build окне IDE, выводится стандартный Maven output, что помогает в локализации и решении большинства проблем.
Конечно у меня тоже могут быть проблемы с обратной совместимостью. Но Maven, на уровне командной строки, проектной модели и plugin api, меняется гораздо реже и более стабилен. И на данный момент у меня нет отдельной логики для Maven 3 и Maven 4. Есть один простой общий процесс — запустить Maven task.
Да, на настоящий момент, в моем плагине меньше возможностей, чем в оригинальном, но с другой стороны из-за этого он в некоторых аспектах быстрее работает и потребляет меньше ОП, т.к. хранит меньше состояния. Я использую свой плагин на текущем месте работы и данного функционала мне достаточно для моих потребностей.
К основным минусам моего плагина можно отнести:
на каждый запуск импорта проектной модели, он создает новый процесс и поднимает программный контекст Maven. В среднем на это уходит 0.5 сек. Я считаю это умеренной платой за простоту. Есть идеи как это можно улучшить — интеграция с mvnd и делегирование выполнение моего maven-плагина ему, чтобы не писать свой «демон» процесс и не заниматься его поддержкой;
не реализован инкрементальный апдейт билд скриптов, но это заметно только на проектах с большим числом Maven модулей — Quarkus/Spring;
проект не покрыт тестами, т.к. главной задачей на данный момент, было скорее закончить разработку и донести свою мысль, не расплескав ее, и выкатить прототип.
Ближайшая цель — это собрать обратную связь и понять, будет ли это кому-то полезно. И исправление багов, которые находятся в процессе работы плагина.
Итог
Плагин опубликован в alpha channel основного маркетплейса. Для того, чтобы загрузить его через IDE, нужно добавить в настройках alpha репозиторий — https://plugins.jetbrains.com/plugins/alpha/list.
Далее его можно использовать для открытия существующих Java Maven проектов. Так и для создания новых, через стандартный wizard. Буду рад, если кому-нибудь это пригодится и поможет решить проблемы импорта проекта в IDE. Если нет, то мои контакты для связи есть на домашней странице плагина и можно заводить issue.