Зависимости в Maven: scope, транзитивные зависимости, конфликты версий и BOM - Вопросы
Всего: 5 вопросов
1. Что такое scope зависимости в Maven и чем отличаются compile, provided, runtime, test, system и import?
Что такое scope зависимости в Maven и чем отличаются compile, provided, runtime, test, system и import?
Scope (область видимости) определяет, в какие classpath Maven добавит зависимость (компиляция основного кода, компиляция и запуск тестов, запуск) и передаётся ли она транзитивно проектам, которые зависят от вашего. По умолчанию - compile.
- compile - везде, включая
WEB-INF/libwar; транзитивна (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 влияет на их передачу?
Что такое транзитивные зависимости в 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 и как найти и исправить такой конфликт?
Как 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 отличается от 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?
Какие ошибки с зависимостями 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). Набор правил и параметры зависят от версии плагина.