Зависимости в Maven: scope, транзитивные зависимости, конфликты версий и BOM - Вопросы

Всего: 5 вопросов

1. 

Что такое scope зависимости в Maven и чем отличаются compile, provided, runtime, test, system и import?

Scope (область видимости) определяет, в какие classpath Maven добавит зависимость (компиляция основного кода, компиляция и запуск тестов, запуск) и передаётся ли она транзитивно проектам, которые зависят от вашего. По умолчанию - compile.

  • compile - везде, включая WEB-INF/lib war; транзитивна (Guava, Jackson).
  • provided - компиляция и тесты, но не запуск и не war: библиотеку даст среда (сервлет-контейнер, JDK); не транзитивна (jakarta.servlet-api, Lombok).
  • runtime - не нужна для компиляции основного кода, есть в тестах, при запуске и в war; транзитивна (JDBC-драйвер, реализация логирования).
  • test - только компиляция и запуск тестов; не транзитивна (JUnit, Mockito).
  • system - устарел: локальный jar по абсолютному systemPath, сборка непереносима. Лучше установить jar в локальный или корпоративный репозиторий.
  • import - не попадает ни в один classpath; допустим только в <dependencyManagement> вместе с <type>pom</type> и подставляет версии из BOM.

Попадание в war - поведение maven-war-plugin; обычный jar от maven-jar-plugin зависимостей не содержит вовсе. Ошибка в scope обычно видна не при компиляции, а при запуске. Lombok со scope provided на JDK 23+ обычно указывают ещё и в annotationProcessorPaths maven-compiler-plugin: javac по умолчанию не запускает процессоры аннотаций из classpath, если обработка аннотаций не настроена явно.

2. 

Что такое транзитивные зависимости в Maven и как scope влияет на их передачу?

Транзитивная зависимость - зависимость вашей зависимости: если проект подключает A, а в POM библиотеки A объявлена B, Maven добавит и B, и её зависимости на любую глубину. Их версии выбираете уже не вы, а авторы библиотеки.

Итоговый scope по документации Maven зависит от сочетания scope прямой и транзитивной зависимости:

  • транзитивные provided и test в проект не попадают;
  • при прямой compile: транзитивная compile остаётся compile, runtime - runtime;
  • при прямой provided, runtime или test транзитивные compile и runtime получают scope прямой. Поэтому библиотека, пришедшая через test-зависимость, доступна только тестам.

optional ставит автор библиотеки на зависимость, нужную части функций: потребители её не получают и подключают сами, иначе вызов такой функции упадёт с NoClassDefFoundError.

Транзитивной compile-зависимостью легко начать пользоваться случайно; документация Maven советует объявлять такие зависимости явно. Их находит mvn dependency:analyze (Used undeclared), а Unused declared - объявленные, но неиспользуемые. Анализ эвристический: библиотеки, загружаемые через рефлексию, ServiceLoader или конфигурацию, и нужные только компилятору (Lombok) он может пометить как unused.

3. 

Как Maven решает конфликт версий по правилу nearest wins и как найти и исправить такой конфликт?

Конфликт версий - одна библиотека (те же groupId и artifactId) встречается в дереве в разных версиях. Maven оставляет одну по правилу nearest wins: побеждает версия, ближайшая к проекту по глубине дерева, при равной глубине - встретившаяся первой в порядке объявления. Новизна не учитывается. Версия, объявленная напрямую или заданная в dependencyManagement проекта, важнее транзитивных.

Сборка проходит, а при запуске библиотека, рассчитанная на другую версию, падает с NoSuchMethodError, NoClassDefFoundError/ClassNotFoundException или AbstractMethodError/IncompatibleClassChangeError.

Как найти: mvn dependency:tree показывает уже разрешённое дерево без проигравших версий; -Dverbose добавляет отброшенные узлы (поддержка зависит от версии maven-dependency-plugin), -Dincludes=groupId:artifactId оставляет только пути к библиотеке.

Как решить: объявить нужную версию напрямую (глубина 1 выигрывает), исключить лишнюю транзитивную зависимость через <exclusions> (только groupId и artifactId, действует на одну ветку) или зафиксировать версию в <dependencyManagement> - предпочтительно в многомодульных проектах. Перестановка строк держится на случайности; после подъёма версии проверьте старую библиотеку тестами.

4. 

Чем dependencyManagement отличается от dependencies и что такое BOM в Maven?

<dependencyManagement> задаёт версии (при необходимости scope и exclusions), но ничего не подключает: библиотека попадёт в проект, только если объявлена в обычном <dependencies> - уже без <version>. Управляемая версия применяется и к транзитивным вхождениям, поэтому это главный способ закрыть конфликт версий. Обычно его пишут в родительском POM: модули объявляют библиотеки без версий и получают одинаковые версии, а сами зависимости им не навязываются, в отличие от <dependencies> родителя. Явная версия в <dependency> проекта важнее управляемой.

BOM (Bill of Materials) - POM с packaging = pom, содержащий только dependencyManagement с согласованными версиями набора библиотек (junit-bom, spring-boot-dependencies, jackson-bom). Его подключают в свой dependencyManagement со <scope>import</scope> и <type>pom</type>; импорт BOM не добавляет ни одной библиотеки.

  • Если два импортированных BOM управляют одной библиотекой, побеждает объявленный первым.
  • Записи, объявленные в вашем dependencyManagement напрямую, важнее импортированных.
  • Свойства BOM вычисляются внутри него: при импорте ваши <properties> на них не влияют (в отличие от родителя spring-boot-starter-parent), и версия молча остаётся прежней.
5. 

Какие ошибки с зависимостями Maven встречаются чаще всего и чем помогает maven-enforcer-plugin?

Большинство таких ошибок не ломают компиляцию: проблема видна при запуске, на сервере или у потребителей библиотеки.

  • provided без среды: при обычном java -jar, а не в сервлет-контейнере, - ClassNotFoundException или NoClassDefFoundError.
  • Нет версии: 'dependencies.dependency.version' for ... is missing - библиотеки нет в dependencyManagement.
  • SNAPSHOT-зависимость в релизе: её содержимое меняется, и сборка через месяц ведёт себя иначе.
  • Одни классы в разных артефактах: javax.servlet:javax.servlet-api:4.0.1 и jakarta.servlet:jakarta.servlet-api:4.0.4 оба содержат пакет javax.servlet. Для Maven это разные библиотеки, конфликта он не видит, nearest wins не срабатывает: лишнюю находят через dependency:tree и вручную исключают в каждой ветке.
  • Порядок вместо версии: перестановка двух dependency «чинит» проект - версию надо зафиксировать явно.

maven-enforcer-plugin останавливает сборку при нарушении правила: dependencyConvergence - одна библиотека в дереве в разных версиях; requireUpperBoundDeps - по nearest wins выбрана версия ниже требуемой транзитивной зависимостью; requireReleaseDeps - запрет SNAPSHOT; bannedDependencies - запрет конкретных артефактов. Дубли классов ловит banDuplicateClasses из extra-enforcer-rules (MojoHaus). Набор правил и параметры зависят от версии плагина.

Страница 1 из 1