Зависимости Maven в Java
Объявляйте и управляйте зависимостями Java в Maven: dependency, scope и транзитивное разрешение.
Реальные Java-проекты редко существуют в изоляции. Они подключают фреймворки для логирования, HTTP-клиенты, парсеры JSON и тестовые библиотеки, написанные другими разработчиками. Задача Maven — загрузить эти библиотеки, затем загрузить их зависимости и собрать единый согласованный classpath, не требуя вручную отслеживать ни одного JAR-файла. Понимание того, как это работает, — разница между сборкой, которая просто работает, и вечером, потраченным на разбор NoSuchMethodError.
В этой главе рассказывается, как именуется зависимость (координаты), как области видимости управляют тем, где доступна каждая библиотека, как Maven обходит граф транзитивных зависимостей и как разрешает конфликты версий. Предполагается, что у вас уже есть pom.xml; если нет — начните с главы Maven POM.
Координаты: как именуется зависимость
Каждый артефакт в мире Maven идентифицируется набором координат. Три обязательных: groupId (кто публикует — обычно перевёрнутый домен), artifactId (название проекта) и version. Вместе они указывают ровно на один JAR в репозитории.
Зависимость объявляется внутри блока <dependencies> файла pom.xml:
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>Сокращение groupId:artifactId:version называется GAV-строкой, и вы будете видеть её повсюду: в сообщениях об ошибках, в дереве зависимостей и на страницах центрального репозитория. Четвёртая координата — type (по умолчанию jar), пятая — classifier (для вариантов вроде sources или javadoc) — дополняют полный адрес.
Области видимости: когда доступна зависимость
Не каждая зависимость нужна на каждом classpath. Тестовый фреймворк не должен попадать в production-JAR, а API сервлетов, предоставляемый сервером приложений, не должен включаться дважды. Maven управляет этим с помощью элемента <scope>.
| Область | Компиляция | Тест | Выполнение | В пакете | Типичное применение |
|---|---|---|---|---|---|
compile (по умолчанию) | Да | Да | Да | Да | Основные библиотеки, используемые напрямую |
provided | Да | Да | Нет | Нет | API, предоставляемые контейнером (сервлет, JDBC-драйвер) |
runtime | Нет | Да | Да | Да | Реализации, нужные только во время выполнения |
test | Нет | Да | Нет | Нет | JUnit, Mockito, библиотеки утверждений |
system | Да | Да | Нет | Нет | Локальные JAR по абсолютному пути (избегать) |
Зависимость с областью test — наиболее распространённый нестандартный случай. JUnit никогда не попадёт в поставляемый артефакт:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>Транзитивные зависимости
Когда вы зависите от библиотеки, вы также зависите от всего, от чего зависит она. Maven читает опубликованный pom.xml каждого артефакта, рекурсивно следует этим объявлениям и автоматически добавляет весь граф в ваш classpath. Эти косвенные записи называются транзитивными зависимостями.
Именно поэтому одна строка <dependency> для веб-фреймворка может притянуть десятки JAR-файлов, которые вы явно не указывали. Область видимости продолжает действовать при обходе графа: зависимость с областью test не тянет свои транзитивные зависимости в classpath компиляции, а зависимости provided вообще не распространяются транзитивно.
Полный граф можно увидеть с помощью плагина dependency:
$ mvn dependency:tree
[INFO] com.example:app:jar:1.0
[INFO] +- org.web:server:jar:2.4:compile
[INFO] | +- org.log:log:jar:1.2:compile
[INFO] | \- org.json:json:jar:1.7:compile - omitted for conflict with 1.9
[INFO] \- org.json:json:jar:1.9:compileРазрешение конфликтов версий
Столь глубокий граф почти всегда запрашивает один и тот же артефакт в двух разных версиях. Maven не может поместить оба в один classpath, поэтому выбирает один по принципу ближайший выигрывает: побеждает версия, объявленная на наименьшей глубине от корня проекта, остальные исключаются как конфликтующие.
В приведённом выше дереве ваш проект запрашивает org.json:json:1.9 напрямую (глубина 1), тогда как org.web:server запрашивает 1.7 транзитивно (глубина 2). Объявление на глубине 1 побеждает. Если два кандидата находятся на одинаковой глубине, побеждает тот, что объявлен первым в pom.xml.
Когда автоматический выбор неверен, вы берёте управление явно. Прямая зависимость всегда выигрывает, или можно зафиксировать версии для всего проекта с помощью <dependencyManagement>:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>1.9</version>
</dependency>
</dependencies>
</dependencyManagement>Чтобы полностью отсечь нежелательную транзитивную ветвь, используйте <exclusions>:
<dependency>
<groupId>org.web</groupId>
<artifactId>server</artifactId>
<version>2.4</version>
<exclusions>
<exclusion>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</exclusion>
</exclusions>
</dependency>Полный пример
Maven сам по себе недоступен в этом исполнителе кода, поэтому программа ниже моделирует его резолвер на чистом Java. Она публикует несколько артефактов в небольшой in-memory репозиторий, а затем выполняет тот же обход в ширину и медиацию «ближайший выигрывает», которые Maven использует для сворачивания графа зависимостей в единый classpath. Обратите внимание, как более глубокий org.json:json:1.7 проигрывает более мелкому 1.9.
Что можно вынести из результата выполнения:
- Разрешённый classpath содержит каждый артефакт ровно один раз — это отражает то, как Maven сворачивает граф в единый набор JAR без дублирующихся записей group:artifact.
org.json:jsonприсутствует в версии1.9, а не1.7, поскольку медиация «ближайший выигрывает» сохраняет кандидата, найденного на меньшей глубине (глубина 1 побеждает глубину 2).- Столбец
depthделает понятие «ближайший» конкретным:app:app— глубина 0, его прямые зависимости — глубина 1, транзитивно подключённыйorg.log:log— глубина 2. - «Tree edges visited: 4» считает объявленные связи зависимостей, а «Distinct artifacts: 4» показывает граф, свёрнутый до четырёх уникальных координат после медиации.
- Координата пропускается при первом повторном появлении (
depthOf.containsKey(ga)), именно поэтому более глубокий1.7«исключается как конфликтующий», а не добавляется второй раз.
После того как зависимости разрешены корректно, следующий вопрос — когда Maven загружает, компилирует, тестирует и упаковывает их. Этот порядок определяется фазами сборки, рассмотренными в главе жизненный цикл Maven.