Pull to refresh

Comments 83

У нас, как раз, такой случай: компилируем антом около 500 проектов с транзитивными зависимостями.
На самом деле, особых проблем нет, но хочется компилировать их по-возможности параллельно, а не последовательно, как сейчас.
Думали писать свой таск, но с антом не хочется больше дела иметь.
В Питер, к сожалению, не попасть. Может обрисуете в трех словах что делать? (Кто виноват — знаем сами)
Я бы посоветовал неспешно перееезжать на Грейдл. В случае с Антом это очень легко, и постепенно. Можно сначала обвернуть вообще весь существующий скрипт в Грейдл, и потихоньку выдирать из Анта таргеты и прописывать их Грейдле. Одно удовольствие. Читать тут. (С Мавеном, к сожалению, такая штука не пройдет, в частности из-за того, что при конфликтах транзитивных зависимостей Мавен ведет себя так, как ведет).
Maven позволяет собирать параллельно независимые модули (например, A — общий код(ядро), B зависит от A, С зависит от A, B и C — независимы. В этом примере модули B и C будут собираться параллельно после сборки A). Для этого ничего особенного делать не надо — просто запустить сборку с ключиком -T xx, где xx — параметр ключа.
Я-ж не говорил, что с Maven-ом нельзя :)
У меня спросили, что бы я посоветовал. Я бы посоветовал выбрать лучшую систему сборки, но с очень легким переходом с Ant-а. Лучше-ж быть здоровым, но богатым, чем бедным, но больным, правда?
А как насчёт параллельной сборки с ключиком -T?
Ой. Прошу прощения. Почему-то пропустил слово «антом». Как-никак пост в основном посвещён мавену.
Зачем вы спойлите? :)
Вся эта прелесть, and more, будет по русски в Питере буквально в субботу!
Зато уже не так мучительно больно тем, кто не может выделить 6 часов в субботу.
Да ладно вам, там и печеньки будут! И Женька зажжот!
Хотел бы, но не смогу посетить. Видео будет записываться?
Проблема в том, что выразить это в 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>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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