Привет, меня зовут Григорий Мясоедов, ранее я имел опыт работы в 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 плагин (<packaging>maven-plugin</packaging>) и перестать играть в постоянные догонялки с Maven. Просто запускать плагин для “резолва” зависимостей как обычный Maven task. Получается такое же api, как и работа с Maven через командную строку. Таким образом выполняется полный Maven процесс со всеми его текущими возможностями/фичами и оригинальным жизненным циклом. На таком уровне абстракции работы с 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 <groupId>:artifactId:<version>:my_task_name. Даже такой простой плагин, благодаря параметру requiresDependencyResolution = TEST, загрузит все зависимости если надо и разрешит их, добавив их в проектную модель Maven (TEST это самый верхнеуровневый scope).

Код моего 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. Также его можно собрать самим - инструкция есть в README.

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

Комментарии (11)


  1. VISTALL
    10.08.2023 16:21
    +1

    идея прикольная. Сам по себе maven плагин живет своей жизню (практически без external system api). Можно посмотреть на мавен плагин с другой стороны


    1. grisha9 Автор
      10.08.2023 16:21

      Спасибо. Теоретически, именно за счет автономности maven-plugin, с таким подходом можно с небольшими усилиями добавить поддержку maven для любой IDE.


  1. sshikov
    10.08.2023 16:21
    +1

    Плагин под названием GMaven уже есть, и ему сто лет в обед. Это груви интеграция для мавена. Более того, у него уже есть версия GMaven Plus, и ей тоже сто лет.


    1. grisha9 Автор
      10.08.2023 16:21

      GMaven Plus это плагин для Maven.
      Мой GMaven это плагин для IDEA. И с аналогичным именем в IDEA Marketplace других плагинов нет. К тому же я не уверен что имя плагина должно быть уникальным, в отличии от plugin id.


      1. sshikov
        10.08.2023 16:21
        +2

        Не, не должно конечно. Я просто о том, что некая потенциальная путаница налицо. Насколько это важно… ну кто знает. Может и не важно вовсе. Когда вы гуглите — вы получаете в ответе оба плагина.


  1. ris58h
    10.08.2023 16:21
    +2

    Тоже не понял зачем такое название запутывающее. То ли с Groovy связано, то ли с Google. А уж если есть другой проект с таким же именем, так вообще...


  1. Zero-Gravity
    10.08.2023 16:21
    +2

    Наш проект, во время сборки, использует собственный maven плагин, который, активируясь во время фазы validate, меняет версии некоторых зависимостей, что в итоге, с учетом транзитивных зависимостей, приводит к изменению списка зависимостей проекта. Само собой Idea этого не видит и формирует другой список зависимостей. С вашим плагином это вроде удалось - в настройках в maven args я указал вызов нашего плагина (в виде <plugin name>:<goal>) и все получилось! Спасибо! Думаю этот use case (запускать свои плагины во время импорта) тоже многим может пригодиться


    1. grisha9 Автор
      10.08.2023 16:21

      Спасибо за обратную связь! Интересный use case. Еще одно доказательство того, что нет ничего лучше чем полноценный maven lifecycle для импорта проекта) Если найдете какие то проблемы, то смело пишите в лс. А как раньше вы работали со своим проектом - Eclipse?


    1. ris58h
      10.08.2023 16:21

      активируясь во время фазы validate, меняет версии некоторых зависимостей

      Это зачем такой трюк?


  1. Digt0
    10.08.2023 16:21

    А почему вы выбрали Gradle для сборки этого проекта?


    1. grisha9 Автор
      10.08.2023 16:21

      Потому что плагины для IDE не пишутся с чистого листа. Для IDEA плагинов есть свой шаблон, встроенный в стандартный wizard. Который использует Gradle и обеспечивает базовый флоу написания плагинов - их запуск, отладку, публикацию в marketplace и прочее. И заниматься изысканиями на тему, а можно ли это переделать на Maven, я не видел смысла, т.к. решал совсем другую проблему.
      Тем более официальная документация не дает альтернатив. А поиск решений для Maven, находит только не активные проекты вроде - https://plugins.jetbrains.com/plugin/7127-intellij-plugin-development-with-maven/versions и https://maven.apache.org/plugins/maven-idea-plugin/