Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Разрешение раздоров в транзитивных зависимостях — Отличный, Дрянной, Гневный

Anna | 4.06.2014 | нет комментариев

Взамен вступления

В ближайшую субботу мы с Женей Борисовым будем выступать в Питере на JUG.ru. Будет много веселого трэша и увлекательной инфы (изредка не разберешь, где проходит граница), и одно из моих выступлений будет посвящено WTF-нутости модульной разработке программ. Я расскажу несколько ужастиков, один из которых будет о том, как все пытаются стремительно, эластично и правильно описать зависимости в плане, и что из этого обыкновенно получается. Увлекательно? Тогда добродушно пожаловать в чистилище!


Скорее, безусловно, «Отличный, Комфортный и WTF-ный».

Чуть-чуть теории…

 

Что Такое Администратор Зависимостей и Для чего Он Необходим?

Всякий теперешний инструмент сборки (исключительно в мире JVM) включает в себя (либо имеет легко подключающийся) администратор зависимостей (a.k.a. dependency manager). Самые знаменитые, это, безусловно, Apache MavenApache Ivy (администратор зависимостей для Apache Ant), и Gradle.
Одна из основных функций инструментов сборки в мире JVM это создавать classpath-ы. Применяются они во время процесса сборки много где — для компиляции кода, для запуска тестов, для сборки архивов (war-ов, ear-ов, либо дистирбутивов и установщиков) и даже для настройки планов в IDE.
Для упрощения процесса нахождения в сети, скачивания, хранения и конфигурации зависимостей и существуют администраторы зависимостей. Вы декларируете, что вам необходим, скажем, commons-lang, и вуаля, он у вас есть.

Что такое транзитивные зависимости?

Транзитивная связанность — это тот артефакт, от которого зависит прямая связанность плана. Представьте себе следующую обстановку:

Наш план A зависит от 2-х артефактов — E и B. На этом прямые зависимости заканчиваются и начинаютсятранзитивные (C, D). В результате мы получаем цепочки зависимостей, артефакты в которых могут повторяться (D, в нашем примере)

Для чего необходимы транзитивные зависимости?

Видимо, не для компиляции. Но, для каждого остального, вероятно, необходимы — эти артефакты обязаны находиться в сборках архивов, они обязаны находиться в classpath для запуска тестов, и пути к ним обязаны быть отданы через API для интеграции с IDE.

Как может образоваться раздор?

Если мы посмотрим на диаграмму выше, то увидим тот самый раздор. В classpath плана A обязаны находиться и артефакт D версии 1 (от него зависит Е), и артефакт D версии 2 (от него зависит C)!

Отчего это нехорошо?

JVM (и javac) определяет уникальность класса по его имени (и classloader-у, но в нашем простом примере все классы загружаются одним classloader-ом). В случае, когда в classpath встречаются два класса с идентичным именем, загружен будет только 1-й. Если предположить, что в D1 и в D2 находятся классы с идентичным именем (согласитесь, скорее каждого, так и есть), то класс из jar-а, тот, что будет прописан в сгенерированном classpath-е вторым легко не будет загружен. Какой из них будет 1-й? Это зависит от логики администратора зависимостей и, в всеобщем случае, неведомо.
Как вы понимаете, это и есть раздор:

Что делать?

Кто повинен ясно (Java, а не те, о ком вы подумали), а вот что дозволено сделать?
Есть несколько стратегий разрешения раздоров в транзитивных зависимостях (некоторые из них логичные, другие — нелепые), но, безусловно, серебряной пули нет. Давайте посмотрим на некоторые из них:

  • Latest. Тактика «Новый» подразумевает обратную совместимость. Если D2 всецело совместим с D1, то оставив в classpath только больше новейший артефакт (D2) мы получим правильную работу C (чай он написан под D2), но и правильную работу E (чай если D2 обратно-совместим, то он работает верно так-же как D1, под тот, что и написан E). Эта тактика бывает 2-х подвидов — новый по версии, и новый по дате. Почаще каждого они сработают идентично (помимо случаев, в которых нет).
    В случае нашего примера, при применении latest в classpath окажется D2.
  • Fail (a.k.a. Strict). При этой стратегии сборка упадет в тот момент, когда администратор зависимостей найдет раздор. Безусловно, самая безвредная, но и самая трудоемкая тактика.
    В случае нашего примера, при применении fail сборка упадет.
  • All (a.k.a. No-conflict). «И то, и другое, и дозволено без хлеба» значит, что и D1 и D2 из нашего примера окажутся в classpath-е (в произвольном порядке). чистилище? чистилище! Но в случае применения спецтехнологий изолирования classpath-а (путем загрузки различных модулей различными classloader-ами), абсолютно может быть не только пригоден, но и нужен.
    В случае нашего примера, при применении all в classpath окажутся и D1, и D2.
  • Nearest. Тактика «Ближайший» это целиком и всецело восхитительный WTF, про тот, что я с удовольствием расскажу ниже. Stay tuned.
  • Custom. В этом случае администратор зависимостей спросит у вас, что изволит помещик. Это, безусловно, «ручное управление», но изредка может быть крайне благотворно. Вот пример псевдокода на псевдогруви:
    coflictManager = {artifact, versionA, versionB ->
        //допустим, я полагаюсь на обратную совместимость только библиотек Apache, но не остальных
        if(artifact.org.startsWith ('org.apache')){
           [versionA, versionB].max()
        } else {
            fail()
        }
    }
    

    В случае нашего примера, при применении этой имплементации custom, если предположить что org у D1 и D2 начинается с ‘org.apache’, то в classpath окажется D2, в отвратном случае, сборка упадет.

 

