Maven ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-11-06

Зависимости в 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.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.