Gradle и Maven долго качают зависимости: что настроить

, 7 мин чтения

Первая сборка JVM-проекта может скачивать сотни артефактов: библиотеки, плагины, сам дистрибутив Gradle или Maven. Медленной она становится по трём причинам: лишние сетевые проверки, короткие таймауты на плохом канале и отсутствие общего кэша. Разберём настройки обоих инструментов.

Где лежат кэши

ЧтоGradleMaven
Зависимости~/.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=true

org.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" -delete

Maven: таймауты и повторы

С версии 3.9 Maven по умолчанию качает через собственный HTTP-транспорт резолвера, а не через Wagon. Поэтому старые советы с -Dmaven.wagon.http.retryHandler.count на новых версиях ничего не меняют, если транспорт не переключён обратно на Wagon. Актуальные ключи зависят от версии Maven:

ПараметрMaven 3.9.xMaven 3.10 и новее
Таймаут соединенияaether.connector.connectTimeoutaether.transport.http.connectTimeout
Таймаут ожидания данныхaether.connector.requestTimeoutaether.transport.http.requestTimeout
Число повторовaether.connector.http.retryHandler.countaether.transport.http.retryHandler.count
Потоки загрузкиmaven.artifact.threadsaether.connector.basic.downstreamThreads

Таймауты указываются в миллисекундах. Число потоков загрузки по умолчанию 5: столько артефактов Maven качает одновременно. Чтобы настройки применялись у всех разработчиков и в CI, положите их в .mvn/maven.config в корне проекта, по одному аргументу на строку. Пример для Maven 3.9:

-Daether.connector.connectTimeout=60000
-Daether.connector.http.retryHandler.count=5

Maven: зеркало в 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 и пропишите его зеркалом.

Стабильный канал для работы

Один ключ, 20 стран, до 5 устройств на Standard. Возврат в течение 7 дней.

Все тарифы