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:
| Plugin | Cosa aggiunge |
|---|---|
java | compileJava, test, jar, source set, le configurazioni dependencies |
application | run e una distribuzione impacchettata con script di avvio |
java-library | La distinzione tra api e implementation per le librerie |
org.springframework.boot | bootJar, bootRun per le applicazioni Spring Boot |
jacoco | Report 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à.
Cosa ricavare dall'esecuzione:
- L'elenco dei task per
gradle buildè calcolato tramite un ordinamento topologico, non scritto a mano.compileJavaeprocessResourcesvengono prima diclasses, che viene prima dijaretest, che vengono prima dibuild— esattamente l'ordine che Gradle stampa con il prefisso:taskNameper ogni task, perché un task può essere eseguito solo dopo che tutto ciò da cuidependsOnè terminato. - Un diamante nel grafo esegue un prerequisito condiviso una sola volta, non due volte. Sia
jarchetestdipendono daclasses, eppureclassesappare una sola volta nell'ordine — il setdoneè 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,processResourceseclassessonoUP-TO-DATEe vengono saltati, così solo3task 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 a2.0.9(diretto) che a1.7.36(transitivo tramite guava), ma il classpath risolto lo elenca una sola volta a2.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.7come se fosse letta dagradle-wrapper.properties. In un vero progetto il wrapper memorizza quella versione nel controllo del codice sorgente, così./gradlew buildusa lo stesso Gradle per tutti — la build è riproducibile indipendentemente da ciò che è (o non è) installato sulla macchina.