Вы учитесь, сравнивая с тем, что уже знаете. Недавно я обжегся, предположив, что Rust работает как Java в отношении разрешения версий транзитивных зависимостей. В этой статье я хочу сравнить эти два подхода.Вы учитесь, сравнивая с тем, что уже знаете. Недавно я обжегся, предположив, что Rust работает как Java в отношении разрешения версий транзитивных зависимостей. В этой статье я хочу сравнить эти два подхода.

Разрешение версий транзитивных зависимостей в Rust и Java: сравнение двух подходов

2025/09/21 23:00

Вы учитесь, сравнивая с тем, что уже знаете. Недавно я столкнулся с проблемой, предположив, что Rust работает так же, как Java, в отношении разрешения версий транзитивных зависимостей. В этой статье я хочу сравнить оба подхода.

Зависимости, транзитивность и разрешение версий

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

\ При разработке любого проекта выше уровня Hello World, вероятно, вы столкнетесь с проблемами, с которыми сталкивались другие. Если проблема распространена, высока вероятность, что кто-то был достаточно добр и сознателен, чтобы упаковать код, решающий её, для повторного использования другими. Теперь вы можете использовать этот пакет и сосредоточиться на решении своей основной проблемы. Именно так индустрия создает большинство проектов сегодня, даже если это приносит другие проблемы: вы стоите на плечах гигантов.

\ Языки поставляются с инструментами сборки, которые могут добавлять такие пакеты в ваш проект. Большинство из них называют пакеты, которые вы добавляете в свой проект, зависимостями. В свою очередь, зависимости проектов могут иметь свои собственные зависимости: последние называются транзитивными зависимостями.

Transitive dependencies

На диаграмме выше C и D являются транзитивными зависимостями.

\ Транзитивные зависимости имеют свои собственные проблемы. Самая большая из них возникает, когда транзитивная зависимость требуется из разных путей, но в разных версиях. На диаграмме ниже A и B оба зависят от C, но от разных его версий.

Какую версию C должен включить инструмент сборки в ваш проект? У Java и Rust разные ответы. Давайте опишем их по очереди.

Разрешение версий транзитивных зависимостей в Java

Напоминание: код Java компилируется в байткод, который затем интерпретируется во время выполнения (и иногда компилируется в нативный код, но это выходит за рамки нашей текущей проблемы). Сначала я опишу разрешение зависимостей во время выполнения и разрешение зависимостей во время сборки.

\ Во время выполнения Java Virtual Machine предлагает концепцию classpath. При необходимости загрузить класс, среда выполнения ищет в настроенном classpath по порядку. Представьте следующий класс:

public static Main {     public static void main(String[] args) {         Class.forName("ch.frankel.Dep");     } } 

\ Давайте скомпилируем и выполним его:

java -cp ./foo.jar:./bar.jar Main 

\ Приведенный выше код сначала будет искать в foo.jar класс ch.frankel.Dep. Если найден, он останавливается и загружает класс, независимо от того, может ли он также присутствовать в bar.jar; если нет, он ищет дальше в классе bar.jar. Если все еще не найден, он завершается с ошибкой ClassNotFoundException.

\ Механизм разрешения зависимостей Java во время выполнения упорядочен и имеет гранулярность на уровне класса. Это применяется независимо от того, запускаете ли вы класс Java и определяете classpath в командной строке, как показано выше, или запускаете JAR, который определяет classpath в своем манифесте.

\ Давайте изменим приведенный выше код на следующий:

public static Main {     public static void main(String[] args) {         var dep = new ch.frankel.Dep();     } } 

\ Поскольку новый код напрямую ссылается на Dep, новый код требует разрешения класса во время компиляции. Разрешение classpath работает таким же образом:

javac -cp ./foo.jar:./bar.jar Main 

\ Компилятор ищет Dep в foo.jar, затем в bar.jar, если не найден. Вышеописанное — это то, что вы узнаете в начале своего пути изучения Java.

\ После этого вашей единицей работы становится Java Archive, известный как JAR, вместо класса. JAR — это улучшенный ZIP-архив с внутренним манифестом, который указывает его версию.

