Сборочный скрипт Java Gradle
Анатомия сборочного скрипта Gradle для Java: плагины, зависимости, задачи и основы DSL.
Сборочный скрипт Gradle описывает, как компилировать, тестировать и упаковывать проект — не в виде жёсткого XML-документа, а в виде кода. Gradle читает файл build.gradle (Groovy DSL) или build.gradle.kts (Kotlin DSL), превращает объявления внутри в граф задач и выполняет только те задачи, которые нужны вашей команде, в правильном порядке. Там, где Maven даёт фиксированный жизненный цикл, Gradle даёт программируемый: применение плагина, объявление зависимости и определение задачи — обычные операторы настоящего языка.
На этой странице предполагается, что вы уже знакомы с Gradle на высоком уровне — если нет, начните с введения в Gradle. Здесь мы разберём реальный build.gradle блок за блоком: плагины, репозитории, зависимости и задачи.
Анатомия build.gradle
Минимальный сборочный скрипт Java состоит из четырёх блоков: какие плагины применять, откуда скачивать зависимости, что это за зависимости и немного конфигурации. Вот полный идиоматичный пример на Kotlin DSL:
plugins {
java
application
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:32.1.3-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
application {
mainClass = "com.example.App"
}
tasks.test {
useJUnitPlatform()
}Плагин java сам по себе даёт вам compileJava, test, jar и ещё дюжину задач бесплатно. Плагин application добавляет run и installDist. Строка tasks.test { useJUnitPlatform() } подключает задачу test для запуска тестов JUnit 5 — без неё Gradle по умолчанию использует устаревший движок JUnit 4 и молча ничего не запускает. Всё остальное — это конфигурация того, что делают эти задачи.
Плагины, репозитории и жизненный цикл сборки
В Gradle почти ничего не встроено — возможности приходят в виде плагинов. Плагин java является основой для работы с JVM; остальные надстраиваются поверх него:
| Плагин | Что добавляет |
|---|---|
java | compileJava, test, jar, наборы исходников, конфигурации dependencies |
application | run и дистрибутив с запускающими скриптами |
java-library | Разделение api и implementation для библиотек |
org.springframework.boot | bootJar, bootRun для приложений Spring Boot |
jacoco | Отчётность по покрытию кода, встроенная в test |
repositories { } сообщает Gradle, откуда скачивать зависимости — mavenCentral() является обычным выбором. Без репозитория ни одна внешняя зависимость не может быть загружена.
Объявление зависимостей и их областей видимости
Зависимости объявляются с конфигурацией, которая определяет, где они появляются в classpath. Правильный выбор конфигурации сохраняет compile classpath чистым и ускоряет сборки:
dependencies {
// On the compile and runtime classpath, but NOT exposed to consumers
implementation("org.apache.commons:commons-lang3:3.14.0")
// Part of this library's public API — leaks to consumers (java-library only)
api("com.google.guava:guava:32.1.3-jre")
// Needed to compile, but provided at runtime by the environment
compileOnly("org.projectlombok:lombok:1.18.30")
// Only on the test classpath
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
// Only at runtime (e.g. a JDBC driver loaded by name)
runtimeOnly("org.postgresql:postgresql:42.7.1")
}Формат координат — group:name:version — те же координаты, которые Maven использует в своём pom.xml. Когда две зависимости подтягивают один и тот же модуль в разных версиях, стратегия Gradle по умолчанию помещает в classpath одну, наибольшую версию — выполняемый пример ниже именно это и моделирует.
Задачи: единица работы
Каждое действие, которое выполняет Gradle, является задачей, и задачи объявляют зависимости от других задач. Выполнение gradle build не запускает одно действие; оно обходит граф и выполняет каждую предпосылку ровно один раз. Вы также можете определять собственные задачи:
tasks.register("printVersion") {
group = "help"
description = "Prints the project version."
doLast {
println("Project version is $version")
}
}
// Make the jar task wait for our custom task
tasks.named("jar") {
dependsOn("printVersion")
}Ещё два факта делают Gradle быстрым. Во-первых, он инкрементален: задача, входные и выходные данные которой не изменились, помечается как UP-TO-DATE и пропускается. Во-вторых, Gradle Wrapper (./gradlew, опирающийся на gradle/wrapper/gradle-wrapper.properties) фиксирует одну версию Gradle для каждого проекта, поэтому каждый разработчик и каждая машина CI собирают проект с одним и тем же инструментарием — устанавливать Gradle глобально не нужно.
Практический пример: сборка, смоделированная на чистом Java
Gradle сам по себе недоступен в этом раннере, поэтому программа ниже моделирует три идеи, которые лежат в основе работы сборочного скрипта, — граф задач и порядок их выполнения, инкрементальное пропускание актуальных задач и разрешение конфликтов версий зависимостей — используя только JDK. Это ментальная модель того, что в действительности выполняет gradle build.
Что можно извлечь из запуска:
- Список задач для
gradle buildвычисляется топологической сортировкой, а не записывается вручную.compileJavaиprocessResourcesидут доclasses, который идёт доjarиtest, которые идут доbuild— именно в том порядке, который Gradle выводит с префиксом:taskName, поскольку задача может выполняться только после того, как всё, от чего онаdependsOn, завершено. - Ромб в графе запускает общую предпосылку один раз, а не дважды. И
jar, иtestзависят отclasses, однакоclassesпоявляется в порядке выполнения только один раз — именно множествоdoneне позволяет Gradle перекомпилировать один и тот же код для каждой последующей задачи. - Второй запуск демонстрирует инкрементальное поведение Gradle:
compileJava,processResourcesиclassesимеют статусUP-TO-DATEи пропускаются, поэтому фактически выполняются только3задачи. Именно поэтому неизменённый проект пересобирается за миллисекунды — Gradle сравнивает входные и выходные данные задач и не делает никакой лишней работы. - Разрешение зависимостей сводит конфликт версий к одному победителю:
slf4j-apiзапрашивается на версии2.0.9(прямая зависимость) и1.7.36(транзитивная через guava), но в разрешённом classpath он присутствует один раз в версии2.0.9. Стратегия Gradle по умолчанию — выигрывает наибольшая версия, поэтому в classpath попадает один согласованный jar, а не два конкурирующих. - Последняя строка называет версию Gradle
8.7, как если бы она была считана изgradle-wrapper.properties. В реальном проекте wrapper хранит эту версию в системе контроля версий, поэтому./gradlew buildиспользует одну и ту же версию Gradle для всех — сборка воспроизводима вне зависимости от того, что (и установлено ли что-либо) на машине.