- Downgrading versions and excluding dependencies
- Overriding transitive dependency versions
- Consequences of using strict versions
- Forced dependencies vs strict dependencies
- Excluding transitive dependencies
- Gradle: управляя зависимостями
- Репозиторий
- Объявление зависимостей
- Виды зависимостей
- Дерево зависимостей
- Разрешение конфликтов
- Заключение
Downgrading versions and excluding dependencies
Overriding transitive dependency versions
Gradle resolves any dependency version conflicts by selecting the latest version found in the dependency graph. Some projects might need to divert from the default behavior and enforce an earlier version of a dependency e.g. if the source code of the project depends on an older API of a dependency than some of the external libraries.
Forcing a version of a dependency requires a conscious decision. Changing the version of a transitive dependency might lead to runtime errors if external libraries do not properly function without them. Consider upgrading your source code to use a newer version of the library as an alternative approach.
In general, forcing dependencies is done to downgrade a dependency. There might be different use cases for downgrading:
a bug was discovered in the latest release
your code depends on a lower version which is not binary compatible
your code doesn’t depend on the code paths which need a higher version of a dependency
In all situations, this is best expressed saying that your code strictly depends on a version of a transitive. Using strict versions, you will effectively depend on the version you declare, even if a transitive dependency says otherwise.
Strict dependencies are to some extent similar to Maven’s nearest first strategy, but there are subtle differences:
strict dependencies don’t suffer an ordering problem: they are applied transitively to the subgraph, and it doesn’t matter in which order dependencies are declared.
conflicting strict dependencies will trigger a build failure that you have to resolve
Let’s say a project uses the HttpClient library for performing HTTP calls. HttpClient pulls in Commons Codec as transitive dependency with version 1.10. However, the production source code of the project requires an API from Commons Codec 1.9 which is not available in 1.10 anymore. A dependency version can be enforced by declaring it as strict it in the build script:
Consequences of using strict versions
Using a strict version must be carefully considered, in particular by library authors. As the producer, a strict version will effectively behave like a force: the version declaration takes precedence over whatever is found in the transitive dependency graph. In particular, a strict version will override any other strict version on the same module found transitively.
However, for consumers, strict versions are still considered globally during graph resolution and may trigger an error if the consumer disagrees.
For example, imagine that your project B strictly depends on C:1.0 . Now, a consumer, A , depends on both B and C:1.1 .
Then this would trigger a resolution error because A says it needs C:1.1 but B , within its subgraph, strictly needs 1.0 . This means that if you choose a single version in a strict constraint, then the version can no longer be upgraded, unless the consumer also sets a strict version constraint on the same module.
In the example above, A would have to say it strictly depends on 1.1.
For this reason, a good practice is that if you use strict versions, you should express them in terms of ranges and a preferred version within this range. For example, B might say, instead of strictly 1.0 , that it strictly depends on the [1.0, 2.0[ range, but prefers 1.0 . Then if a consumer chooses 1.1 (or any other version in the range), the build will no longer fail (constraints are resolved).
Forced dependencies vs strict dependencies
Forcing dependencies via ExternalDependency.setForce(boolean) is deprecated and no longer recommended: forced dependencies suffer an ordering issue which can be hard to diagnose and will not work well together with other rich version constraints. You should prefer strict versions instead. If you are authoring and publishing a library, you also need to be aware that force is not published.
If, for some reason, you can’t use strict versions, you can force a dependency doing this:
If the project requires a specific version of a dependency on a configuration-level then it can be achieved by calling the method ResolutionStrategy.force(java.lang.Object[]).
Excluding transitive dependencies
While the previous section showed how you can enforce a certain version of a transitive dependency, this section covers excludes as a way to remove a transitive dependency completely.
Similar as forcing a version of a dependency, excluding a dependency completely requires a conscious decision. Excluding a transitive dependency might lead to runtime errors if external libraries do not properly function without them. If you use excludes, make sure that you do not utilise any code path requiring the excluded dependency by sufficient test coverage.
Transitive dependencies can be excluded on the level of a declared dependency. Exclusions are spelled out as a key/value pair via the attributes group and/or module as shown in the example below. For more information, refer to ModuleDependency.exclude(java.util.Map).
In this example, we add a dependency to commons-beanutils but exclude the transitive dependency commons-collections . In our code, shown below, we only use one method from the beanutils library, PropertyUtils.setSimpleProperty() . Using this method for existing setters does not require any functionality from commons-collections as we verified through test coverage.
Effectively, we are expressing that we only use a subset of the library, which does not require the commons-collection library. This can be seen as implicitly defining a feature variant that has not been explicitly declared by commons-beanutils itself. However, the risk of breaking an untested code path increased by doing this.
For example, here we use the setSimpleProperty() method to modify properties defined by setters in the Person class, which works fine. If we would attempt to set a property not existing on the class, we should get an error like Unknown property on class Person . However, because the error handling path uses a class from commons-collections , the error we now get is NoClassDefFoundError: org/apache/commons/collections/FastHashMap . So if our code would be more dynamic, and we would forget to cover the error case sufficiently, consumers of our library might be confronted with unexpected errors.
This is only an example to illustrate potential pitfalls. In practice, larger libraries or frameworks can bring in a huge set of dependencies. If those libraries fail to declare features separately and can only be consumed in a «all or nothing» fashion, excludes can be a valid method to reduce the library to the feature set actually required.
On the upside, Gradle’s exclude handling is, in contrast to Maven, taking the whole dependency graph into account. So if there are multiple dependencies on a library, excludes are only exercised if all dependencies agree on them. For example, if we add opencsv as another dependency to our project above, which also depends on commons-beanutils , commons-collection is no longer excluded as opencsv itself does not exclude it.
If we still want to have commons-collections excluded, because our combined usage of commons-beanutils and opencsv does not need it, we need to exclude it from the transitive dependencies of opencsv as well.
Historically, excludes were also used as a band aid to fix other issues not supported by some dependency management systems. Gradle however, offers a variety of features that might be better suited to solve a certain use case. You may consider to look into the following features:
Update or downgrade dependency versions: If versions of dependencies clash, it is usually better to adjust the version through a dependency constraint, instead of attempting to exclude the dependency with the undesired version.
Источник
Gradle: управляя зависимостями
Управление зависимостями – одна из наиболее важных функций в арсенале систем сборки. С приходом Gradle в качестве основной системы сборки Android-проектов в части управления зависимостями произошёл существенный сдвиг, закончилась эпоха ручного копирования JAR-файлов и долгих танцев с бубном вокруг сбоящих конфигураций проекта.
В статье рассматриваются основы управления зависимостями в Gradle, приводятся углублённые практические примеры, небольшие лайфхаки и ссылки на нужные места в документации.
Репозиторий
Как известно, Gradle не имеет собственных репозиториев и в качестве источника зависимостей использует Maven- и Ivy-репозитории. При этом интерфейс для работы с репозиториями не отличается на базовом уровне, более развёрнуто об отличиях параметров вы можете узнать по ссылкам IvyArtifactRepository и MavenArtifactRepository. Стоит отметить, что в качестве url могут использоваться ‘http’, ‘https’ или ‘file’ протоколы. Порядок, в котором записаны репозитории, влияет на порядок поиска зависимости в репозиториях.
Объявление зависимостей
В приведённом выше примере вы видите сценарий сборки, в котором подключены две зависимости для различных конфигураций (compile и testCompile) компиляции проекта. JsonToken будет подключаться во время компиляции проекта и компиляции тестов проекта, jUnit только во время компиляции тестов проекта. Детальнее о конфигурациях компиляции — по ссылке.
Также можно увидеть, что jUnit-зависимость мы подключаем как динамическую(+), т.е. будет использоваться самая последняя из доступных версия 4.+, и нам не нужно будет следить за минорными обновлениями (рекомендую не использовать эту возможность в compile-типе компиляции приложения, т.к. могут появиться неожиданные, возможно, сложно локализуемые проблемы).
На примере с jUnit-зависимостью рассмотрим стандартный механизм Gradle по поиску необходимой зависимости:
В Gradle реализована система кэширования, которая по умолчанию хранит зависимости в течение 24 часов, но это поведение можно переопределить.
После того, как время, установленное для хранения данных в кэше, вышло, система при запуске задач сначала проверит возможность обновления динамических (dynamic) и изменяемых (changing) зависимостей и при необходимости их обновит.
Gradle старается не загружать те файлы, которые были загруженны ранее, и использует для этого систему проверок, даже если URL/источники файлов будут отличаться. Gradle всегда проверяет кэш (URL, версия и имя модуля, кэш других версий Gradle, Maven-кэш), заголовки HTTP-запроса (Date, Content-Length, ETag) и SHA1-хэш, если он доступен. Если совпадений не найдено, то система загрузит файл.
Также в системе сборки присутствуют два параметра, используя которые при запуске вы можете изменить политику кэширования для конкретного выполнения задачи.
– –offline – Gradle никогда не будет пытаться обратиться в сеть для проверки обновлений зависимостей.
– –refresh-dependencies – Gradle попытается обновить все зависимости. Удобно использовать при повреждении данных, находящихся в кэше. Верифицирует кэшированные данные и при отличии обновляет их.
Более детально про кэширование зависимостей можно прочитать в Gradle User Guide.
Виды зависимостей
Существует несколько видов зависимостей в Gradle. Наиболее часто используемыми являются:
– Внешние зависимости проекта — зависимости, загружаемые из внешних репозиториев;
– Проектные зависимости — зависимость от модуля (подпроекта) в рамках одного проекта;
– Файловые зависимости — зависимости, подключаемые как файлы (jar/aar архивы).
Также существуют зависимости клиентских модулей, зависимости Gradle API и локальные Groovy-зависимости. Они используются редко, поэтому в рамках данной статьи не будем их разбирать, но почитать документацию о них можно здесь.
Дерево зависимостей
Каждая внешняя или проектная зависимость может содержать собственные зависимости, которые необходимо учесть и загрузить. Таким образом, при выполнении компиляции происходит загрузка зависимостей для выбранной конфигурации и строится дерево зависимостей, человеческое представление которого можно увидеть, выполнив Gradle task ‘dependencies’ в Android Studio или команду gradle %module_name%:dependencies в консоли, находясь в корневой папке проекта. В ответ вы получите список деревьев зависимостей для каждой из доступных конфигураций.
Используя параметр configuration, указываем имя конфигурации, чтобы видеть дерево зависимостей только указанной конфигурации.
Возьмем специально подготовленные исходники репозитория, расположенного на github и попробуем получить дерево зависимостей для конкретной конфигурации (в данный момент проект находится в состоянии 0, т.е. в качестве build.gradle используется build.gradle.0):
Проанализировав дерево зависимостей, можно увидеть, что модуль app использует в качестве зависимостей две внешних зависимости (appcompat и guava), а также две проектных зависимости (first и second), которые в свою очередь используют библиотеку jsontoken версий 1.0 и 1.1 как внешнюю зависимость. Совершенно очевидно, что проект не может содержать две версии одной библиотеки в Classpath, да и нет в этом необходимости. На этом этапе Gradle включает модуль разрешения конфликтов.
Разрешение конфликтов
Gradle DSL содержит компонент, используемый для разрешения конфликтов зависимостей. Если посмотреть на зависимости библиотеки jsontoken на приведённом выше дереве зависимостей, то мы увидим их только раз. Для модуля second зависимости библиотеки jsontoken не указаны, а вывод самой зависимости содержит дополнительно ‘–> 1.1’, что говорит о том, что версия библиотеки 1.0 не используется, а автоматически была заменена на версию 1.1 с помощью Gradle-модуля разрешения конфликтов.
Для объяснения каким образом была разрешена конфликтная ситуация, также можно воспользоваться Gradle-таском dependencyInsight, например:
Стоит обратить внимание, что версия 1.1 выбирается в результате conflict resolution, также возможен выбор в результате других правил (например: selected by force или selected by rule). В статье будут приведены примеры использования правил, влияющих на стратегию разрешения зависимостей, и выполнив таск dependencyInsight вы сможете увидеть причину выбора конкретной версии библиотеки на каждом из приведённых ниже этапов. Для этого при переходе на каждый этап вы можете самостоятельно выполнить таск dependencyInsight.
При необходимости есть возможность переопределить логику работы Gradle-модуля разрешения конфликтов, например, указав Gradle падать при выявлении конфликтов во время конфигурирования проекта. (состояние 1)
После чего даже при попытке построить дерево зависимостей Gradle таски будут прерываться по причине наличия конфликта в зависимостях приложения.
У задачи есть четыре варианта решения:
Первый вариант – удалить строки, переопределяющие стратегию разрешения конфликтов.
Второй вариант – добавить в стратегию разрешения конфликтов правило обязательного использования библиотеки jsonToken, с указанием конкретной версии (состояние 2):
При применении этого варианта решения дерево зависимостей будет выглядеть следующим образом:
Третий вариант — добавить библиотеку jsonToken явно в качестве зависимости для проекта app и присвоить зависимости параметр force, который явно укажет, какую из версий библиотеки стоит использовать. (состояние 3)
А дерево зависимостей станет выглядеть следующим образом:
Четвёртый вариант – исключить у одной из проектных зависимостей jsontoken из собственных зависимостей с помощью параметра exclude. (состояние 4)
И дерево зависимостей станет выглядеть следующим образом:
Стоит отметить, что exclude не обязательно передавать оба параметра одновременно, можно использовать только один.
Но несмотря на правильный вывод дерева зависимостей, при попытке собрать приложение Gradle вернёт ошибку:
Причину ошибки можно понять из вывода сообщений выполнения задачи сборки — класс GwtCompatible с идентичным именем пакета содержится в нескольких зависимостях. И это действительно так, дело в том, что проект app в качестве зависимости использует библиотеку guava, а библиотека jsontoken использует в зависимостях устаревшую Google Collections. Google Collections входит в Guava, и их совместное использование в одном проекте невозможно.
Добиться успешной сборки проекта можно тремя вариантами:
Первый — удалить guava из зависимостей модуля app. Если используется только та часть Guava, которая содержится в Google Collections, то предложенное решение будет неплохим.
Второй — исключить Google Collections из модуля first. Добиться этого мы можем используя описанное ранее исключение или правила конфигураций. Рассмотрим оба варианта, сначала используя исключения (состояние 5)
Пример использования правил конфигураций (состояние 6):
Дерево зависимостей для обеих реализаций исключения Google Collections будет идентично.
Третий вариант — использовать функционал подмены модулей (состояние 7):
Дерево зависимостей будет выглядеть следующим образом:
Нужно учесть, что если оставить предопределенную логику разрешения конфликтов, которая указывает прерывать сборку при наличии любого конфликта, то выполнение любого таска будет прерываться на этапе конфигурации. Другими словами, использование правил замены модулей является одним из правил стратегии разрешения конфликтов между зависимостями.
Также важно заметить, что последний из озвученных вариантов является самым гибким, ведь при удалении guava из списка зависимостей Gradle, Google Collections сохранится в проекте, и функционал, от него зависящий, сможет продолжить выполнение. А дерево зависимостей будет выглядеть следующим образом:
После каждого из вариантов мы достигнем успеха в виде собранного и запущенного приложения.
Но давайте рассмотрим другую ситуацию (состояние 8), у нас одна единственная сильно урезанная (для уменьшения размеров скриншотов) динамическая зависимость wiremock. Мы её используем сугубо в целях обучения, представьте вместо неё библиотеку, которую поставляет ваш коллега, он может выпустить новую версию в любой момент, и вам непременно необходимо использовать самую последнюю версию:
Дерево зависимостей выглядит следующим образом:
Как вы можете увидеть, Gradle загружает последнюю доступную версию wiremock, которая является beta. Ситуация нормальная для debug сборок, но если мы собираемся предоставить сборку пользователям, то нам определённо необходимо использовать release-версию, чтобы быть уверенными в качестве приложения. Но при этом в связи с постоянной необходимостью использовать последнюю версию и частыми релизами нет возможности отказаться от динамического указания версии wiremock. Решением этой задачи будет написание собственных правил стратегии выбора версий зависимости:
Стоит отменить, что данное правило применится ко всем зависимостям, а не только к wiremock.
После чего, запустив задачу отображения дерева зависимостей в информационном режиме, мы увидим, как отбрасываются beta-версии библиотеки, и причину, по которой они были отброшены. В конечном итоге будет выбрана стабильная версия 1.58:
Но при тестировании было обнаружено, что в версии 1.58 присутствует критичный баг, и сборка не может быть выпущена в таком состоянии. Решить эту задачу можно, написав ещё одно правило выбора версии зависимости:
После чего версия wiremock 1.58 будет также отброшена, и начнёт использоваться версия 1.57, а дерево зависимостей будет выглядеть следующим образом:
Заключение
Несмотря на то, что статья получилась достаточно объемной, тема Dependency Management в Gradle содержит много не озвученной в рамках этой статьи информации. Глубже погрузиться в этот мир лучше всего получится с помощью официального User Guide в паре с документацией по Gradle DSL, в изучение которых придется инвестировать немало времени.
Зато в результате вы получите возможность сэкономить десятки часов, как благодаря автоматизации, так и благодаря пониманию того, что необходимо делать при проявлении различных багов. Например, в последнее время достаточно активно проявляются баги с 65К-методов и Multidex, но благодаря грамотному просмотру зависимостей и использованию exclude проблемы решаются очень быстро.
Источник