- PVSM.RU - https://www.pvsm.ru -
В ближайшую субботу мы с Женей Борисовым будем выступать в Питере на JUG.ru [1]. Будет много веселого трэша и интересной инфы (иногда не разберешь, где проходит граница), и одно из моих выступлений будет посвящено WTF-нутости модульной разработке программ. Я расскажу несколько ужастиков, один из которых будет о том, как все пытаются быстро, гибко и корректно описать зависимости в проекте, и что из этого обычно получается. Интересно? Тогда добро пожаловать в ад!
Скорее, конечно, «Хороший, Удобный и WTF-ный».
Любой современный инструмент сборки (особенно в мире JVM) включает в себя (либо имеет легко подключающийся) менеджер зависимостей (a.k.a. dependency manager). Самые известные, это, конечно, Apache Maven [2], Apache Ivy [3] (менеджер зависимостей для Apache Ant), и Gradle [4].
Одна из главных функций инструментов сборки в мире JVM это создавать classpath-ы. Используются они во время процесса сборки много где — для компиляции кода, для запуска тестов, для сборки архивов (war-ов, ear-ов, или дистирбутивов и установщиков) и даже для настройки проектов в IDE.
Для облегчения процесса нахождения в сети, скачивания, хранения и конфигурации зависимостей и существуют менеджеры зависимостей. Вы декларируете, что вам нужен, например, commons-lang
, и вуаля, он у вас есть.
Транзитивная зависимость — это тот артефакт, от которого зависит прямая зависимость проекта. Представьте себе следующую ситуацию:
Наш проект A
зависит от двух артефактов — E
и B
. На этом прямые зависимости заканчиваются и начинаются транзитивные [5] (C, D
). В итоге мы получаем цепочки зависимостей, артефакты в которых могут повторяться (D
, в нашем примере)
Очевидно, не для компиляции. Но, для всего остального, пожалуй, нужны — эти артефакты должны находиться в сборках архивов, они должны находиться в classpath для запуска тестов, и пути к ним должны быть отданы через API для интеграции с IDE.
Если мы посмотрим на диаграмму выше, то увидим тот самый конфликт. В classpath проекта A
должны находиться и артефакт D
версии 1 (от него зависит Е), и артефакт D
версии 2 (от него зависит C
)!
JVM (и javac) определяет уникальность класса по его имени (и classloader-у, но в нашем простом примере все классы загружаются одним classloader-ом). В случае, когда в classpath встречаются два класса с одинаковым именем, загружен будет только первый. Если предположить, что в D1
и в D2
находятся классы с одинаковым именем (согласитесь, скорее всего, так и есть), то класс из jar-а, который будет прописан в сгенерированном classpath-е вторым просто не будет загружен. Какой из них будет первый? Это зависит от логики менеджера зависимостей и, в общем случае, неизвестно.
Как вы понимаете, это и есть конфликт:
Кто виноват понятно (Java, а не те, о ком вы подумали), а вот что можно сделать?
Есть несколько стратегий разрешения конфликтов в транзитивных зависимостях (некоторые из них логичные, другие — абсурдные), но, естественно, серебряной пули нет. Давайте посмотрим на некоторые из них:
ivy.coflictManager = {artifact, versionA, versionB ->
//допустим, я полагаюсь на обратную совместимость только библиотек Apache, но не остальных
if(artifact.org.startsWith ('org.apache')){
(versionA <=> versionB) > 0 ? versionA : versionB
} else {
fail()
}
}
В случае нашего примера, при использовании этой имплементации custom, если предположить что org у D1 и D2 начинается с 'org.apache', то в classpath окажется D2, в противном случае, сборка упадет.
Теперь давайте посмотрим, кто из Дер Гроссе Тройки [6] упомянутой выше, что умеет.
В плане менеджеров конфликтов [7] Ivy прекрасен. Они подключаемы, оба варианта latest, а так же fail и all идут в коробке. Custom, правда, ограничен regex-ом, но это лучше, чем ничего. По умолчанию работает latest (по версии).
В первых версиях Gradle (до 0.6, если мне не изменяет память) использовался Ivy как менеджер зависимостей. Соответственно, всё сказанное выше было верно для Gradle тоже, но ребята из Gradleware написали свой менеджер зависимостей (в основном из за проблем с локальным хранилищем Ivy при параллельной сборкe, одного из главных преимуществ Gradle). В процессе выпуска своего менеджера такие «второстепенные» фичи как замена менеджера конфликтов были задвинуты далеко в roadmap, и довольно долгое время Gradle существовал только с latest. Не нравится latest — отключай транзитивные зависимости, и вперед, перечислять всё в ручную. Но, сегодня всё в порядке. Начиная с 1.0 есть fail [8], а с 1.4 и custom [9]тоже.
Ну, ради следующей картинки и был задуман весь пост.
Как вы считаете, какая из версий D попадет в classpath? D1? D2? обе? ни одной? сборка упадет?
Как вы уже, наверняка, догадались, в classpath попадет D1 (что с огромной вероятностью приведет к проблемам, потому что весь код в C, написанный под новую функциональность, которой не существует в D1, просто упадет). Это тот самый чудесный WTF, который я вам обещал. Maven работает с уникальной стратегией nearest [10], которая выбирает в classpath тот артефакт, который находится ближе к корню проекта (А) в дереве проектов.
Корень проблемы лежит в трудности исключения зависимости в Maven. Если, например, вы хотите использовать D2, а не D1, то, по хорошему, вы должны сказать Maven-у: Дорогой Maven, никогда не используй D1. Просто для примера, в Gradle мы бы написали вот так:
configurations {all*.exclude group: 'mygroup', module: 'D', version: '1'}
Проблема в том, что выразить это в Maven нельзя никак. Можно сказать конкретно модулю E: «ты думал у тебя есть зависимость на D? Так вот, ее нет». Это хороший выход, конфликта больше нет, D2 в classpath, win. Но это решение совершенно не масштабируемо. Что если от D1 зависят десятки артефактов? На разных уровнях транзитивности?
Проблема отсутствия глобального exclude была решена в Maven-е очень «интересным» способом. Было решено, что если вы объявили в вашем проекте А зависимость с определенной версией, то только эта версия и попадет в classpath. То есть практически, это ультимативный nearest — ближе чем в A быть не может (это root), поэтому конфликт решён, не нужно искать все места откуда нужно исключать D. По дороге, правда, мы получили очень странное и трудно-предсказуемое поведение в тех случаях, когда A не объявляет D напрямую (см. наш пример), но что есть, то есть.
Достаточно интересно, что идея «то, что пользователь объявил сам — закон» используется в Gradle тоже, и это не мешает им использовать вменяемые стратегии типа latest и fail для всего остального.
Этот прекрасный вопрос (что делать, если бы в нашем примере B зависел от D2) не приходил ребятам из Maven-а в голову на протяжении двух с половиной лет (от релиза 2.0 в октябре 2005 и до версии 2.0.9 в апреле 2008) и какой артефакт будет в classpath было просто неопределенно. В Maven 2.0.9 было принято решение — будет первый!
Как это нам помогает? Правильно, никак. Потому что мы в общем случае не знаем, какой из них будет первый, ведь транзитивные зависимости не проявляют себя пока не случается конфликт (либо пока мы не начинаем расследовать эту загадку). Спасибо, пацаны!
WTF-нутость Maven-а, естественно, не ограничивается чудесным порождением альтернативного разума — стратегией nearest. Но на сегодня, я думаю, хватит. Холивары в комментах всячески приветствуются (если что, я притоплю за Gradle), а все питерцы, приходят на JUG в субботу 31 числа в ПетроКонгресс.
Автор: jbaruch
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/maven/41743
Ссылки в тексте:
[1] В ближайшую субботу мы с Женей Борисовым будем выступать в Питере на JUG.ru: http://jugru.timepad.ru/event/72119/
[2] Apache Maven: http://maven.apache.org/
[3] Apache Ivy: http://ant.apache.org/ivy
[4] Gradle: http://gradle.org/
[5] транзитивные: http://ru.wikipedia.org/wiki/%D0%A2%D1%80%D0%B0%D0%BD%D0%B7%D0%B8%D1%82%D0%B8%D0%B2%D0%BD%D0%BE%D1%81%D1%82%D1%8C
[6] Дер Гроссе Тройки: http://youtu.be/Bxhs8jMnC7w
[7] менеджеров конфликтов: http://ant.apache.org/ivy/history/latest-milestone/settings/conflict-managers.html
[8] fail: http://www.gradle.org/docs/current/dsl/org.gradle.api.artifacts.ResolutionStrategy.html#org.gradle.api.artifacts.ResolutionStrategy:failOnVersionConflict%28%29
[9] custom : http://www.gradle.org/docs/current/userguide/userguide_single.html#N14F71
[10] nearest: http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
[11] Источник: http://habrahabr.ru/post/191246/
Нажмите здесь для печати.