Заметки о Gradle: Project, Task и Task Graph
При первом знакомстве с Gradle файл build.gradle часто выглядит непривычно. Он содержит Groovy- или Kotlin-инструкции вида plugins {}, dependencies {}, repositories {}, tasks.named(...), которые не похожи ни на XML-конфигурацию Maven, ни на обычный Java-код.
Из-за этого не всегда очевидно, что именно происходит при выполнении сборки.
Модель работы Gradle
Прежде чем переходить к объектам Project, Task и Action, полезно понять общую картину: какие элементы участвуют в сборке и как они взаимодействуют между собой.
Gradle использует те же стандартные соглашения о структуре проекта, что и Maven. По умолчанию исходный код располагается в каталогах src/main/java, тесты — в src/test/java, а результаты сборки помещаются в каталог build/ (аналог Maven-каталога target/).
На этой схеме показаны основные компоненты процесса сборки:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
src/main/java
src/test/java
│
▼
build.gradle
│
▼
Gradle
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
Dependencies Tasks build/
│ │ (результаты)
│ │
▼ ▼
Maven Central compileJava
Nexus processResources
Artifactory test
jar
build
▼
~/.gradle/caches
(локальный кэш)
Исходный код
В Java-проекте исходный код и тесты обычно располагаются в стандартных каталогах:
1
2
src/main/java — основной код приложения
src/test/java — тесты
Gradle, как и Maven, использует эту структуру по умолчанию и рассматривает эти директории как входные данные для сборки.
Файл build.gradle
Файл build.gradle содержит описание сборки: подключаемые плагины, используемые репозитории, зависимости и настройки задач. При запуске Gradle этот файл выполняется как обычный Groovy- или Kotlin-скрипт.
Зависимости и репозитории
Если в build.gradle указана зависимость, например:
1
implementation 'org.springframework.boot:spring-boot-starter-web'
Gradle обращается к указанным репозиториям (Maven Central, Nexus, Artifactory), скачивает необходимые артефакты и сохраняет их в локальный кэш.
Локальный кэш
Все скачанные зависимости сохраняются в каталоге:
1
~/.gradle/caches
При последующих сборках Gradle использует уже загруженные файлы.
Задачи (Task)
После выполнения build.gradle Gradle формирует набор задач. Для Java-проекта это, например: compileJava, processResources, test, jar, build.
Каждая задача отвечает за отдельный шаг сборки.
Результаты сборки
Результаты выполнения задач помещаются в каталог build/.
Например:
build/classes— скомпилированные классы;build/libs— готовые JAR-файлы;build/reports— отчеты;build/test-results— результаты тестов.
Общая последовательность
При запуске команды:
1
./gradlew build
Gradle выполняет следующие действия:
- Выполняет
build.gradle; - Загружает зависимости из репозиториев или локального кэша;
- Создает и настраивает задачи;
- Определяет порядок их выполнения;
- Запускает необходимые задачи;
- Сохраняет результаты в каталог
build/.
Итоговая идея
На концептуальном уровне работа Gradle выглядит следующим образом:
1
2
3
4
5
6
7
8
9
Исходный код
↓
build.gradle
↓
Gradle
↓
Dependencies + Tasks
↓
build/
Переход от общей схемы к build.gradle
В отличие от Maven, где сборка описывается декларативно в файле pom.xml, Gradle использует исполняемый build script. По умолчанию это файл build.gradle или его Kotlin-вариант build.gradle.kts.
Этот файл представляет собой Groovy- или Kotlin-скрипт, который Gradle выполняет при запуске сборки.
На первый взгляд инструкции вида:
1
2
3
4
5
6
7
plugins {
id 'java'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
могут выглядеть как элементы отдельного языка.
Фактически это вызовы методов API Gradle, реализованного на Java. Gradle DSL не является самостоятельным языком, а представляет собой удобную оболочку над объектной моделью Gradle.
Именно поэтому build.gradle следует воспринимать не как статический конфигурационный файл, а как исполняемый код, который создает и настраивает объекты, участвующие в процессе сборки.
Project, Task и Action
При выполнении build.gradle Gradle создает и настраивает объекты своей внутренней модели.
В упрощенном виде эту модель можно представить следующим образом:
1
2
3
4
5
6
7
build.gradle
↓
Project
↓
Task
↓
Action
Здесь build.gradle — скрипт сборки; Project — объект, представляющий текущий проект; Task — отдельный шаг сборки; Action — код, выполняемый внутри задачи.
Project
Project — центральный объект Gradle, представляющий текущий модуль сборки.
Он содержит свойства проекта (group, name, version), подключенные плагины, зависимости, задачи, методы конфигурации. Во время выполнения build.gradle объект Project доступен неявно, поэтому большинство инструкций в скрипте фактически являются вызовами его методов.
Task
Task представляет отдельный шаг сборки. Каждая задача имеет имя, набор свойств и может зависеть от других задач.
Примеры стандартных задач: compileJava, test, jar, build.
Action
Action — это код, выполняемый внутри задачи.
Обычно действия добавляются с помощью методов doFirst {} и doLast {}.
1
2
3
4
5
tasks.register('hello') {
doLast {
println 'Hello, Gradle!'
}
}
В данном примере:
hello— задача;doLast { ... }— действие;println— код, который будет выполнен при запуске задачи.
Итоговая идея
build.gradle выполняется в контексте объекта Project, внутри которого создаются и настраиваются задачи (Task), содержащие исполняемые действия (Action).
Три фазы выполнения Gradle
Работа Gradle делится на три последовательные фазы:
- Initialization
- Configuration
- Execution
В упрощенном виде это можно представить следующим образом:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Initialization
init.gradle
settings.gradle
↓
Gradle
Settings
Configuration
build.gradle (для каждого проекта)
↓
Script
↓
Project
↓
Task
Execution
Выбранные задачи
↓
Выполнение Actions
1. Initialization
На этапе инициализации Gradle определяет структуру сборки.
Выполняются:
init.gradle— глобальные пользовательские настройки;settings.gradle— описание структуры проекта.
В результате создаются объекты:
Gradle— корневой объект сборки;Settings— объект, описывающий состав проектов.
Для многомодульного проекта именно settings.gradle определяет, какие модули входят в сборку.
1
include 'api', 'service', 'web'
2. Configuration
На этапе конфигурации Gradle выполняет build.gradle каждого проекта. В процессе создаются объекты Project, регистрируются задачи (Task), настраиваются плагины, зависимости и свойства.
Важно понимать, что на этом этапе задачи создаются и конфигурируются, но не выполняются.
3. Execution
На этапе выполнения Gradle определяет, какие задачи действительно нужно запустить, и выполняет их в соответствии с графом зависимостей.
Например, команда:
1
./gradlew build
приводит к выполнению задачи build и всех задач, от которых она зависит.
Пример
1
2
3
4
5
6
7
println 'Configuration phase'
tasks.register('hello') {
doLast {
println 'Execution phase'
}
}
При запуске:
1
./gradlew hello
будет выведено:
1
2
Configuration phase
Execution phase
Если выполнить:
1
./gradlew tasks
будет выведено только:
1
Configuration phase
поскольку задача hello не запускается.
Итоговая схема
1
2
3
4
5
6
7
8
9
10
11
12
13
settings.gradle
↓
Initialization
↓
build.gradle
↓
Configuration
↓
Project + Tasks
↓
Execution
↓
Результат сборки
Понимание этих трех фаз важно для работы с Gradle, поскольку оно объясняет, почему код в build.gradle выполняется при каждом запуске, а действия задач (doFirst, doLast) выполняются только тогда, когда соответствующая задача действительно запускается.
Task Graph
Задачи в Gradle образуют ориентированный ациклический граф (Directed Acyclic Graph, DAG).
Это означает, что каждая задача может зависеть от других задач, а Gradle автоматически определяет порядок их выполнения.
В упрощенном виде граф для стандартной задачи build выглядит следующим образом:
1
2
3
4
5
build
├── assemble
│ └── jar
└── check
└── test
При запуске команды:
1
./gradlew build
Gradle сначала строит граф зависимостей, затем выполняет только те задачи, которые необходимы для достижения запрошенной задачи.
Пример зависимости между задачами
1
2
3
4
5
6
7
8
9
10
11
12
13
tasks.register('prepare') {
doLast {
println 'Preparing...'
}
}
tasks.register('deploy') {
dependsOn 'prepare'
doLast {
println 'Deploying...'
}
}
В данном случае задача deploy зависит от задачи prepare.
1
prepare → deploy
При выполнении:
1
./gradlew deploy
сначала будет выполнена задача prepare, затем deploy.
Другие способы задания порядка
Gradle предоставляет и другие механизмы управления порядком выполнения задач:
dependsOn— объявляет обязательную зависимость;mustRunAfter— задает порядок выполнения без зависимости;finalizedBy— указывает задачу, которая должна быть выполнена после основной.
Отличие от Maven
В Maven порядок выполнения определяется заранее жизненным циклом (compile, test, package).В Gradle порядок вычисляется динамически на основе связей между задачами. Именно Task Graph является ключевым механизмом, который определяет, какие задачи и в каком порядке будут выполнены.
Задачи, добавляемые Java Plugin
При подключении плагина:
1
2
3
plugins {
id 'java'
}
Gradle автоматически создает набор стандартных задач для работы с Java-проектом.
В упрощенном виде их взаимосвязь выглядит следующим образом:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
compileJava
↓
processResources
↓
classes
↓
jar
↓
assemble
test
↓
check
assemble + check
↓
build
Основные задачи
compileJava— компиляция исходного кода.processResources— копирование ресурсов изsrc/main/resources.classes— агрегирующая задача, завершающая подготовку классов и ресурсов.jar— упаковка приложения в JAR-файл.assemble— сборка артефактов без запуска тестов.test— выполнение unit-тестов.check— запуск всех проверок.build— полная сборка проекта.clean— удаление каталогаbuild/.
Задача build сама по себе не содержит бизнес-логики. Она агрегирует две основные задачи:
1
2
3
build
├── assemble
└── check
assembleотвечает за создание артефактов;checkвыполняет тесты и другие проверки.
Задача clean удаляет каталог build/ и все результаты предыдущих сборок.
Следующая команда сначала очищает рабочий каталог, а затем выполняет полную сборку проекта:
1
./gradlew clean build
Почему это важно
После подключения Java Plugin большая часть стандартного build pipeline уже настроена. Именно поэтому в простом Java-проекте достаточно указать:
1
2
3
plugins {
id 'java'
}
и Gradle автоматически создаст все основные задачи сборки.
Gradle Wrapper
В большинстве проектов Gradle запускается не через установленный в системе gradle, а через Wrapper:
1
./gradlew build
Gradle Wrapper позволяет зафиксировать конкретную версию Gradle и гарантировать, что все разработчики и CI-серверы используют одну и ту же версию.
Файлы Wrapper
При создании Wrapper в проект добавляются следующие файлы:
1
2
3
4
gradlew
gradlew.bat
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties
Где задается версия Gradle
Версия Gradle указывается в файле:
1
gradle/wrapper/gradle-wrapper.properties
Например:
1
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
Как работает Wrapper
При первом запуске ./gradlew происходит следующее:
- Wrapper читает
gradle-wrapper.properties. - Загружает указанную версию Gradle.
- Сохраняет ее в локальный кэш.
- Запускает сборку с этой версией.
При последующих запусках используется уже скачанная версия.
Почему рекомендуется использовать Wrapper
Gradle Wrapper устраняет зависимость от установленной в системе версии Gradle и гарантирует, что все разработчики и CI-серверы используют одну и ту же версию инструмента.
Это обеспечивает одинаковое поведение сборки на разных машинах и позволяет избежать ошибок, связанных с несовместимостью версий.
По этой причине в реальных проектах практически всегда используется команда ./gradlew build, а не gradle build.
Полезные команды
Ниже приведены несколько команд, которые полезно знать при работе с Gradle.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# вывести список доступных задач
./gradlew tasks
# очистить результаты предыдущей сборки
./gradlew clean
# выполнить unit-тесты
./gradlew test
# выполнить полную сборку проекта
./gradlew build
# запустить Spring Boot приложение
./gradlew bootRun
# показать дерево зависимостей
./gradlew dependencies
# показать, откуда пришла конкретная зависимость
./gradlew dependencyInsight --dependency lombok
На практике чаще всего используются команды clean, test, build, bootRun и dependencies.
Дополнительные возможности
Пользовательские задачи
Gradle позволяет не только использовать готовые задачи, но и определять собственные. Простейшую задачу можно объявить прямо в build.gradle:
1
2
3
4
5
tasks.register('hello') {
doLast {
println 'Hello, Gradle!'
}
}
После этого задача запускается обычной командой:
1
./gradlew hello
Вынос логики в отдельные скрипты
По мере роста проекта build script можно разбивать на несколько файлов и подключать их с помощью apply from::
1
2
apply from: 'gradle/publishing.gradle'
apply from: 'gradle/docker.gradle'
Собственные плагины
Если логика сборки используется повторно, ее можно оформить в виде собственного Gradle-плагина.
Плагины обычно пишутся на Java, Groovy или Kotlin и позволяют инкапсулировать задачи, настройки и соглашения сборки.
Итоговая ментальная модель
Если свести все рассмотренное к одной схеме, работа Gradle выглядит следующим образом:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
settings.gradle
↓
Определение структуры сборки
↓
build.gradle
↓
Project
↓
Tasks
↓
Actions
↓
Task Graph
↓
Execution
↓
build/
Иными словами:
settings.gradleопределяет состав проекта;build.gradleсодержит код конфигурации сборки;Projectявляется центральным объектом Gradle;Taskпредставляет отдельный шаг сборки;Actionсодержит исполняемый код задачи;- Task Graph определяет порядок выполнения задач;
- результаты сборки помещаются в каталог
build/.
Если воспринимать Gradle через эту модель, build script перестает выглядеть как набор специальных конструкций и становится обычным кодом, который конфигурирует объектную модель сборки.