\ Теперь представьте, что вы пользователь foo.jar. Разработчики foo.jar устанавливают определенный classpath при компиляции, возможно, включая другие JAR-файлы. Вам понадобится эта информация для запуска собственной команды. Как разработчик библиотеки передает эти знания нижестоящим пользователям?

\ Сообщество предложило несколько идей для ответа на этот вопрос: первым ответом, который прижился, был Maven. Maven имеет концепцию Project Object Model, где вы устанавливаете метаданные вашего проекта, а также зависимости. Maven может легко разрешать транзитивные зависимости, потому что они также публикуют свой POM со своими собственными зависимостями. Следовательно, Maven может отслеживать зависимости каждой зависимости вплоть до листовых зависимостей.

\ Теперь вернемся к постановке проблемы: как Maven разрешает конфликты версий? Какую версию зависимости Maven разрешит для C, 1.0 или 2.0?

\ Документация ясна: ближайшую.

Dependency resolution with the same dependency in different versions

На диаграмме выше путь к v1 имеет расстояние два, один до B, затем один до C; между тем, путь к v2 имеет расстояние три, один до A, затем один до D, и наконец один до C. Таким образом, кратчайший путь указывает на v1.

\ Однако на исходной диаграмме обе версии C находятся на одинаковом расстоянии от корневого артефакта. Документация не дает ответа. Если вам интересно, это зависит от порядка объявления A и B в POM! В итоге Maven возвращает одну версию дублированной зависимости для включения в classpath компиляции.

\ Если A может работать с C v2.0 или B с C 1.0, отлично! Если нет, вам, вероятно, придется обновить версию A или понизить версию B, чтобы разрешенная версия C работала с обоими. Это ручной процесс, который болезнен — спросите меня, откуда я знаю. Хуже того, вы можете обнаружить, что нет версии C, которая работает как с A, так и с B. Пора заменить A или B.

Разрешение версий транзитивных зависимостей в Rust

Rust отличается от Java в нескольких аспектах, но я думаю, что следующие являются наиболее релевантными для нашего обсуждения:

  • Rust имеет одинаковое дерево зависимостей во время компиляции и во время выполнения
  • Он предоставляет инструмент сборки из коробки, Cargo
  • Зависимости разрешаются из исходного кода

\ Давайте рассмотрим их по одному.

\ Java компилируется в байткод, затем вы запускаете последний. Вам нужно установить classpath как во время компиляции, так и во время выполнения. Компиляция с определенным classpath и запуск с другим может привести к ошибкам. Например, представьте, что вы компилируете с классом, от которого зависите, но класс отсутствует во время выполнения. Или, альтернативно, он присутствует, но в несовместимой версии.

\ В отличие от этого модульного подхода, Rust компилирует в уникальный нативный пакет код крейта и каждую зависимость. Более того, Rust предоставляет свой собственный инструмент сборки, тем самым избегая необходимости запоминать особенности различных инструментов. Я упомянул Maven, но другие инструменты сборки, вероятно, имеют разные правила для разрешения версии в вышеуказанном случае использования.

\ Наконец, Java разрешает зависимости из бинарных файлов: JAR-файлов. Напротив, Rust разрешает зависимости из исходных кодов. Во время сборки Cargo разрешает все дерево зависимостей, загружает все необходимые исходные коды и компилирует их в правильном порядке.

\ С учетом этого, как Rust разрешает версию зависимости C в исходной проблеме? Ответ может показаться странным, если вы пришли из мира Java, но Rust включает обе. Действительно, на диаграмме выше Rust скомпилирует A с C v1.0 и скомпилирует B с C v2.0. Проблема решена.

Заключение

Языки JVM, и Java в частности, предлагают как classpath времени компиляции, так и classpath времени выполнения. Это позволяет модульность и повторное использование, но открывает дверь для проблем, связанных с разрешением classpath. С другой стороны, Rust собирает ваш крейт в единый автономный бинарный файл, будь то библиотека или исполняемый файл.

\ Для дальнейшего изучения:

  • Maven - Введение в механизм зависимостей
  • Эффективный Rust - Пункт 25: Управляйте своим графом зависимостей

Первоначально опубликовано на A Java Geek 14 сентября 2025 г.

Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу service@support.mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.

Вам также может быть интересно