W3docs

Сборочный скрипт 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; остальные надстраиваются поверх него:

ПлагинЧто добавляет
javacompileJava, test, jar, наборы исходников, конфигурации dependencies
applicationrun и дистрибутив с запускающими скриптами
java-libraryРазделение api и implementation для библиотек
org.springframework.bootbootJar, 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.

java— editable, runs on the server

Что можно извлечь из запуска:

  • Список задач для 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 для всех — сборка воспроизводима вне зависимости от того, что (и установлено ли что-либо) на машине.

Практика

Практика
В Java-проекте на Gradle зависимость 'org.slf4j:slf4j-api:2.0.9' объявлена напрямую, а транзитивная зависимость подтягивает 'org.slf4j:slf4j-api:1.7.36'. Что окажется в classpath при стратегии разрешения Gradle по умолчанию?
В Java-проекте на Gradle зависимость 'org.slf4j:slf4j-api:2.0.9' объявлена напрямую, а транзитивная зависимость подтягивает 'org.slf4j:slf4j-api:1.7.36'. Что окажется в classpath при стратегии разрешения Gradle по умолчанию?
Was this page helpful?