Если вы автоматически пишете mvn clean install
при сборке проекта, то вы такой точно не один — эта команда прочно укоренилась в привычке Java-разработчиков. Но действительно ли она всегда необходима? Что, если вместо неё использовать verify
?
В новом переводе от команды Spring АйО мы погрузимся в детали жизненного цикла Maven, поведение реактора, инкрементные сборки и подводные камни использования clean
и install
. А главное — попробуем понять, когда можно (и нужно) обойтись без install
и clean
, чтобы ускорить сборку и сократить расходы памяти.
Как и многие разработчики, при необходимости собрать Maven-проект, я в первую очередь вызываю mvn clean install
. В конце концов, именно эту команду можно найти практически в каждом README или BUILD-файле — в этом нет сомнений. Неважно, одиночный это проект или мультимодульный — инструкция всегда одна и та же. Более того, если в сборке участвуют несколько проектов, то install вроде как строго необходим, не так ли? Иначе соседние модули не смогут найти зависимости друг от друга. Или мне так казалось.
На JCrete 2018 у меня была возможность пообщаться с Робертом Шолте (@rfscholte) о Maven. Я столкнулся с проблемой при сборке подмножества модулей в реакторе, и он показал мне флаги команд -am -pl
.
Комментарий от эксперта Spring АйО, Михаила Поливахи
Роберт является контрибьютором в Maven, а также является "PMC Member-ом", т.е. входит в так называемый Project Management Committee
Под "реактором" имеется в виду не Project Reactor, а имеется в виду система, которая занимается сборкой мульти-модульных/мульти-проектных приложений. Эта система уже давно существует в Maven, и носит имя "Reactor".
О ней мало говорят, но я посчитал нужным это пояснить, чтобы убрать недопонимание. Флаги -am
и -al
это флаги реактора, можете про них почитать, там всё относительно просто
Часть нашего разговора позже была описана в этом посте. Он также отметил, что можно использовать verify
вместо clean install
или просто install
. Сначала я был озадачен — я даже не знал, что такая команда как verify вообще существует и зачем она нужна. Моя первая реакция на предложение перейти на неё была примерно такой: «Погоди, другие проекты ведь не скомпилируются — им нужно разрешать зависимости между собой». С ухмылкой на лице Роберт терпеливо начал объяснять основы жизненных циклов Maven, целей, привязок плагинов и того, как всё это исполняется внутри реактора. Всем участникам сессии очень понравилось, и полученные знания оказались невероятно полезными. Если попытаться кратко передать суть сказанного, получится примерно следующее:
Плагины Maven (Mojos) предоставляют поведение через цели (goals). Цель — это точка входа для выполнения определённого действия.
Maven запускает последовательность фаз жизненного цикла.
Плагины могут привязывать свои цели к конкретным фазам, чтобы они автоматически выполнялись при наступлении соответствующей фазы.
Последовательность фаз предопределена (строго говоря, она может быть изменена с помощью расширений, но на практике это случается редко).
В стандартной (наиболее распространённой) последовательности фаз идут:
validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy
.
Обратите внимание, что install
следует сразу после verify
. Это значит, что каждый раз, вызывая install
, вы также вызываете и verify. Запомните этот момент. В ноябре 2019 года Роберт делал доклад о Maven на Devoxx BE (видео), суть которого заключалась в адаптации к новому поведению инструмента. Конечно же, он упомянул тему clean install
vs verify
. Исторически (во времена Maven 2) нам действительно приходилось постоянно использовать install
, если один модуль в мультипроектной сборке зависел от другого, потому что тогда поддержка таких сборок в Maven была на начальной стадии. Более того, изначально она реализовывалась не в ядре Maven, а через плагин maven-reactor-plugin. Опыт, полученный при разработке этого плагина, позже был интегрирован в ядро Maven 3. Теперь, когда реактор стал частью ядра, его возможности глубже связаны с остальной инфраструктурой, в частности — артефакты, созданные в рамках текущей сессии (реактора), прикрепляются к ней, и другие модули могут получать эти артефакты напрямую, а не запрашивать их из репозиториев. И вот эта логика начинает работать уже на этапе verify
.
Ещё один неочевидный момент — порядок вызова целей внутри реактора. Если мы вызываем несколько целей, они исполняются в заданном порядке для каждого модуля. То есть при вызове clean install в структуре проекта вроде
.
├── pom.xml
├── project1
│ └── pom.xml
├── project2
│ └── pom.xml
├── project3
│ └── pom.xml
└─��� project4
└── pom.xml
цели вызываются так:
:clean
:install
:project1:clean
:project1:install
:project2:clean
:project2:install
:project3:clean
:project3:install
:project4:clean
:project4:install
Вместо вызова всех целей сначала по одной для каждого проекта, например:
:clean
:project1:clean
:project2:clean
:project3:clean
:project4:clean
:install
:project1:install
:project2:install
:project3:install
:project4:install
Именно из-за такого поведения, если мы вызываем verify
для такой сборки, артефакты, сгенерированные project1, становятся доступны для всех остальных проектов, которым они могут понадобиться, без запроса этих артефактов из репозитория.
Комментарий от эксперта Spring АйО, Михаила Поливахи
Автор не акцентирует внимание, но это важно. По сути, из всего, что описано выше, вы должны сделать простой вывод: В Maven 3 (а у вас у всех Maven 3, поверьте), собрать мультимодульное/мультипроектное приложение можно как с помощью install
, так и с помощью verify
. Просто install
делает дополнительный шаг - инсталлирует jar в локальный .m2 кеш каждый проект, что было необходимо в Maven 2, но в Maven 3 уже нет.
Поэтому, можете просто использовать mvn verify
, он должен работать быстрее, чем install
.
Ещё один плюс отказа от clean install
— в Maven 3 появилась поддержка инкрементных сборок, а команда clean эту возможность сводит на нет. Важно понимать, что хотя Maven и поддерживает инкрементные сборки, именно плагины должны обеспечивать корректную работу этой функциональности. Поэтому, если вы сталкиваетесь с проблемами при использовании инкрементных сборок, сообщите об этом команде Maven или авт��рам плагинов — возможно, где-то что-то реализовано неправильно.
Комментарий от эксперта Spring АйО, Михаила Поливахи
Инкрементальные билды вообще довольно распространенная практика в билд системах. например в Gradle или Make на них очень много построено. Просто в Maven, как показывает эмпирический опыт, люди редко используют их функционал.
Хорошо, пора перейти к цифрам. Я решил провести эксперимент на наборе популярных проектов, которые ежедневно собираются десятки (если не сотни) раз, используют множество плагинов и настроек: проверки качества кода, annotation процессоры, генерация исходников и так далее. Я выбрал следующие шесть проектов (показано количество только Java-кода):
Проект |
Модулей |
Файлов |
Пустых строк |
Комментариев |
Кода |
Guava |
6 |
1976 |
65074 |
139019 |
375825 |
Byte Buddy |
9 |
1128 |
33408 |
76365 |
159056 |
jOOQ |
9 |
1772 |
73330 |
206477 |
194156 |
Sentinel |
80 |
1064 |
12772 |
28026 |
58270 |
Helidon |
202 |
3040 |
52731 |
120873 |
223469 |
Quarkus |
737 |
6849 |
80264 |
46954 |
361403 |
Подсчёт строк кода был выполнен с помощью https://github.com/AlDanial/cloc после клонирования проектов. Все сборки выполнялись на Java 15, за исключением Guava и jOOQ — они собирались на Java 8. Сырые данные можно найти в этом gist. Для всех сборок использовались следующие команды:
mvn verify -DskipTests
mvn clean
mvn verify -DskipTests
mvn verify -DskipTests
mvn verify -DskipTests
mvn clean
mvn verify -DskipTests
mvn clean verify -DskipTests
mvn clean
mvn install -DskipTests
mvn install -DskipTests
mvn install -DskipTests
mvn clean
mvn install -DskipTests
mvn clean install -DskipTests
Первая команда mvn verify
предназначена для тестирования сборки проекта и одновременного скачивания всех необходимых зависимостей и плагинов в локальный репозиторий Maven — так сетевой доступ не будет влиять на время сборки. Затем репозиторий очищается, и начинаются собственно измерения. Я решил пропустить тесты, поскольку они меня в этом эксперименте не интересуют. Позже в посте я ещё вернусь к этому решению. Полученные измерения представлены на следующих графиках: синие линии — вызовы verify, красные — вызовы install
. Теоретически красные линии должны быть немного длиннее синих, но лишь незначительно. Все значения приведены в секундах.

