company_banner

Разрешение конфликтов в транзитивных зависимостях — Хороший, Плохой, Злой

    Вместо предисловия


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


    Скорее, конечно, «Хороший, Удобный и WTF-ный».


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


    Что Такое Менеджер Зависимостей и Зачем Он Нужен?

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

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

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

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

    Зачем нужны транзитивные зависимости?

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

    Как может образоваться конфликт?

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

    Почему это плохо?

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


    Что делать?


    Кто виноват понятно (Java, а не те, о ком вы подумали), а вот что можно сделать?
    Есть несколько стратегий разрешения конфликтов в транзитивных зависимостях (некоторые из них логичные, другие — абсурдные), но, естественно, серебряной пули нет. Давайте посмотрим на некоторые из них:
    • Latest. Стратегия «Новейший» подразумевает обратную совместимость. Если D2 полностью совместим с D1, то оставив в classpath только более новый артефакт (D2) мы получим корректную работу C (ведь он написан под D2), но и корректную работу E (ведь если D2 обратно-совместим, то он работает точно так-же как D1, под который и написан E). Эта стратегия бывает двух подвидов — новейший по версии, и новейший по дате. Чаще всего они сработают одинаково (кроме случаев, в которых нет).
      В случае нашего примера, при использовании 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 нельзя никак. (Опытный, и потому внимательный и вдумчивый пользователь Maven-а воскликнет здесь «А как же enforcer-plugin?!» И будет неправ). Можно сказать конкретно модулю E: «ты думал у тебя есть зависимость на D? Так вот, ее нет». Это хороший выход, конфликта больше нет, D2 в classpath, win. Но это решение совершенно не масштабируемо. Что если от D1 зависят десятки артефактов? На разных уровнях транзитивности?

    Ну, и причем тут nearest?

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

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

    Update: несколько человек в комментах напомнили, что у enforcer-plugin есть функциональность fail Это частично решает проблему. Остается: 1. дикий nearest по умолчанию. 2. варианты решения проблемы — прописывать все конфликтующие зависимости в своем проекте (сносно), либо бесконечные exclude-ы (адово).

    А если одинаковая глубина?

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

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

    Вместо эпилога


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

    Comments 83

      +2
      У нас, как раз, такой случай: компилируем антом около 500 проектов с транзитивными зависимостями.
      На самом деле, особых проблем нет, но хочется компилировать их по-возможности параллельно, а не последовательно, как сейчас.
      Думали писать свой таск, но с антом не хочется больше дела иметь.
      В Питер, к сожалению, не попасть. Может обрисуете в трех словах что делать? (Кто виноват — знаем сами)
        +1
        Я бы посоветовал неспешно перееезжать на Грейдл. В случае с Антом это очень легко, и постепенно. Можно сначала обвернуть вообще весь существующий скрипт в Грейдл, и потихоньку выдирать из Анта таргеты и прописывать их Грейдле. Одно удовольствие. Читать тут. (С Мавеном, к сожалению, такая штука не пройдет, в частности из-за того, что при конфликтах транзитивных зависимостей Мавен ведет себя так, как ведет).
          0
          Maven позволяет собирать параллельно независимые модули (например, A — общий код(ядро), B зависит от A, С зависит от A, B и C — независимы. В этом примере модули B и C будут собираться параллельно после сборки A). Для этого ничего особенного делать не надо — просто запустить сборку с ключиком -T xx, где xx — параметр ключа.
            0
            Я-ж не говорил, что с Maven-ом нельзя :)
            У меня спросили, что бы я посоветовал. Я бы посоветовал выбрать лучшую систему сборки, но с очень легким переходом с Ant-а. Лучше-ж быть здоровым, но богатым, чем бедным, но больным, правда?
          0
          А как насчёт параллельной сборки с ключиком -T?
            0
            В ant-е?
              0
              Ой. Прошу прощения. Почему-то пропустил слово «антом». Как-никак пост в основном посвещён мавену.
          +2
          Кстати, есть видео несколько более развернутой лекции автора по этой теме с JDay Lviv 2013. Вот только язык лекции — английский.
          Кому интересно — ссылки:
          www.youtube.com/watch?v=NbGGiv9elyg
          www.youtube.com/watch?v=W-hiS6vAbvM
            +2
            Зачем вы спойлите? :)
            Вся эта прелесть, and more, будет по русски в Питере буквально в субботу!
              0
              Зато уже не так мучительно больно тем, кто не может выделить 6 часов в субботу.
                +1
                Да ладно вам, там и печеньки будут! И Женька зажжот!
                  0
                  Хотел бы, но не смогу посетить. Видео будет записываться?
                    0
                    да.
            +4
            Проблема в том, что выразить это в Maven нельзя никак. Можно сказать конкретно модулю E: «ты думал у тебя есть зависимость на D? Так вот, ее нет». Это хороший выход, конфликта больше нет, D2 в classpath, win. Но это решение совершенно не масштабируемо. Что если от D1 зависят десятки артефактов? На разных уровнях транзитивности?

            Можно хоть на весь проект. maven-enforcer-plugin

            Пример наведения порядка «сквозняком» по всему проекту:
                        <plugin>
                            <groupId>org.apache.maven.plugins</groupId>
                            <artifactId>maven-enforcer-plugin</artifactId>
                            <version>1.1.1</version>
                            <executions>
                                <execution>
                                    <id>enforce-prerequisites</id>
                                    <goals>
                                        <goal>enforce</goal>
                                    </goals>
                                    <configuration>
                                        <fail>true</fail>
                                        <rules>
                                            <requireJavaVersion>
                                                <version>${java.version}</version>
                                            </requireJavaVersion>
                                            <bannedDependencies>
                                                <searchTransitive>true</searchTransitive>
                                                <excludes>
                                                    <!-- Prohibits Log4J, commons-logging, all SLF4J bindings -->
                                                    <exclude>org.slf4j</exclude>
                                                    <exclude>commons-logging</exclude>
                                                    <exclude>log4j</exclude>
                                                    <!-- Prohibits servlet-api -->
                                                    <exclude>javax.servlet:servlet-api</exclude>
                                                    <!-- Prohibits libthrift by default -->
                                                    <exclude>org.apache.thrift:libthrift</exclude>
                                                    <!-- Prohibits old javassist, should be org.javassist -->
                                                    <exclude>javassist:javassist</exclude>
                                                    <!-- Prohibits old aspectj, should be org.aspectj -->
                                                    <exclude>aspectj:aspectj</exclude>
                                                    <!-- Prohibits Spring Framework by default -->
                                                    <exclude>org.springframework</exclude>
                                                </excludes>
                                                <includes>
                                                    <!-- Allow exact version of SLF4J API and migration bindings -->
                                                    <include>org.slf4j:slf4j-api:${slf4j.version}</include>
                                                    <include>org.slf4j:jcl-over-slf4j:${slf4j.version}</include>
                                                    <include>org.slf4j:log4j-over-slf4j:${slf4j.version}</include>
                                                    <include>org.slf4j:jul-to-slf4j:${slf4j.version}</include>
                                                    <!-- Allow exact versiob of libthrift -->
                                                    <include>org.apache.thrift:libthrift:${libthrift.version}</include>
                                                    <!-- Allow exact version of Spring -->
                                                    <include>org.springframework:*:${spring.version}</include>
                                                </includes>
                                            </bannedDependencies>
                                        </rules>
                                    </configuration>
                                </execution>
                            </executions>
                        </plugin>
            

              +1
              Ваш пример не защищает от появления новых транзитивных конфликтных зависимостей. Я предпочитаю использовать правило dependencyConvergence (http://maven.apache.org/enforcer/enforcer-rules/dependencyConvergence.html) и exclude'ы. Слишком многословно, но надёжно на 100%.
                0
                Добавил update про dependencyConvergence в пост.
                Спасибо.
                +4
                Черт, так и знал, что надо написать про enforcer plugin. Ну ладно, здесь напишу.
                И так, что мы имеем с гуся?
                Мы имеем чудесный rule, под обманчивым названием bannedDependencies, которое можно понять как «эти зависимости мы запретим». А вот и нет. Это означает «это запрещенные зависимости, на них мы упадем». «Так это-же твоя любимая стратегия fail!» воскликнут неумные среди нас, и будут не правы. Стратегия fail роняет сборку при наличии конфликта, а enforcer роняет при наличии конкретно прописанной зависимости.

                Обратите внимание на использованные библиотеки в примере (он с сайта плагина) — commons-logging, log4j! Они запрещены не из-за конфликтов версий, а из-за конфликтов библиотек! Про мотивацию и исполнение запретов такого вида читать тут (инглиш).

                Короче, не то :)
                0
                Не раз сталкивался с такой ситуацией в проекте с сотней модулей. dependency:tree для расследования откуда растут ноги, т.е. откуда пришла транзитивная зависимость и dependency/exclusions после в каждом отдельном проекте.

                А вообще на крупном проекте jar hell грамотно решает только osgi, но вносит в процесс разработки новую активность по миграции legacy кода на osgi.
                Если страшный legacy!? В этом случае выручает Embed-Dependency, Fragment-Host если нет контроля/исходного кода для зависимостей.
                  0
                  Я совершенно не согласен с тем, что решение для jar hell — osgi. Он идеально подходит в случаях, когда несколько версий одного класса необходимы (например, pluggable софт). Но пользоваться им просто потому что ваш инструмент сборки коряв и не может дать вам нормального решения конфликтов? Overkill-чик.
                    0
                    То что вы лечите с помощью выбора в видимости транзитивных зависимостей gradle, maven, ivy и т.п. это по сути зарывание проблемы поглубже. OSGI в этом случае исправляет ущербность стандартной модели classloading до полноценной поддержки в jvm jigsaw. Но опять же есть смысл пременять только в больших проектах, где сложно контроллировать зависимости.

                    >>Overkill-чик
                    только для проектов до пары десятков модулей. Чем крупнее, тем в лучшем порядке надо держать зависимости и появляются несколько разных версий одного артефакта в рантайме с разным интерфейсом/без обратной совместимости!

                    Из приятных дополнений динамизм поведения, реестр сервисов, blueprint — почти тот же спринг, dosgi! Не только pluggable софт, но и любой модульный софт.

                    Provisioning, централизованный logging, мониторинг и т.п. с помощью fuse fabric/karaf. Чего только стоит распределенный деплоймент приложения из maven репозитария и анализ логов приложения.
                      0
                      OSGi является совершеннейшим оффтопиком в данной теме :)
                      Де факто, чаще всего мы сегодня собираем обычные Java приложения, без OSGi и Jigsaw. И чаще всего, достаточно предотвратить конфликты таким образом как описано в посте, чтобы приложение заработало as designed.
                        0
                        Координальное решение «Разрешение конфликтов в транзитивных зависимостях». Насколько это оффтопик решать читателям. Надеюсь что кому-нибудь окажется полезным!
                          0
                          Ну, надо конечно, запилить топик ярости с такой картинкой:
                          image
                          И вот там и пообщаемся.
                          И потом, зачем останавливаться на OSGi? раз уж по вашему принципу мы ищем кардинальное решение «Разрешение конфликтов в транзитивных зависимостях», то можем, например, перестать писать на языках, в которых эта проблема есть. Решать, так решать!

                          Ну а пока что, я бы хотел остаться в контексте этого топика, который, всё таки, по системам сборки.
                          0
                          Желаю вам удачи в проведении курсов по gradle! :)
                            0
                            Спасибо! А о каких курсах речь, если не секрет?
                              0
                              Спасибо!
                              А о каких курсах речь? :)
                      0
                      >> WTF-нутость Maven-а
                      Бросьте, Maven просто инструмент, как и Gradle и Ivy со своими ограничениями и сильными сторонами. Решить вполне можно с помощью dependency/exclusions в случае проблемы!

                      Как правильно указали про проблему модуляризации java платформы
                      jar-а, который будет прописан в сгенерированном classpath-е вторым просто не будет загружен
                      это и является главной проблемой.

                      А исключать зависимости и разрешать такой конфликт за счет какая зависимость будет видна обычно приводит к java.lang.NoSuchMethodError или логической проблеме, т.к. код артефакта был протестирован именно с той версией, от которой он зависит.
                        +2
                        Ваше заявление, что раз Maven просто инструмент, то я должен бросить обращать внимание на его WTF-нуть странно :)
                        Если я вам дам нож с лезвиями в обе стороны и без рукоятки, то вы тоже решите, что раз это просто инструмент, то с ним все ОК? :)

                        Я-ж не говорил, что в Maven-е проблема не решается вообще. Я конкретно упомянул dependency/exclusions и сказал про них, что
                        Это хороший выход, конфликта больше нет, D2 в classpath, win.

                        Проблема этими решением что слегка устанете 1. находить конфликты без fail. 2 exclude-ить их ручками. Причем, чем больше проект, тем сильнее устанете. Не масштабируемо.
                          0
                          Холивар gradle/maven не продуктивен!!! Просто надо координально решать проблему на уровне classloaderов, либо лепить лейкопластыри с тем какую из нескольких, возможно не очень совместимых, версий включить в результирующий classpath. Уверены что у вас 100% покрытие тестами всего-всего и ничего не упадет в продакшн с NoSuchMethodError?
                        0
                        Мне кажется в консерватории есть проблема. Исходный код это очень личная штука, и тащить туда всякую гадость, да еще транзитивно, это рак мозга.

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

                        К сожалению, люди разрабатывающие библиотеки и выкладывающие их в репозитории, сами имеют грешок с каками в зависимостях. Я уж молчу про обычные проекты, которые их используют. Задолбался на каждом новом проекте вычищать бардак.

                        В итоге побойтесь бога, не добавляете бездумно всякую каку и фигачьте побольше эксклудов. Серебряной пули нет.
                          –1
                          Ну и еще мысль, сейчас у нас проект тупо не собирается, если есть две зависимости разный версий. Ибо думать надо!
                            +1
                            Это вам повезло, что он тупо не собирается, вы в курсе, что есть проблема. Хуже, когда случайно «всё работает».
                              0
                              Я потерял очень много анальной крови, и не собирается он не просто так — maven-enforcer-plugin
                                +2
                                Да, enforcer-plugin помогает облегчить попаболь, но, увы, не решение.
                            +1
                            Вот это дельная идея. Только не очень масштабируемая :) Я вам расскажу, что Gradle вплоть до относительно поздних версий (0.5, что-ли) вообще по дефолту шёл с отключенной транзитивностью. А потом его начали использовать в энтерпрайзе :D

                            Безусловно, нужен контроль за новыми зависимостями. Не только из-за того, что «кака», но из-за виральных лицензий, например. Есть инструменты, например Artifactory, с помощью которых можно очень легко отслеживать изменения зависимостей после каждой сборки.

                            Так что я считаю, что транзитивность можно оставить, но стратегия fail + контроль за изменениями — это наше всё.
                              +1
                              Лицензии это само-собой, но тут про них скучно, и не интересно. А энтерпрайз это да, ведь даже школота может его писать!

                              p/s
                              А продаете вы субботу презабавно, вот только у детей и жены отпрошусь :)
                                +1
                                Ну, я имел ввиду, что enterprise, сцука, большой!

                                P.S. Спасибо за комплимент, буду раз вас видеть!
                                +1
                                А, ну и забыл сказать, что и сейчас отключить в Грейдле транзитивность — дело одной строчки:
                                configurations {all*. transitive = false}
                                
                              0
                              а чем вам dependencyManagement в Maven не угодил, что вы про него ни слова не сказали?
                                0
                                Видимо тем, что dependencyManagement работает только внутри текущего проекта и не распространяется на зависимости.
                                  +1
                                  что в одномодульном, что в многомодульном нормально пашет. я dependencyManagement описываю в корневом pom и далее подхватывается «автоматом» по нисходящей.

                                  если же вы хотите протянуть зависимости за пределы основного проекта, то там свои правила и своя «жизнь» — там могут быть и иные версии, а значит снова dependencyManagement

                                  например я хочу свою версию (новее) Log4J подключить для Slf4J: pastebin.com/dybFbdQy
                                    +1
                                    Был не прав — в случае обнаружения конфликта версий Maven возьмёт nearest, т.е. ближайшую к корню проекта. В случае указывания версии библиотеки в dependencyManagement/dependecnies Maven выберет именно её (линк). Тогда, действительно, не понятно в чём проблема автора статьи.
                                +1
                                Как чем не угодил?
                                Тем, что он не менеджер конфликтов и не отключатель конфликтов:) Он просто de-facto отключает транзитивные зависимости, но только для тех артефактов, которые вы там прописали!
                                Т.е. если вы там прописали D — молодец! А если, то держи nearest, и не жалуйся!
                                Это же ни рыба, ни мясо — вы не знаете, что там писать, и уж точно не знаете, когда случается конфликт, пока что-то не перестанет работать (Мерфи подсказывает — в продакшне).
                                  0
                                  Как Gradle спасает в этом случае? Пока вы на грабли не встанете (с тем что, последняя версия библиотеки не совместима с предидущими), так и не будете знать о проблеме.
                                    +1
                                    Fail же! (да, моя любимая)
                                      0
                                      Т.е. вы предлагаете всегда собирать проект Gradle с использованием стратегии fail и руками управлять зависимостями? Или только при добавлении новых зависимостей проверять с fail, а далее переключаться на latest?
                                        +1
                                        Я, похоже, плохо объяснил как работает fail, за что прошу прощения. Вам не нужно ничего переключать. Когда сборка падает по конфликту, вы либо исключаете D1 (в посте есть пример), либо энфорсите D2 добавив его ручками (просто добавь воды зависимость в скрипт).
                                          +3
                                          ОК, понятно теперь.

                                          Но, вроде как, Maven Enforcer Plugin может детектить различные версии транзитивных зависимостей и падать в случае обнаружения последних (так называемый Dependency Convergence). Правильно ли я понимаю, что в этом случае (при использовании dependency convergence rule) поведение будет таким же как и в Gradle со стратегией fail? (Пока не исправим — не будет сбоираться)
                                            +1
                                            Да, это неплохая замена fail.
                                            Мы всё еще остаемся а адом exclusions, но можно знать что прописать в dependency management.
                                            Да, пожалуй пойдет. Сейчас проапдейчу пост.
                                    +1
                                    к хорошему быстро привыкаешь: img825.imageshack.us/img825/5465/fse4.png
                                    здесь в pom явно прописана версия Log4J старее — 1.x.
                                      +1
                                      Да, это удобней, чем mvn dependency:tree, и exclude ручками, но это всё равно ручная работа.
                                  0
                                  Как я понимаю, описанная проблема аналогична проблеме разрешения транзитивных зависимостей в Gentoo Linux, где тоже нельзя одновременно в систему поставить 2 версии пакета, и если есть различные приложения, которым требуются разные версии этих библиотек, то ничего нельзя сделать. Думается, что в таких ситуациях нужно переходить к более «умному» рантайму (OSGI или как в .NET CLR), или эмулировать его (например как это сделано в NixOS с пакетным менеджером Nix). Этот рантайм должен уметь одновременно грузить обе версии так, чтобы они друг другу не мешали. Ведь нет никаких гарантий, что пакет, который хочет зависимость именно D версии 2, заработает успешно на D версии 1, и наоборот.
                                    +1
                                      +1
                                      а про остальные проблемы, в том числе о пакетах, для которых нельзя одновременно в систему поставить 2 версии, приходите слушать в субботу :) Там их есть ;)
                                        +3
                                        К сожалению, я не из Питера. Но с радостью посмотрю видео, если оно будет.
                                      0
                                      По мнению одного моего знакомого у Maven есть огромный плюс есть, как раз в его негибкости. В нем очень сложно изгаляться, и начинать запускать всякую чушь вне стандартного скопа, который почти всегда правильный для большой аппликации. Т.е. в большой фирме, в которой «чтобы срочно починить до релиза» можно радостно взять gradle и наломать дров по самое нехочу. А вот с maven это будет значительно сложнее и есть шанс что все таки решение будет менее грязно. Хотя в этом конечно отчасти проблема квалификации тех, кто поддерживает билд, но тут опять же, вопрос большой фирмы и билда которому немало лет.
                                        +1
                                        Да-да, «отобрать и запретить» это наше всё. Для всех остальных есть Gradle.

                                        Если серьезно, то по хорошему в билде Грейдла не должно быть никаких тасков вообще. Корпоративный init.gralde apply-ит корпоративный плагин, и всё, проблема решена.
                                          0
                                          Ну дык, предпочесть Perforce над Git, чтобы у одного смертного простого разработчика не было опции получить весь код фирмы — вполне «логично». Баааальшой ынтерпрайз такой ынтерпрайз…
                                            +1
                                            Так я-ж не против, «отобрать и запретить» вполне себе имеет право на жизнь. И его вполне можно оформить в Грейдле с помощью корпоративного плагина.
                                        +1
                                        Для мавена есть еще shading — переименование пакета в процессе сборки. Ну последствия такого подхода тоже очевидны, зато вот — гибкость, отличная от nearest.
                                        docs.codehaus.org/display/MAVENUSER/Shade+Plugin
                                          +2
                                          Shading заточен не под обход nearest, а под обход конфликта, когда нужны обе версии класса. Этакий OSGi для бедных. Ужасный, конечно, костыль, но когда не хочется заморачиваться с OSGi — сойдет.

                                          Я про него не писал, потому что слегка out of scope.
                                          0
                                          Кстати, если вы пишете свою библиотеку, то можно в её зависимостях в POM указывать не конкретную версию, а диапазон версий. Например:

                                          <dependency>
                                              <groupId>com.google.guava</groupId>
                                              <artifactId>guava</artifactId>
                                              <version>[10.0.1,</version>
                                          </dependency>
                                          


                                          В таком случае будет резолвиться самая последняя версия из диапазона. Однако эта должна быть такая библиотека, которая даже через 10 версий обеспечивает обратную совместимость.
                                            +1
                                            А к чему это? Как сделать, чтобы всё работало ещё менее надёжно?
                                              0
                                              Ну это я к тому, что если бы в библиотеках E и C версия библиотеки D была бы прописана как диапазон, то проблемы бы и не было — зарезолвилась бы последняя версия библиотеки D.
                                                0
                                                Так шансов что в сторонних библиотеках будет version-range практически нет. И слава богу.
                                                  0
                                                  Ну не знаю, если разработчик протестировал свою библиотеку на всём диапазоне версий (если есть ограничение сверху) и гарантирует, что она работает, то почему бы и не прописать? Это хорошая практика, ИМХО.
                                                    0
                                                    Ну, если есть ограничение сверху, то это ничего не решает, потому что если версия конфликтующей зависимости выпадает из диапазона, то здравствуй, nearset.
                                                      0
                                                      Кстати, а в Gradle же есть диапазоны версий? Что будет, если две транзитивных библиотеки имеют диапазон? Возьмется latest среди их пересечения?
                                                        +1
                                                        Зачем? Возьмется latest среди их объединения.
                                                          +1
                                                          Пересечение более надёжно, чем объединение
                                                            +1
                                                            а если они не пересекаются?
                                                              +1
                                                              Ну если не пересекаются, то выхода нет — придётся идти на компромисс. Если же пересекаются, то логично было бы выбрать более надёжный вариант.
                                              +1
                                              это вообще жесть! потому что чревато тем, что в какой-то момент в какой-нибудь мавеновский репозиторий приедет какая-нибудь новая версия какой-нибудь библиотеки и всё развалится к чОртовой матери.
                                                0
                                                Если есть обратная совместимость, то почему развалится?
                                                  0
                                                  потому что по умолчанию ее нет.
                                                    +1
                                                    в плане совместимости над чётко понимать, что имеется в виду. Совместимость бывает очень разная. Например, в Java есть binary, source и behavioral compatibility. Подробнее тут.
                                                    +2
                                                    Совершенно очевидно, что _любая_ новая версия библиотеки не полностью совместима со старой. Любое изменение поведения, даже пофикшенный баг, — это потенциально бомба. Потому что где-то кто-то вокруг этого бага мог написать какой-то код, который станет разваливаться при апдейте версии.
                                                      +1
                                                      Ну так и старые баги — это потенциально бомба. Вы сидите на какой-нибудь древней библиотеке и сами не знаете об этом, когда в это время выпустили не один десяток фиксов.
                                                0
                                                Но в случае использования технологий изолирования classpath-а (путем загрузки разных модулей разными classloader-ами), вполне может быть не только полезен, но и необходим.

                                                А можете рассказать, как это решается класслоадерами?
                                                Вот как раз на вашем примере: загрузить оба D-1.jar и D-2.jar и в итоге вызвать, например, A.main(«hello, world»).
                                                Так чтобы E.jar использовал D-1.jar а C.jar использовал D-2.jar

                                                Я что-то сходу придумать не могу.
                                                Вернее, оно придумывается сразу вместе со сценариями в которых будет глючить.
                                                  0
                                                  Я имел ввиду всякие pluggable архитектуры, в которых каждый плагин грузится в своем класслоадере со своим набором зависимостей. Обычно для этого используют OSGi, я считаю, что это overkill — сервисы, контейнеры, переупаковкова в бандлы — нахрен это нужно? Там ничего такого ракетностроительного нет, а если лень возиться с класслоадерами есть JBoss Modules (про которого нужно запилить пост).
                                                    0
                                                    Как раз с класслоадерами возиться не лень, а с OSGi и прочими страшными словами — лень :)
                                                      0
                                                      дадада :)

                                                Only users with full accounts can post comments. Log in, please.