Gradle и Maven долго качают зависимости: что настроить
Первая сборка JVM-проекта может скачивать сотни артефактов: библиотеки, плагины, сам дистрибутив Gradle или Maven. Медленной она становится по трём причинам: лишние сетевые проверки, короткие таймауты на плохом канале и отсутствие общего кэша. Разберём настройки обоих инструментов.
Где лежат кэши
| Что | Gradle | Maven |
|---|---|---|
| Зависимости | ~/.gradle/caches/modules-2 | ~/.m2/repository |
| Дистрибутив сборщика | ~/.gradle/wrapper/dists | ~/.m2/wrapper/dists |
| Глобальные настройки | ~/.gradle/gradle.properties | ~/.m2/settings.xml |
| Как сменить каталог | переменная GRADLE_USER_HOME | -Dmaven.repo.local=путь |
Если CI каждый раз стартует с пустого каталога, всё качается заново. Сохранить эти каталоги между запусками важнее любой тонкой настройки, к которой мы перейдём дальше. Готовые средства для этого есть почти везде:
- GitHub Actions: у
actions/setup-javaесть параметрcacheсо значениямиmavenиgradle. - Для Gradle есть отдельное действие
gradle/actions/setup-gradle, которое берёт на себя сохранение и восстановление кэша Gradle. - Для общего кэша зависимостей на нескольких агентах Gradle поддерживает переменную
GRADLE_RO_DEP_CACHE: она указывает на заранее заполненный каталог сmodules-2, который используется только для чтения.
Сам кэш Gradle следит за размером: не чаще раза в сутки он проверяет содержимое и удаляет артефакты, которые не использовались больше 30 дней. Чистить ~/.gradle/caches вручную ради места обычно не нужно.
Gradle: меньше сетевых проверок
Gradle по умолчанию 24 часа помнит результат для динамических версий вроде 1.+ или latest.release и для изменяемых модулей вроде -SNAPSHOT. После этого он снова идёт в репозитории проверить, не появилось ли что-то новее. Чем больше таких зависимостей, тем больше запросов при сборке. Фиксированные версии проверяются один раз.
Полезные флаги:
--offlineсобирает только из кэша и не делает сетевых запросов. Если чего-то в кэше нет, сборка упадёт с понятной ошибкой.--refresh-dependenciesигнорирует сохранённые результаты и заново опрашивает репозитории. Файлы при этом не качаются целиком: Gradle сверяет их и скачивает только изменившиеся. Флаг нужен при подозрении на битый кэш, а не в каждой сборке.--profileпишет HTML-отчёт вbuild/reports/profile, где отдельно видно время на разрешение зависимостей.
Не удаляйте ~/.gradle/caches целиком при каждой странной ошибке: это стоит полной повторной загрузки всех проектов на машине. Сначала попробуйте --refresh-dependencies. И посмотрите, сколько всего тянет проект: ./gradlew dependencies для Gradle или mvn dependency:tree для Maven. Иногда одна лишняя библиотека приносит десятки транзитивных зависимостей.
Gradle: gradle.properties
Общие настройки ускорения кладут в gradle.properties проекта или в ~/.gradle/gradle.properties:
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=trueorg.gradle.parallel выполняет независимые подпроекты параллельно. org.gradle.caching включает кэш результатов задач. org.gradle.configuration-cache переиспользует результат фазы конфигурации. По умолчанию все три выключены. Сам канал они не ускоряют, но убирают лишнюю работу вокруг загрузок. Кэш конфигурации поддерживают не все плагины, проверьте его на своём проекте отдельно.
Gradle: таймауты на медленном канале
Сетевая ошибка Gradle выглядит как лесенка вложенных причин:
Could not resolve all files for configuration ':app:compileClasspath'.
> Could not resolve com.squareup.okhttp3:okhttp:4.12.0.
> Could not get resource 'https://repo.maven.apache.org/maven2/com/squareup/okhttp3/okhttp/4.12.0/okhttp-4.12.0.pom'.
> Could not GET 'https://repo.maven.apache.org/maven2/com/squareup/okhttp3/okhttp/4.12.0/okhttp-4.12.0.pom'.
> Read timed outЧитайте её снизу вверх. Последняя строка называет настоящую причину: Read timed out и Connect timed out говорят о сети, Received status code 401 или 403 о доступе, а Could not find значит, что артефакта просто нет в этом репозитории.
По умолчанию Gradle ждёт установки соединения и данных от репозитория по 30 секунд. Значения меняются системными свойствами в корневом gradle.properties или в ~/.gradle/gradle.properties:
systemProp.org.gradle.internal.http.connectionTimeout=120000
systemProp.org.gradle.internal.http.socketTimeout=120000Значения в миллисекундах. Свойства внутренние: в руководстве пользователя их нет, но их читает HTTP-транспорт Gradle в исходном коде. Учтите, что systemProp из gradle.properties подпроектов игнорируются, работают только корневой файл и файл в домашнем каталоге.
Отдельно настраивается загрузка дистрибутива через wrapper. Если ./gradlew падает ещё до сборки на скачивании zip-архива, увеличьте таймаут wrapper:
./gradlew wrapper --network-timeout 60000Команда запишет networkTimeout в gradle/wrapper/gradle-wrapper.properties. Там же находится distributionUrl: его можно направить на внутренний сервер с копией дистрибутива. Проверку целостности при этом оставьте через distributionSha256Sum.
Если выход в интернет только через корпоративный прокси, Gradle берёт его из стандартных системных свойств JVM. Их тоже можно записать в gradle.properties:
systemProp.https.proxyHost=proxy.example.com
systemProp.https.proxyPort=8080
systemProp.http.nonProxyHosts=*.example.com|localhostВ официальном примере Gradle исключения для HTTPS задаются именно ключом http.nonProxyHosts. Пароль прокси лучше не хранить в файле проекта: положите его в ~/.gradle/gradle.properties или в переменные окружения.
Gradle: один репозиторий для всех модулей
Список репозиториев удобно задать централизованно в settings.gradle.kts, тогда все модули ходят в одно место:
pluginManagement {
repositories {
maven { url = uri("https://nexus.example.com/repository/gradle-plugins/") }
}
}
dependencyResolutionManagement {
repositories {
maven { url = uri("https://nexus.example.com/repository/maven-public/") }
}
}Порядок важен: Gradle опрашивает репозитории по очереди, пока не найдёт модуль. Если первым стоит медленный или недоступный репозиторий, каждая зависимость сначала ждёт его ответа. Ставьте первым тот, где лежит большинство артефактов.
Ещё лучше прямо сказать Gradle, что лежит в каждом репозитории, чтобы он не задавал лишних вопросов:
exclusiveContent {
forRepository {
maven { url = uri("https://repo.example.com/maven2") }
}
filter {
includeGroup("com.example")
}
}С exclusiveContent артефакты группы com.example ищутся только в этом репозитории, и остальные репозитории про них не опрашиваются. Обратная задача решается блоком content { includeGroup(...) } внутри обычного maven { }: такой репозиторий опрашивается только для перечисленных групп. В Android-проектах то же касается пары google() и mavenCentral(): пакеты androidx лежат в репозитории Google.
Если вы меняете источник артефактов, включите проверку зависимостей. Команда ./gradlew --write-verification-metadata sha256 help запишет контрольные суммы в gradle/verification-metadata.xml, и дальше Gradle откажется принимать файл с другим содержимым, откуда бы он ни пришёл.
Maven: офлайн, параллельность и сорванные загрузки
mvn -o(или--offline) работает только с локальным репозиторием~/.m2/repository.mvn dependency:go-offlineзаранее скачивает зависимости и плагины проекта. Плагины, которые подтягивают артефакты во время выполнения, команда может пропустить, поэтому проверьте сборку с-o.mvn -T 1Cсобирает независимые модули параллельно, по одному потоку на ядро процессора.-Uпринудительно перепроверяет snapshot-версии и ранее сорвавшиеся загрузки.-ntp(--no-transfer-progress) убирает из логов CI вывод прогресса загрузок.
Дистрибутив самого Maven при использовании wrapper качается по адресу из distributionUrl в файле .mvn/wrapper/maven-wrapper.properties. Как и в Gradle, его можно направить на внутренний сервер.
Понять, где тормозит Maven, помогает его же лог. Каждая загрузка видна парой строк Downloading from central: и Downloaded from central:, во второй указаны размер и скорость. Имя после from совпадает с id репозитория или зеркала, так что сразу видно, какой источник медленный.
Главная ловушка: если загрузка артефакта однажды сорвалась, Maven запоминает это в файле .lastUpdated и какое-то время не пытается снова. Сообщение выглядит так:
Failure to transfer com.example:lib:jar:1.2.0 from https://repo.maven.apache.org/maven2 was cached in the local repository, resolution will not be reattempted until the update interval of central has elapsed or updates are forcedСеть уже в порядке, а сборка продолжает падать. Запустите сборку с -U или удалите файлы-метки:
find ~/.m2/repository -name "*.lastUpdated" -deleteMaven: таймауты и повторы
С версии 3.9 Maven по умолчанию качает через собственный HTTP-транспорт резолвера, а не через Wagon. Поэтому старые советы с -Dmaven.wagon.http.retryHandler.count на новых версиях ничего не меняют, если транспорт не переключён обратно на Wagon. Актуальные ключи зависят от версии Maven:
| Параметр | Maven 3.9.x | Maven 3.10 и новее |
|---|---|---|
| Таймаут соединения | aether.connector.connectTimeout | aether.transport.http.connectTimeout |
| Таймаут ожидания данных | aether.connector.requestTimeout | aether.transport.http.requestTimeout |
| Число повторов | aether.connector.http.retryHandler.count | aether.transport.http.retryHandler.count |
| Потоки загрузки | maven.artifact.threads | aether.connector.basic.downstreamThreads |
Таймауты указываются в миллисекундах. Число потоков загрузки по умолчанию 5: столько артефактов Maven качает одновременно. Чтобы настройки применялись у всех разработчиков и в CI, положите их в .mvn/maven.config в корне проекта, по одному аргументу на строку. Пример для Maven 3.9:
-Daether.connector.connectTimeout=60000
-Daether.connector.http.retryHandler.count=5Maven: зеркало в settings.xml
Чтобы все запросы к Maven Central и другим публичным репозиториям шли через внутренний прокси, пропишите зеркало в ~/.m2/settings.xml:
<settings>
<mirrors>
<mirror>
<id>company</id>
<name>Company proxy</name>
<url>https://nexus.example.com/repository/maven-public/</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
</settings>Значение * в mirrorOf перенаправляет все репозитории, central только Maven Central, external:* всё, кроме локальных и файловых. Nexus или Artifactory в роли прокси скачивают артефакт из интернета один раз и дальше раздают его из локальной сети. Особенно это заметно, когда десятки сборок тянут одни и те же библиотеки.
С версии 3.8.1 Maven по умолчанию отклоняет репозитории по незащищённому http://. Если зеркало или старый репозиторий работают без HTTPS, сборка падает с сообщением Blocked mirror for repositories. Правильное решение: перевести репозиторий на HTTPS, а не отключать эту защиту.
Корпоративный HTTP-прокси для Maven задаётся там же, в блоке <proxies> файла settings.xml: элемент <proxy> с полями active, protocol, host, port, а если нужно, ещё и nonProxyHosts. Переменные окружения вроде HTTPS_PROXY Maven сам не читает.
Если не помогает
Если сборка падает с Connect timed out или Read timed out на одном и том же хосте, а внутреннего прокси нет, посмотрите маршрут до этого хоста. Если проблема в маршруте до репозитория, помогает защищённое подключение через ближайшую к нему локацию.
Коротко
- Сохраняйте
~/.gradle/cachesи~/.m2/repositoryмежду сборками. - Фиксируйте версии, избегайте
+и лишних snapshot-зависимостей. - На плохом канале поднимите HTTP-таймауты Gradle и ключи резолвера Maven для вашей версии.
- Ставьте первым репозиторий, где лежит большинство артефактов.
- После сетевого сбоя в Maven запускайте сборку с
-U. - Для команды поднимите Nexus или Artifactory и пропишите его зеркалом.