Kто во что способен

Сейчас давайте посмотрим, кто из Дер Гроссе Тройки упомянутой выше, что может.

Apache Ivy

В плане администраторов раздоров Ivy красив. Они подключаемы, оба варианта latest, а так же fail и all идут в коробке. С custom-ом тоже всё прекрасно. Дозволено всецело имплементировать свою логику, а дозволено воспользоваться полуфабрикатом и лишь придумать подходящий regex. По умолчанию работает latest (по версии).

Gradle

В первых версиях Gradle (до 0.6, если мне не изменяет память) применялся Ivy как администратор зависимостей. Соответственно, всё сказанное выше было правильно для Gradle тоже, но ребята из Gradleware написали свой администратор зависимостей (в основном из за задач с локальным хранилищем Ivy при параллельной сборкe, одного из основных превосходств Gradle). В процессе выпуска своего администратора такие «второстепенные» фичи как замена администратора раздоров были задвинуты вдалеке в roadmap, и достаточно длинное время Gradle существовал только с latest. Не нравится latest — отключай транзитивные зависимости, и вперед, перечислять всё в ручную. Но, сегодня всё в порядке. Начиная с 1.0 есть fail, а с 1.4 и custom тоже.

Apache Maven

Ну, ради дальнейшей картинки и был замыслен каждый пост.
Как вы считаете, какая из версий D попадет в classpath? D1? D2? обе? ни одной? сборка упадет?

Как вы теснее, наверно, додумались, в classpath попадет D1 (что с большой вероятностью приведет к задачам, потому что каждый код в C, написанный под новую функциональность, которой не существует в D1, легко упадет). Это тот самый Удивительный WTF, тот, что я вам обещал. Maven работает с уникальной стратегией nearest, которая выбирает в classpath тот артефакт, тот, что находится ближе к корню плана (А) в дереве планов.

Как же так? Что за галиматья?

Корень задачи лежит в сложности исключения зависимости в Maven. Если, скажем, вы хотите применять D2, а не D1, то, по отличному, вы обязаны сказать Maven-у: Драгоценный Maven, никогда не используй D1. Легко для примера, в Gradle мы бы написали вот так:

configurations {all*.exclude group: 'mygroup', module: 'D', version: '1'}

Задача в том, что выразить это в Maven невозможно никак. Дозволено сказать определенно модулю E: «ты думал у тебя есть связанность на D? Так вот, ее нет». Это отличный выход, раздора огромнее нет, D2 в classpath, win. Но это решение абсолютно не масштабируемо. Что если от D1 зависят десятки артефактов? На различных ярусах транзитивности?

Ну, и причем здесь nearest?

Задача отсутствия глобального exclude была решена в Maven-е дюже «увлекательным» методом. Было решено, что если вы объявили в вашем плане А связанность с определенной версией, то только эта версия и попадет в classpath. То есть фактически, это ультимативный nearest — ближе чем в A быть не может (это root), следственно раздор решён, не необходимо искать все места откуда необходимо исключать D. По дороге, правда, мы получили дюже необычное и сложно-предсказуемое поведение в тех случаях, когда A не объявляет D напрямую (см. наш пример), но что есть, то есть.

Довольно увлекательно, что идея «то, что пользователь объявил сам — закон» применяется в Gradle тоже, и это не мешает им применять приписываемые стратегии типа latest и fail для каждого остального.

А если идентичная глубина?

Данный очаровательный вопрос (что делать, если бы в нашем примере B зависел от D2) не приходил ребятам из Maven-а в голову на протяжении 2-х с половиной лет (от релиза 2.0 в октябре 2005 и до версии 2.0.9 в апреле 2008) и какой артефакт будет в classpath было легко неопределенно. В Maven 2.0.9 было принято решение — будет 1-й!

Как это нам помогает? Верно, никак. Потому что мы в всеобщем случае не знаем, какой из них будет 1-й, чай транзитивные зависимости не проявляют себя пока не случается раздор (либо пока мы не начинаем расследовать эту загадку). Спасибо, пацаны!

Взамен эпилога

WTF-нутость Maven-а, безусловно, не ограничивается Удивительным порождением альтернативного интеллекта — стратегией nearest. Но на сегодня, я думаю, хватит. Холивары в комментах различно приветствуются (если что, я притоплю за Gradle), а все питерцы приходят на JUG в субботу 31 числа в ПетроКонгресс на продолжение банкета.

Источник: programmingmaster.ru

 

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB