Post

Заметки о Gradle: Project, Task и Task Graph

Заметки о 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 выполняет следующие действия:

  1. Выполняет build.gradle;
  2. Загружает зависимости из репозиториев или локального кэша;
  3. Создает и настраивает задачи;
  4. Определяет порядок их выполнения;
  5. Запускает необходимые задачи;
  6. Сохраняет результаты в каталог 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 делится на три последовательные фазы:

  1. Initialization
  2. Configuration
  3. 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 происходит следующее:

  1. Wrapper читает gradle-wrapper.properties.
  2. Загружает указанную версию Gradle.
  3. Сохраняет ее в локальный кэш.
  4. Запускает сборку с этой версией.

При последующих запусках используется уже скачанная версия.

Почему рекомендуется использовать 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 перестает выглядеть как набор специальных конструкций и становится обычным кодом, который конфигурирует объектную модель сборки.

This post is licensed under CC BY 4.0 by the author.