Dipendenze Maven in Java
Dichiara e gestisci le dipendenze Java in Maven con dependency, scope e risoluzione transitiva.
I progetti Java reali raramente sono autonomi. Importano framework di logging, client HTTP, parser JSON e librerie di test scritte da altri. Il compito di Maven è scaricare quelle librerie, scaricare a sua volta le loro librerie e assemblare un unico classpath coerente senza che tu debba tracciare un singolo JAR a mano. Capire come funziona fa la differenza tra una build che funziona al primo tentativo e un pomeriggio perso dietro a un NoSuchMethodError.
Questo capitolo spiega come viene denominata una dipendenza (coordinate), come gli scope controllano dove ogni libreria è visibile, come Maven percorre il grafo delle dipendenze transitive e come risolve i conflitti di versione. Si presume che tu abbia già un pom.xml; in caso contrario, inizia dal capitolo sul Maven POM.
Coordinate: come viene denominata una dipendenza
Ogni artefatto nel mondo Maven è identificato da un insieme di coordinate. Le tre che fornisci sempre sono il groupId (chi lo pubblica, di solito un dominio inverso), l'artifactId (il nome del progetto) e la version. Insieme puntano esattamente a un JAR in un repository.
Dichiari una dipendenza all'interno del blocco <dependencies> del tuo pom.xml:
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>La forma abbreviata groupId:artifactId:version viene chiamata stringa GAV e la vedrai ovunque: nei messaggi di errore, nell'albero delle dipendenze e nelle pagine web del repository centrale. Una quarta coordinata, il type (jar per impostazione predefinita), e una quinta, il classifier (per varianti come sources o javadoc), completano l'indirizzo completo.
Scope: quando una dipendenza è visibile
Non tutte le dipendenze appartengono a ogni classpath. Un framework di test non dovrebbe essere incluso nel tuo JAR di produzione, e una servlet API fornita dal server applicativo non dovrebbe essere inclusa due volte. Maven controlla questo con l'elemento <scope>.
| Scope | Compilazione | Test | Runtime | Incluso | Uso tipico |
|---|---|---|---|---|---|
compile (default) | Sì | Sì | Sì | Sì | Librerie core chiamate direttamente |
provided | Sì | Sì | No | No | API fornite dal container (servlet, driver JDBC) |
runtime | No | Sì | Sì | Sì | Implementazioni necessarie solo a runtime |
test | No | Sì | No | No | JUnit, Mockito, librerie di asserzione |
system | Sì | Sì | No | No | JAR locali tramite percorso assoluto (da evitare) |
La dipendenza con scope test è la più comune tra quelle non predefinite. JUnit non entra mai nell'artefatto distribuito:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>Dipendenze transitive
Quando dipendi da una libreria, dipendi anche da tutto ciò da cui essa dipende. Maven legge il pom.xml pubblicato di ogni artefatto, segue ricorsivamente quelle dichiarazioni e aggiunge l'intero grafo al tuo classpath automaticamente. Queste voci indirette sono le dipendenze transitive.
È per questo motivo che una singola riga <dependency> per un framework web può trascinare decine di JAR che non hai mai nominato. Lo scope si applica ancora durante questa navigazione: una dipendenza con scope test non porta le sue dipendenze transitive nel classpath di compilazione, e le dipendenze provided non vengono propagate transitivamente.
Puoi vedere il grafo completo con il plugin dependency:
$ mvn dependency:tree
[INFO] com.example:app:jar:1.0
[INFO] +- org.web:server:jar:2.4:compile
[INFO] | +- org.log:log:jar:1.2:compile
[INFO] | \- org.json:json:jar:1.7:compile - omitted for conflict with 1.9
[INFO] \- org.json:json:jar:1.9:compileMediazione dei conflitti di versione
Un grafo così profondo quasi sempre richiede lo stesso artefatto a due versioni diverse. Maven non può mettere entrambe su un unico classpath, quindi ne sceglie una usando la mediazione nearest-wins (vince il più vicino): la versione dichiarata alla profondità minore dalla radice del tuo progetto prevale, e le altre vengono omesse per conflitto.
Nell'albero sopra, il tuo progetto richiede org.json:json:1.9 direttamente (profondità 1), mentre org.web:server richiede 1.7 transitivamente (profondità 2). La dichiarazione a profondità 1 vince. Se due candidati si trovano alla stessa profondità, vince quello dichiarato per primo nel pom.xml.
Quando la scelta automatica è errata, prendi il controllo esplicitamente. Una dipendenza diretta vince sempre, oppure puoi fissare le versioni nell'intero progetto con <dependencyManagement>:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>1.9</version>
</dependency>
</dependencies>
</dependencyManagement>Per eliminare completamente un ramo transitivo indesiderato, usa <exclusions>:
<dependency>
<groupId>org.web</groupId>
<artifactId>server</artifactId>
<version>2.4</version>
<exclusions>
<exclusion>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</exclusion>
</exclusions>
</dependency>Un esempio completo
Maven stesso non è disponibile in questo code runner, quindi il programma seguente modella il suo resolver in Java puro. Pubblica alcuni artefatti in un piccolo repository in memoria, poi esegue la stessa visita in ampiezza e la mediazione nearest-wins che Maven usa per appiattire un grafo di dipendenze in un unico classpath. Osserva come il più profondo org.json:json:1.7 perde rispetto al più superficiale 1.9.
Cosa ricavare dall'esecuzione:
- Il classpath risolto elenca ogni artefatto esattamente una volta, rispecchiando come Maven appiattisce un grafo in un unico insieme di JAR senza voci duplicate per group:artifact.
org.json:jsonappare alla versione1.9, non1.7, perché la mediazione nearest-wins mantiene il candidato trovato alla profondità minore (profondità 1 batte profondità 2).- La colonna
depthrende concreto il concetto di "più vicino":app:appè alla profondità 0, le sue dipendenze dirette sono alla profondità 1, eorg.log:logestratto transitivamente è alla profondità 2. - "Tree edges visited: 4" conta le relazioni di dipendenza dichiarate, mentre "Distinct artifacts: 4" mostra il grafo ridotto a quattro coordinate univoche dopo la mediazione.
- Una coordinata viene saltata nel momento in cui viene vista di nuovo (
depthOf.containsKey(ga)), ed è esattamente per questo che il più profondo1.7è "omitted for conflict" anziché aggiunto una seconda volta.
Una volta che le dipendenze si risolvono correttamente, la prossima domanda è quando Maven scarica, compila, testa e impacchetta. Quell'ordine è governato dalle fasi di build trattate nel capitolo sul ciclo di vita Maven.