Зависимости в Maven: scope, транзитивные зависимости, конфликты версий и BOM
В проекте две условные библиотеки: report-lib внутри использует Guava 19.0, а http-lib — Guava 33.4.0-jre. Обе подключены в pom.xml, report-lib объявлена первой. mvn package проходит без ошибок, а при запуске приложение падает с java.lang.NoSuchMethodError: 'java.util.stream.Collector com.google.common.collect.ImmutableList.toImmutableList()'. Maven молча выбрал Guava 19.0: обе версии находятся на одинаковой глубине дерева, и побеждает объявленная первой. А метод toImmutableList(), который вызывает http-lib, появился только в Guava 21. Если поменять две зависимости местами, ошибка исчезнет — ниже разберём, почему так происходит и как управлять этим явно.
Зависимость в Maven — это внешний артефакт (обычно jar-библиотека), который проект использует и который объявлен в pom.xml элементом <dependency> по координатам groupId, artifactId и version. Maven сам находит её в репозитории, добавляет в нужные classpath в зависимости от scope (области видимости) и подтягивает её собственные, транзитивные зависимости. Если одна библиотека встречается в дереве в нескольких версиях, Maven оставляет одну из них по правилу nearest wins, а управлять выбором позволяют <exclusions>, <dependencyManagement> и BOM. Что такое зависимости и зачем они нужны в принципе, кратко описано в уроке Что такое Maven; здесь — как ими управлять. Примеры рассчитаны на Maven 3.9.x и Java 25.
Как объявить зависимость: GAV, type, classifier, optional
Зависимость объявляется элементом <dependency> внутри <dependencies>: обязательны groupId и artifactId, а version обязательна, если версия не задана в dependencyManagement (своём, родительском или импортированном из BOM). Остальные элементы — scope, type, classifier, optional, exclusions — необязательны и уточняют, какой именно файл нужен и как он используется.
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.0-jre</version>
<!-- scope is omitted: compile by default -->
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<version>4.1.115.Final</version>
<!-- native library for one platform -->
<classifier>linux-x86_64</classifier>
</dependency>
</dependencies> | Элемент | Что задаёт | По умолчанию | Пример значения |
|---|---|---|---|
groupId, artifactId, version | Координаты библиотеки (GAV) | version — из dependencyManagement, иначе ошибка | com.google.guava, guava, 33.4.0-jre |
scope | В какие classpath попадает зависимость и передаётся ли дальше | compile | test, provided, runtime |
type | Тип артефакта (от него зависит расширение файла) | jar | pom (для BOM), test-jar, war |
classifier | Вариант артефакта с теми же GAV | Нет (основной артефакт) | linux-x86_64, sources, tests |
optional | Не передавать зависимость потребителям вашего проекта | false | true |
exclusions | Транзитивные зависимости, которые нужно отбросить | Нет | См. раздел про exclusions |
Где найти координаты. Официальный поиск по Maven Central — central.sonatype.com (старый адрес search.maven.org). На странице артефакта есть готовый фрагмент <dependency> и список всех версий. Неофициальный каталог mvnrepository.com удобен тем, что показывает, какие зависимости тянет библиотека и какие у версий известные уязвимости. Копируйте координаты оттуда, а не из старых статей: у многих библиотек менялся groupId (например, при переезде с javax.* на jakarta.*). Номера версий в примерах урока реальные, но актуальную версию всегда сверяйте на Maven Central. Откуда Maven скачивает файлы и где хранит их локально, описано в уроке Виды репозиториев в Maven.
Scope зависимостей: compile, provided, runtime, test, system, import
Scope (область видимости) зависимости определяет, в какие classpath Maven её добавит — для компиляции основного кода, для компиляции и запуска тестов, для запуска — и передаётся ли она транзитивно проектам, которые зависят от вашего. Значение по умолчанию — compile. Неверный scope обычно проявляется не при компиляции, а при запуске: ClassNotFoundException или лишние библиотеки в сборке.
| Scope | Компиляция main | Компиляция и запуск тестов | Classpath запуска | В WEB-INF/lib war | Транзитивна | Типичный пример |
|---|---|---|---|---|---|---|
compile (по умолчанию) | Да | Да | Да | Да | Да | Guava, Jackson, commons-lang3 |
provided | Да | Да | Нет — предоставит среда (сервер, JDK) | Нет | Нет | jakarta.servlet-api, Lombok |
runtime | Нет | Да | Да | Да | Да | JDBC-драйвер PostgreSQL, реализация логирования |
test | Нет | Да | Нет | Нет | Нет | JUnit, Mockito, AssertJ |
system (устарел) | Да | Да | Нет | Нет | Нет | Локальный jar по systemPath — не используйте |
import | Не попадает ни в один classpath: допустим только в <dependencyManagement> вместе с <type>pom</type> и подставляет версии из BOM | junit-bom, spring-boot-dependencies | ||||
Колонка «В WEB-INF/lib war» описывает поведение maven-war-plugin. Обычный jar, который собирает maven-jar-plugin, не содержит ни одной зависимости, независимо от scope. Какие зависимости попадут в «толстый» jar, решает плагин, который его собирает (например, maven-shade-plugin или spring-boot-maven-plugin), — смотрите его документацию.
<dependencies>
<!-- the servlet container (Tomcat) provides it at runtime -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<!-- code uses only java.sql.*, the driver is loaded at runtime -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
<scope>runtime</scope>
</dependency>
<!-- tests only -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies> Драйвер JDBC объявляют со scope runtime, потому что код работает только с интерфейсами java.sql.Connection и DriverManager, а конкретный драйвер находится при запуске. Если же случайно импортировать в коде класс драйвера, например org.postgresql.PGConnection, компиляция упадёт с package org.postgresql does not exist — и это полезная защита от привязки к одной СУБД.
Scope system требует абсолютный путь к jar в <systemPath> и в документации Maven помечен как устаревший: сборка перестаёт быть переносимой, а потребители проекта такой файл не получат. Если jar нет в публичных репозиториях, установите его в локальный или корпоративный репозиторий и подключайте как обычную зависимость.
Lombok: provided ещё не значит «работает»
Lombok нужен только при компиляции, поэтому его объявляют со scope provided (поддержка JDK 25 появилась в Lombok 1.18.40; новые JDK Lombok поддерживает с задержкой, поэтому сверяйте changelog и берите свежую версию). Но начиная с JDK 23 javac по умолчанию не запускает процессоры аннотаций, найденные в classpath, если обработка аннотаций не настроена явно. Тогда @Getter «не срабатывает» и компиляция падает с cannot find symbol на вызове геттера. Обычно Lombok дополнительно указывают в annotationProcessorPaths плагина maven-compiler-plugin — о настройке плагинов см. урок Использование плагинов в Maven.
Транзитивные зависимости
Транзитивная зависимость — это зависимость вашей зависимости: если проект подключает библиотеку A, а в POM библиотеки A объявлена библиотека B, Maven добавит в проект и B, и её зависимости, на любую глубину. Попадёт ли транзитивная зависимость в проект и с каким scope, определяется сочетанием scope прямой и транзитивной зависимостей; optional-зависимости и зависимости со scope provided и test дальше не передаются.
Например, одна строка с junit-jupiter в pom.xml приводит в проект ещё несколько артефактов: API Jupiter, движок, junit-platform-commons, opentest4j и другие. Их не нужно объявлять вручную — но их версии выбираете уже не вы, а авторы библиотеки.
Итоговый scope транзитивной зависимости по документации Maven (строка — scope вашей прямой зависимости, столбец — scope, с которым транзитивная зависимость объявлена в её POM; «—» означает, что зависимость не попадает в проект):
| Прямая \ транзитивная | compile | provided | runtime | test |
|---|---|---|---|---|
compile | compile | — | runtime | — |
provided | provided | — | provided | — |
runtime | runtime | — | runtime | — |
test | test | — | test | — |
Из таблицы следуют два практических вывода. Первый: библиотека, пришедшая транзитивно через test-зависимость, доступна только тестам, даже если в своём POM она объявлена как compile. Второй: транзитивная compile-зависимость видна при компиляции вашего кода, и ей легко начать пользоваться «случайно». Документация Maven прямо отмечает, что такие зависимости правильнее объявлять явно: если автор библиотеки уберёт её в следующей версии, ваш код перестанет компилироваться. Найти такие места помогает mvn dependency:analyze.
optional. Флаг <optional>true</optional> ставит автор библиотеки на зависимость, нужную только для части функций. Сама библиотека с ней компилируется, а проекты-потребители её не получают и должны подключить явно, если пользуются этой функцией:
<!-- in the POM of a library, for example report-lib -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.3.0</version>
<!-- needed only for Excel export; users add it themselves -->
<optional>true</optional>
</dependency> Если потребитель вызовет функцию экспорта, не добавив poi-ooxml, сборка пройдёт, а при запуске вызов упадёт с NoClassDefFoundError. optional — инструмент автора библиотеки; в обычном приложении, которое никто не подключает как зависимость, этот флаг ничего не меняет.
Конфликт версий: правило nearest wins
Конфликт версий в Maven возникает, когда одна и та же библиотека (одинаковые groupId и artifactId) встречается в дереве зависимостей в разных версиях. Maven оставляет в classpath ровно одну версию по правилу nearest wins (dependency mediation): побеждает версия, ближайшая к вашему проекту по глубине дерева; при равной глубине — та, что встретилась первой в порядке объявления зависимостей. Новизна версии при этом не учитывается.
com.examclouds:shop
+- com.example:report-lib:1.0.0
| \- com.google.guava:guava:19.0 depth 2, declared first -> WINS
\- com.example:http-lib:2.0.0
\- com.google.guava:guava:33.4.0-jre depth 2, declared second -> omitted | Ситуация в дереве | Какую версию выберет Maven |
|---|---|
Версия объявлена в вашем pom.xml напрямую (глубина 1) | Её, какие бы версии ни пришли транзитивно |
Версия задана в dependencyManagement вашего проекта (своём, родительском или из BOM) | Её — управляемая версия применяется и к транзитивным зависимостям |
| Транзитивные версии на разной глубине | Ту, что ближе к корню дерева |
| Транзитивные версии на одинаковой глубине | Ту, что встретилась раньше в порядке объявления <dependency> |
Опасность правила в том, что сборка проходит успешно. Ваш код компилируется против выбранной версии, а библиотека, рассчитанная на другую версию, ломается только тогда, когда выполнение доходит до отсутствующего API:
NoSuchMethodError— класс есть, но нужного метода (или его сигнатуры) в выбранной версии нет; как в примере из начала урока.NoClassDefFoundErrorилиClassNotFoundException— в выбранной версии нет целого класса (его добавили позже или перенесли в другой пакет).AbstractMethodError,IncompatibleClassChangeError— изменилась иерархия или интерфейс.
Поэтому порядок <dependency> в pom.xml может влиять на результат, и «починка» перестановкой строк опасна: она держится на случайности и сломается при следующем обновлении. Надёжное решение — явно зафиксировать нужную версию (см. ниже). Обратная ситуация тоже возможна: если принудительно поднять версию, старая библиотека может не найти удалённый в новой версии API, поэтому после обновления проверяйте её работу тестами.
mvn dependency:tree и dependency:analyze
Команда mvn dependency:tree выводит дерево зависимостей проекта с версиями и scope, которые Maven выбрал после разрешения конфликтов, а mvn dependency:analyze сравнивает объявленные зависимости с реально используемыми в скомпилированных классах. Обе команды — цели плагина maven-dependency-plugin, который не нужно объявлять в pom.xml, чтобы вызвать их из командной строки.
Дерево зависимостей
mvn dependency:tree Вывод для проекта из начала урока (фрагмент; точное оформление зависит от версии плагина):
[INFO] com.examclouds:shop:jar:1.0-SNAPSHOT
[INFO] +- com.example:report-lib:jar:1.0.0:compile
[INFO] | \- com.google.guava:guava:jar:19.0:compile
[INFO] +- com.example:http-lib:jar:2.0.0:compile
[INFO] \- org.junit.jupiter:junit-jupiter:jar:5.11.4:test
[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:5.11.4:test
[INFO] ... Обозначения: +- — очередная зависимость на этом уровне, \- — последняя на уровне, | — продолжение ветки. Формат строки — groupId:artifactId:type:version:scope (при наличии classifier он стоит перед версией).
Неочевидный момент: проигравшей версии в дереве не видно
Обычный dependency:tree показывает уже разрешённое дерево. В выводе выше у http-lib нет ни одной дочерней строки: Guava 33.4.0-jre, которую она запрашивала, отброшена, и по дереву не скажешь, что конфликт вообще был. Чтобы увидеть отброшенные версии, нужен режим -Dverbose.
-Dverbose добавляет в дерево отброшенные узлы с причиной. Поддержка этой опции зависит от версии maven-dependency-plugin: в ранних версиях ветки 3.x она не работала и была возвращена позже (в 3.2.0), поэтому при странном результате укажите свежую версию плагина явно, например mvn org.apache.maven.plugins:maven-dependency-plugin:3.8.1:tree -Dverbose. Примерный вид строк:
mvn dependency:tree -Dverbose
[INFO] +- com.example:report-lib:jar:1.0.0:compile
[INFO] | \- com.google.guava:guava:jar:19.0:compile
[INFO] +- com.example:http-lib:jar:2.0.0:compile
[INFO] | \- (com.google.guava:guava:jar:33.4.0-jre:compile - omitted for conflict with 19.0) -Dincludes оставляет в дереве только пути к нужной библиотеке — на большом проекте это единственный способ быстро ответить на вопрос «кто притащил эту версию?». Формат фильтра: groupId:artifactId, допускаются *; фильтр можно сочетать с -Dverbose:
mvn dependency:tree -Dincludes=com.google.guava:guava [INFO] com.examclouds:shop:jar:1.0-SNAPSHOT
[INFO] \- com.example:report-lib:jar:1.0.0:compile
[INFO] \- com.google.guava:guava:jar:19.0:compile В IntelliJ IDEA то же дерево показывает узел Dependencies в окне Maven, а в Ultimate-версии есть отдельная диаграмма зависимостей с подсветкой конфликтов.
Анализ используемых зависимостей
mvn dependency:analyze проверяет байткод скомпилированных классов и выводит два списка: Used undeclared — классы библиотеки используются в коде, но сама она пришла транзитивно (её стоит объявить явно); Unused declared — зависимость объявлена, но её классы в коде не встречаются (кандидат на удаление или смену scope).
mvn dependency:analyze
[WARNING] Used undeclared dependencies found:
[WARNING] com.google.guava:guava:jar:19.0:compile
[WARNING] Unused declared dependencies found:
[WARNING] org.apache.commons:commons-lang3:jar:3.17.0:compile Анализ эвристический: он не видит библиотек, загружаемых через рефлексию, ServiceLoader или конфигурацию, и не знает про библиотеки, нужные только компилятору (Lombok). Такие зависимости он может пометить как unused — прежде чем удалять, проверьте сборку и запуск.
Как решить конфликт версий: явная версия и exclusions
Конфликт версий в Maven решают тремя способами: объявляют нужную версию библиотеки напрямую в своём pom.xml (глубина 1 всегда выигрывает), исключают нежелательную транзитивную зависимость через <exclusions> или фиксируют версию в <dependencyManagement>. Для одной-двух библиотек в небольшом проекте подходит любой; в многомодульных проектах и при наборах связанных библиотек предпочтителен dependencyManagement.
| Способ | Что делает | Когда подходит | Недостаток |
|---|---|---|---|
| Прямая зависимость с нужной версией | Библиотека оказывается на глубине 1 и выигрывает по nearest wins | Ваш код и так использует эту библиотеку | Добавляет зависимость, которую код может и не использовать; dependency:analyze отметит её как unused |
<exclusions> | Убирает транзитивную зависимость из конкретной ветки дерева | Библиотека не нужна вовсе или её заменяет другая (мост логирования) | Действует только на одну ветку; другая библиотека может принести то же самое снова |
<dependencyManagement> | Фиксирует версию для всех вхождений библиотеки в дереве, не подключая её | Почти всегда, особенно в многомодульных проектах | Версию нужно поддерживать вручную (или через BOM) |
Явная версия для примера из начала урока:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.0-jre</version>
</dependency> После этого mvn dependency:tree -Dincludes=com.google.guava:guava покажет Guava прямо под корнем проекта с версией 33.4.0-jre, и NoSuchMethodError исчезнет. Теперь уже report-lib работает с Guava новее той, под которую её собирали, — прогоните тесты, затрагивающие её код: между мажорными версиями Guava удаляет устаревшие API.
exclusions — исключение транзитивной зависимости. Классический пример: org.apache.httpcomponents:httpclient 4.5.x тянет за собой commons-logging, а в проекте логирование идёт через SLF4J. commons-logging исключают и вместо него подключают мост jcl-over-slf4j, который предоставляет те же классы org.apache.commons.logging.*, но перенаправляет вызовы в SLF4J:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
<exclusions>
<exclusion>
<!-- version is not specified: all versions are excluded -->
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>2.0.16</version>
</dependency> В <exclusion> указываются только groupId и artifactId, без версии. Исключение действует лишь на ветку дерева под той зависимостью, внутри которой оно записано: если commons-logging придёт ещё и через другую библиотеку, её нужно исключить и там (проверьте через dependency:tree -Dincludes=commons-logging:commons-logging). Исключив зависимость без замены, вы рискуете получить NoClassDefFoundError при первом обращении библиотеки к её классам.
dependencyManagement: версии в одном месте
Раздел <dependencyManagement> задаёт версии (и при необходимости scope и exclusions) для зависимостей, но сам ничего не подключает: библиотека попадёт в проект, только если объявлена в обычном <dependencies> — уже без <version>. Управляемая версия применяется и к транзитивным вхождениям этой библиотеки, поэтому dependencyManagement — главный способ раз и навсегда закрыть конфликт версий.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.0-jre</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- guava is NOT declared here, but report-lib still gets 33.4.0-jre -->
<dependency>
<groupId>com.example</groupId>
<artifactId>report-lib</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>http-lib</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies> Теперь dependency:tree покажет Guava под report-lib с версией 33.4.0-jre, а в режиме -Dverbose у узла обычно появляется пометка вида version managed from 19.0. Если библиотеку, которой нет в dependencyManagement, объявить без версии, сборка остановится уже на чтении модели:
[ERROR] 'dependencies.dependency.version' for com.google.code.gson:gson:jar is missing. Наследование. Чаще всего dependencyManagement пишут в родительском POM: дочерние модули объявляют нужные им библиотеки без версий, и все модули гарантированно используют одни и те же версии. Сами зависимости при этом модулям не навязываются, в отличие от <dependencies> родителя, которые наследуются всеми. Устройство многомодульного проекта будет разобрано в отдельном уроке; как работает <parent> и где увидеть итоговые управляемые версии (mvn help:effective-pom), описано в разделе parent, Super POM и effective POM. Сами номера версий удобно вынести в свойства properties.
Явная версия в <dependency> проекта имеет приоритет над управляемой. Поэтому, если версия «не применяется», первым делом ищите, не указана ли она прямо в зависимости модуля.
BOM: импорт набора версий
BOM (Bill of Materials) — это POM-файл с packaging = pom, который содержит только <dependencyManagement> с согласованными версиями набора связанных библиотек. Его подключают в свой <dependencyManagement> со <scope>import</scope> и <type>pom</type>: все управляемые версии из BOM становятся вашими, а сами библиотеки дальше объявляются без версий.
Пример с junit-bom: версия указывается один раз, а модули JUnit (junit-jupiter, junit-platform-suite и другие) гарантированно получают совместимые версии.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.11.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<!-- version comes from junit-bom -->
<scope>test</scope>
</dependency>
</dependencies> Своими BOM-файлами управляют версиями многие экосистемы: Spring Boot (org.springframework.boot:spring-boot-dependencies), Jackson (com.fasterxml.jackson:jackson-bom), Netty, AWS SDK. В Spring Boot-проектах те же версии чаще получают через родителя spring-boot-starter-parent, а импорт BOM используют, когда родитель уже занят (например, корпоративным POM):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<!-- check the latest version on Maven Central -->
<version>3.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement> Порядок нескольких BOM. Если два импортированных BOM управляют одной и той же библиотекой, побеждает тот, что объявлен в <dependencyManagement> первым. Записи, объявленные в вашем dependencyManagement напрямую, имеют приоритет над импортированными. Отсюда правило: чтобы поднять версию одной библиотеки из BOM, объявите её в dependencyManagement явно или поставьте BOM этой библиотеки (например, jackson-bom) выше общего BOM фреймворка.
Нюанс: property из BOM не переопределяется при импорте
С родителем spring-boot-starter-parent версию, например, Jackson меняют одним свойством в своём <properties> (имя свойства — из документации Spring Boot). При импорте spring-boot-dependencies со scope = import этот приём не работает: свойства BOM вычисляются внутри самого BOM, а ваши <properties> на них не влияют. Версия молча остаётся прежней. При импорте переопределяйте версию записью в dependencyManagement (или BOM библиотеки), объявленной до импорта общего BOM.
На чём чаще всего ошибаются
Большинство проблем с зависимостями Maven не ломают компиляцию: сборка проходит, а ошибка появляется при запуске, на сервере или у потребителей вашей библиотеки. В таблице — симптом, причина и исправление; для постоянного контроля в CI используют плагин maven-enforcer-plugin.
| Что произошло | Причина | Как исправить |
|---|---|---|
'dependencies.dependency.version' for ... is missing | Версия не указана, а в dependencyManagement (своём, родительском, импортированном BOM) этой библиотеки нет | Указать <version> или добавить библиотеку/её BOM в dependencyManagement |
ClassNotFoundException или NoClassDefFoundError при запуске, хотя всё компилируется | Зависимость объявлена как provided, а среда выполнения её не предоставляет (обычный java -jar, а не сервлет-контейнер) | Сменить scope на compile или runtime; provided оставлять только для того, что точно даст сервер или JDK |
package org.junit.jupiter.api does not exist (или org.mockito) при mvn compile | Класс из test-зависимости используется в src/main/java | Перенести код в src/test/java; если библиотека нужна основному коду — убрать scope = test |
NoSuchMethodError в библиотеке после добавления новой зависимости | Новая зависимость изменила выбор версии по nearest wins | dependency:tree -Dverbose -Dincludes=..., затем зафиксировать версию в dependencyManagement |
| Релизная сборка через месяц ведёт себя иначе, чем при выпуске | В релизе осталась -SNAPSHOT-зависимость: её содержимое меняется | Перед выпуском заменить SNAPSHOT на релизные версии; правило requireReleaseDeps в enforcer запрещает их автоматически |
Дубли классов, «случайный» выбор реализации, ClassCastException между «одинаковыми» классами | Одни и те же классы пришли в двух артефактах с разными groupId/artifactId. Например, javax.servlet:javax.servlet-api:4.0.1 и jakarta.servlet:jakarta.servlet-api:4.0.4 оба содержат пакет javax.servlet. Для Maven это разные библиотеки, конфликта он не видит, nearest wins не срабатывает | Найти обе через dependency:tree и вручную исключить лишнюю в каждой ветке через <exclusions>; дубли классов ловит правило banDuplicateClasses из extra-enforcer-rules (MojoHaus) |
Перестановка двух <dependency> «чинит» или «ломает» проект | Конфликт на одинаковой глубине решается порядком объявления | Зафиксировать версию явно, а не полагаться на порядок |
Код компилируется с классами библиотеки, которой нет в pom.xml | Используется транзитивная compile-зависимость | Объявить её явно (подсказывает dependency:analyze в списке Used undeclared) |
Maven Enforcer. Плагин maven-enforcer-plugin останавливает сборку, если нарушено заданное правило. Для зависимостей полезны: dependencyConvergence — ошибка, если одна библиотека встречается в дереве в разных версиях (заставляет явно решить каждый конфликт); requireUpperBoundDeps — ошибка, если по nearest wins выбрана версия ниже той, которую требует какая-то транзитивная зависимость (ровно случай из начала урока); requireReleaseDeps — запрет SNAPSHOT-зависимостей; bannedDependencies — запрет конкретных артефактов. Набор правил и их параметры зависят от версии плагина. Как подключать плагины и привязывать их к фазам, описано в уроках Использование плагинов в Maven и Maven — жизненный цикл сборки.
Часто задаваемые вопросы
Чем scope provided отличается от optional?
Они отвечают на разные вопросы. provided говорит, что библиотеку при запуске даст среда (сервлет-контейнер, JDK), поэтому её нет в classpath запуска и в WEB-INF/lib. optional говорит, что зависимость нужна только части функций библиотеки, поэтому она не передаётся проектам-потребителям, но в самой библиотеке ведёт себя по своему scope. Общее у них одно: обе не становятся транзитивными. Подробнее — в разделах Scope и Транзитивные зависимости.
Как исключить сразу все транзитивные зависимости библиотеки?
Начиная с Maven 3.2.1 в <exclusion> можно указать * и в groupId, и в artifactId — тогда библиотека подключится без своих зависимостей. Приём нужен редко: всё, что библиотека реально использует, придётся объявить самостоятельно, иначе при запуске будет NoClassDefFoundError.
Можно ли указать диапазон версий вместо точной версии?
Можно: Maven понимает диапазоны вида [1.0,2.0) (от 1.0 включительно до 2.0 не включительно). Но результат сборки тогда зависит от того, что опубликовано в репозитории на момент сборки, и сегодняшняя сборка может отличаться от вчерашней. Для воспроизводимости используйте точные версии, а обновляйте их осознанно — например, с помощью versions-maven-plugin (цель versions:display-dependency-updates показывает доступные обновления).
Почему зависимость из dependencyManagement не появилась в проекте?
Потому что dependencyManagement только задаёт версию «на случай использования» и ничего не подключает. Чтобы библиотека попала в classpath, объявите её в обычном <dependencies> — уже без <version>. То же относится к BOM: импорт BOM не добавляет ни одной библиотеки. Пример — в разделе dependencyManagement.
Как узнать, какая библиотека притащила ненужную зависимость?
Запустите mvn dependency:tree -Dincludes=groupId:artifactId с координатами ненужной библиотеки: в выводе останутся только пути от вашего проекта к ней, и первый узел каждого пути — ваша прямая зависимость, внутри которой нужно поставить <exclusion>. Если путей несколько, исключать придётся в каждом. Пример вывода — в разделе mvn dependency:tree.
Комментарии