Dependencias de Maven en Java
Declara y gestiona dependencias Java en Maven con dependency, scope y resolución transitiva.
Los proyectos Java reales raramente están solos. Incorporan frameworks de logging, clientes HTTP, analizadores JSON y bibliotecas de pruebas escritas por otras personas. El trabajo de Maven es obtener esas bibliotecas, obtener sus bibliotecas a su vez, y ensamblar un classpath consistente sin que tengas que rastrear un solo JAR manualmente. Entender cómo lo hace es la diferencia entre una compilación que simplemente funciona y una tarde perdida por un NoSuchMethodError.
Este capítulo cubre cómo se nombra una dependencia (coordenadas), cómo los alcances controlan dónde es visible cada biblioteca, cómo Maven recorre el grafo de dependencias transitivas y cómo resuelve los conflictos de versiones. Se asume que ya tienes un pom.xml; si no es así, comienza con el capítulo Maven POM.
Coordenadas: Cómo se nombra una dependencia
Cada artefacto en el mundo Maven se identifica mediante un conjunto de coordenadas. Las tres que siempre debes proporcionar son el groupId (quién lo publica, generalmente un dominio invertido), el artifactId (el nombre del proyecto) y la version. Juntas apuntan exactamente a un JAR en un repositorio.
Declaras una dependencia dentro del bloque <dependencies> de tu pom.xml:
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>La forma abreviada groupId:artifactId:version se llama cadena GAV, y la verás en todas partes: en los mensajes de error, en el árbol de dependencias y en las páginas web del repositorio central. Una cuarta coordenada, el tipo (jar por defecto), y una quinta, el clasificador (para variantes como sources o javadoc), completan la dirección completa.
Alcances: Cuándo es visible una dependencia
No todas las dependencias pertenecen a todos los classpaths. Un framework de pruebas no debería enviarse en tu JAR de producción, y una API de servlet proporcionada por el servidor de aplicaciones no debería incluirse dos veces. Maven controla esto con el elemento <scope>.
| Alcance | Compilación | Prueba | Ejecución | Empaquetado | Uso típico |
|---|---|---|---|---|---|
compile (por defecto) | Sí | Sí | Sí | Sí | Bibliotecas principales que llamas directamente |
provided | Sí | Sí | No | No | APIs que provee el contenedor (servlet, driver JDBC) |
runtime | No | Sí | Sí | Sí | Implementaciones necesarias solo en tiempo de ejecución |
test | No | Sí | No | No | JUnit, Mockito, bibliotecas de aserciones |
system | Sí | Sí | No | No | JARs locales por ruta absoluta (evitar) |
Una dependencia con alcance test es la no predeterminada más común. JUnit nunca se filtra en tu artefacto publicado:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>Dependencias transitivas
Cuando dependes de una biblioteca, también dependes de todo lo que ella depende. Maven lee el pom.xml publicado de cada artefacto, sigue esas declaraciones de forma recursiva y agrega todo el grafo a tu classpath automáticamente. Estas entradas indirectas son dependencias transitivas.
Por eso una sola línea <dependency> para un framework web puede arrastrar docenas de JARs que nunca nombraste. El alcance sigue aplicándose durante este recorrido: una dependencia con alcance test no arrastra sus dependencias transitivas al classpath de compilación, y las dependencias provided no se propagan transitivamente en absoluto.
Puedes ver el grafo completo con el plugin de dependencias:
$ 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:compileMediación de conflictos de versiones
Un grafo tan profundo casi siempre requiere el mismo artefacto en dos versiones diferentes. Maven no puede poner ambas en un mismo classpath, por lo que elige una usando la mediación nearest-wins (gana el más cercano): la versión declarada a la menor profundidad desde la raíz de tu proyecto prevalece, y las demás se omiten por conflicto.
En el árbol anterior, tu proyecto solicita org.json:json:1.9 directamente (profundidad 1), mientras que org.web:server solicita 1.7 de forma transitiva (profundidad 2). La declaración en profundidad 1 gana. Si dos candidatos están a la misma profundidad, gana el declarado primero en el pom.xml.
Cuando la elección automática es incorrecta, tomas el control explícitamente. Una dependencia directa siempre gana, o puedes fijar versiones en todo el proyecto con <dependencyManagement>:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>1.9</version>
</dependency>
</dependencies>
</dependencyManagement>Para eliminar completamente una rama transitiva no deseada, 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 ejemplo completo
Maven en sí no está disponible en este ejecutor de código, por lo que el programa a continuación modela su resolutor en Java puro. Publica algunos artefactos en un pequeño repositorio en memoria, luego realiza el mismo recorrido en anchura y la misma mediación nearest-wins que usa Maven para aplanar un grafo de dependencias en un único classpath. Observa cómo el org.json:json:1.7 más profundo pierde frente al 1.9 más superficial.
Lo que hay que extraer de la ejecución:
- El classpath resuelto lista cada artefacto exactamente una vez, reflejando cómo Maven aplana un grafo en un único conjunto de JARs sin entradas duplicadas de group:artifact.
org.json:jsonaparece en la versión1.9, no1.7, porque la mediación nearest-wins conserva el candidato encontrado en la menor profundidad (profundidad 1 supera a profundidad 2).- La columna
depthhace concreto el concepto de "más cercano":app:appestá en profundidad 0, sus dependencias directas en profundidad 1, yorg.log:logarrastrado transitivamente en profundidad 2. - "Tree edges visited: 4" cuenta las relaciones de dependencia declaradas, mientras que "Distinct artifacts: 4" muestra el grafo colapsado en cuatro coordenadas únicas tras la mediación.
- Una coordenada se omite en el momento en que se vuelve a ver (
depthOf.containsKey(ga)), que es exactamente la razón por la que el1.7más profundo queda "omitted for conflict" en lugar de añadirse una segunda vez.
Una vez que tus dependencias se resuelven correctamente, la siguiente pregunta es cuándo Maven descarga, compila, prueba y empaqueta. Ese orden está regido por las fases de compilación cubiertas en el capítulo del ciclo de vida de Maven.