W3docs

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

java— editable, runs on the server

Что можно вынести из результата выполнения:

  • Разрешённый 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.

Практика

Практика
В Maven в графе зависимостей присутствуют две версии одного артефакта: версия 1.9 объявлена напрямую в вашем pom (глубина 1) и версия 1.7 подключена транзитивно (глубина 2). Какая версия окажется в classpath?
В Maven в графе зависимостей присутствуют две версии одного артефакта: версия 1.9 объявлена напрямую в вашем pom (глубина 1) и версия 1.7 подключена транзитивно (глубина 2). Какая версия окажется в classpath?
Was this page helpful?