Guava соответствует ожиданиям: вызовы №2 и №3 выполняются быстрее, чем первый — примерно на 50%, поскольку сгенерированные результаты (классы, обработанные ресурсы и т.д.) скорее всего остались нетронутыми и могут быть переиспользованы. Тем не менее какая-то работа всё же выполняется — возможно, проверки качества кода. Затраты на очистку между сборками невелики: очистка репозитория занимает около 4 секунд, и это время включено в последние два измерения.

Byte Buddy выполняет одни и те же действия независимо от предыдущих результатов. Это может означать, что сборка не использует поддержку инкрементной сборки, реализованную в Maven, либо по своей специфике Byte Buddy вынужден выполнять одни и те же действия каждый раз.

jOOQ — весьма интересный случай. Обратите внимание: вызовы №2 и №3 выполняются на 85% быстрее. Эта сборка не тратит время на выполнение ненужных задач. Я был приятно удивлён этими цифрами — похоже, Лукас явно знает, что делает.
Комментарий от эксперта Spring АйО, Михаила Поливахи
Под Лукасом имеется в виду автор Jooq - Lukas Eder

Sentinel демонстрирует ожидаемую экономию времени: от 40% до 50% на вызовах №2 и №3.

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

И наконец, Quarkus (в котором сейчас более 745 модулей!). Пришлось использовать дополнительный профиль (quickly), чтобы отключить проверки качества кода, форматтеры и прочее, иначе сборка заняла бы более 20 минут. Quarkus показывает экономию до 60% времени при вызовах №2 и №3.
Вы можете провести свои собственные замеры. Эти данные — всего лишь выборка и не претендуют на абсолютную истину, но они отражают определённые тенденции. Они не дают серьёзных оснований утверждать, что install
значительно медленнее, чем verify — различия, как правило, несущественны. Тем не менее я всё же рекомендую вам провести измерения в своих проектах — возможно, именно для вас разница окажется значимой. А вот с использованием clean
и инкрементных сборок всё куда очевиднее. В идеале вы вообще не должны вызывать clean
, оставляя его только для особых случаев, когда действительно необходимо удалить промежуточные результаты. Например, бывают ситуации, когда тесты требуют "чистой" среды — определённых директорий и��и ресурсов. И да, плагин EAR на данный момент действительно сломан, и для него по-прежнему приходится использовать clean install
.
Есть и другие оправданные сценарии использования install
в мультимодульной сборке. Например, может быть тест, которому нужно разрезолвить артефакт для работы — и тогда публикация через install
становится быстрым решением. Другой вариант — использовать плагин mrm-maven-plugin (Mock Repository Manager), который предоставляет артефакты реактора как будто они доступны из Maven-совместимого репозитория. При этом тесты нужно будет адаптировать для обращения к этому репозиторию. Однако плагин используется редко — ведь проще запустить clean install -DskipTests
, чем добавлять новый плагин.
Ещё один распространённый сценарий — запуск целей на одном модуле вне контекста реактора. Как известно, все цели в Maven запускаются во всех модулях, участвующих в реакторе. Допустим, мы хотим запустить тесты только в project3:
$ mvn -am -pl :project3 test
Эта команда запустит фазу test для всех проектов в реакторе: root, project1, project2 и project3. Хотя мы хотели проверить только project3, нам всё равно нужны POM и JAR-файлы всех его зависимостей. Проблема в том, что project3 зависит от project2, а тот — от project1, и все они унаследованы от root. Альтернатива — вызвать install в корне:
$ mvn -am -pl :project3 install -DskipTests
$ cd project3
$ mvn test
И далее запускать test или другие цели только для project3, пока не достигнем нужного результата. Похоже, Maven изначально задумывался для запуска именно из корня проекта, а не из подмодуля.
Итак, опровергнута ли идея о том, что verify
лучше clean install
, исходя из наших измерений? Кажется, что да. Но (а "но" всегда есть) — всё зависит от контекста. Помните, что install отвечает за копирование артефактов из реактора в локальный репозиторий. Если вам не нужны эти файлы в репозитории, и verify
даёт те же наблюдаемые результаты (код работает, тесты проходят), то нет смысла вызывать install
. Вы сэкономите дисковое пространство. Если на вашем компьютере это не критично, возможно, стоит подумать о CI-среде. Частая проблема: после вызова install
и запуска тестов на одном модуле используется устаревший артефакт зависимости — мы забыли сделать install
на другом модуле, и теперь тратим время на отладку несуществующей ошибки, вызванной бинарной несовместимостью из-за использования устаревшей зависимости.
В итоге: вызов verify
вместо install
чаще всего даёт те же преимущества с меньшими недостатками. Есть сценарии, где без install
не обойтись. Постоянное использование clean
— тоже не лучшая идея, но иногда оно необходимо. Задаётесь вопросом, когда можно обойтись без него, а когда нельзя? Учитывая показанные здесь тенденции (и возможные замеры в вашем проекте), выигрыш по времени может быть незначительным — возможно, проще продолжать использовать clean install и не забивать себе голову.
Возможно. А возможно — и нет. Лично мне нравится "копать глубже", изучать внутреннюю кухню Maven и находить способы сделать сборку быстрее и эффективнее. Мне интересно использовать инструмент шире, чем просто набор из трёх команд. Я не против поэкспериментировать. Но я также понимаю, что не все горят желанием поступать так же — да и не всегда обстоятельства позволяют.
Напоследок: попробуйте использовать verify
. Возможно, он лучше подходит для ваших задач, чем install
.
Пишем код дальше!

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.