W3docs

Script di Build Gradle per Java

Anatomia di uno script di build Gradle per Java: plugin, dipendenze, task e nozioni di base del DSL.

Uno script di build Gradle descrive come compilare, testare e impacchettare un progetto — non come un rigido documento XML, ma come codice. Gradle legge un file build.gradle (Groovy DSL) o un file build.gradle.kts (Kotlin DSL), trasforma le dichiarazioni al suo interno in un grafo di task ed esegue solo i task richiesti dal tuo comando, nell'ordine corretto. Dove Maven offre un ciclo di vita fisso, Gradle ne offre uno programmabile: applicare un plugin, dichiarare una dipendenza e definire un task sono tutte istruzioni ordinarie in un linguaggio reale.

Questa pagina presuppone che tu già conosca Gradle ad alto livello — in caso contrario, inizia con l'introduzione a Gradle. Qui analizziamo un vero build.gradle blocco per blocco: plugin, repository, dipendenze e task.

L'anatomia di build.gradle

Uno script di build Java minimale ha quattro blocchi: quali plugin applicare, da dove scaricare le dipendenze, quali sono quelle dipendenze e un po' di configurazione. Ecco uno completo e idiomatico in 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()
}

Il plugin java da solo ti fornisce compileJava, test, jar e una dozzina di altri task gratuitamente. Il plugin application aggiunge run e installDist. La riga tasks.test { useJUnitPlatform() } collega il task test per eseguire i test JUnit 5 — senza di essa Gradle utilizza per default il vecchio motore JUnit 4 e non esegue nulla in silenzio. Tutto il resto è configurazione di ciò che quei task fanno.

Plugin, repository e il ciclo di vita della build

Quasi nulla in Gradle è integrato — le funzionalità arrivano sotto forma di plugin. Il plugin java è la base per il lavoro con la JVM; gli altri si aggiungono sopra:

PluginCosa aggiunge
javacompileJava, test, jar, source set, le configurazioni dependencies
applicationrun e una distribuzione impacchettata con script di avvio
java-libraryLa distinzione tra api e implementation per le librerie
org.springframework.bootbootJar, bootRun per le applicazioni Spring Boot
jacocoReport di code coverage collegato a test

repositories { } indica a Gradle da dove scaricare le dipendenze — mavenCentral() è la scelta abituale. Senza un repository, nessuna dipendenza esterna può essere risolta.

Dichiarare le dipendenze e i loro scope

Le dipendenze vengono dichiarate con una configurazione che controlla dove compaiono nel classpath. Scegliere quella giusta mantiene il tuo classpath di compilazione pulito e le build veloci:

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")
}

Il formato delle coordinate è group:name:version — le stesse coordinate che Maven usa nel suo pom.xml. Quando due dipendenze richiedono lo stesso modulo a versioni diverse, la strategia predefinita di Gradle mette sul classpath un'unica versione, quella più alta — l'esempio eseguibile qui sotto modella esattamente questo comportamento.

Task: l'unità di lavoro

Ogni azione che Gradle esegue è un task, e i task dichiarano dipendenze da altri task. Eseguire gradle build non fa una sola cosa; percorre un grafo ed esegue ogni prerequisito una sola volta. Puoi anche definire i tuoi task personalizzati:

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")
}

Altri due aspetti rendono Gradle veloce. Prima di tutto è incrementale: un task i cui input e output non sono cambiati viene riportato come UP-TO-DATE e saltato. In secondo luogo, il Gradle Wrapper (./gradlew, supportato da gradle/wrapper/gradle-wrapper.properties) fissa una versione di Gradle per ogni progetto, così ogni sviluppatore e macchina CI costruisce con la stessa toolchain — non è necessario installare Gradle globalmente.

Un esempio pratico: una build modellata in Java puro

Gradle stesso non è disponibile su questo runner, quindi il programma qui sotto modella le tre idee che fanno funzionare uno script di build — il grafo dei task e il suo ordine di esecuzione, il comportamento incrementale di salto degli up-to-date, e la risoluzione dei conflitti di versione tra dipendenze — usando solo il JDK. È il modello mentale che gradle build esegue nella realtà.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • L'elenco dei task per gradle build è calcolato tramite un ordinamento topologico, non scritto a mano. compileJava e processResources vengono prima di classes, che viene prima di jar e test, che vengono prima di build — esattamente l'ordine che Gradle stampa con il prefisso :taskName per ogni task, perché un task può essere eseguito solo dopo che tutto ciò da cui dependsOn è terminato.
  • Un diamante nel grafo esegue un prerequisito condiviso una sola volta, non due volte. Sia jar che test dipendono da classes, eppure classes appare una sola volta nell'ordine — il set done è ciò che impedisce a Gradle di ricompilare lo stesso codice per ogni task a valle.
  • La seconda esecuzione mostra il comportamento incrementale di Gradle: compileJava, processResources e classes sono UP-TO-DATE e vengono saltati, così solo 3 task vengono effettivamente eseguiti. Questo è il motivo per cui un progetto invariato viene ricompilato in millisecondi — Gradle confronta gli input e gli output dei task e non fa lavoro che può evitare.
  • La risoluzione delle dipendenze collassa un conflitto di versione a un unico vincitore: slf4j-api è richiesto sia a 2.0.9 (diretto) che a 1.7.36 (transitivo tramite guava), ma il classpath risolto lo elenca una sola volta a 2.0.9. La strategia predefinita di Gradle è la versione più alta vince, quindi un singolo jar consistente finisce sul classpath invece di due copie in conflitto.
  • L'ultima riga indica la versione di Gradle 8.7 come se fosse letta da gradle-wrapper.properties. In un vero progetto il wrapper memorizza quella versione nel controllo del codice sorgente, così ./gradlew build usa lo stesso Gradle per tutti — la build è riproducibile indipendentemente da ciò che è (o non è) installato sulla macchina.

Esercizio

Pratica
Un progetto Java Gradle dichiara 'org.slf4j:slf4j-api:2.0.9' direttamente, mentre una dipendenza transitiva richiede 'org.slf4j:slf4j-api:1.7.36'. Con la strategia di risoluzione predefinita di Gradle, cosa finisce nel classpath?
Un progetto Java Gradle dichiara 'org.slf4j:slf4j-api:2.0.9' direttamente, mentre una dipendenza transitiva richiede 'org.slf4j:slf4j-api:1.7.36'. Con la strategia di risoluzione predefinita di Gradle, cosa finisce nel classpath?
Was this page helpful?