Tengo que reconocer que cuanto más uso Maven más me gusta. Un sistema de construcción de proyectos estándar, extensible y basado en una lenguaje declarativo[1] es, al menos en su planteamiento, una herramienta muy prometedora.
Sin embargo, el proceso de aprendizaje ha sido largo y duro, entre otras cosas, porque Maven parece estar en un continuo estado de inestabilidad, plagado de detalles indocumentados, de versiones Beta, actualizaciones, etc. Tanto es así, que aún después de haber leído los dos libros gratuitos que hay en la Red[2], la documentación y numerosos foros y artículos, todavía me quedan muchos puntos oscuros.
Hasta ahora Maven me ha sido muy útil en temas como en la automatización del proceso de construcción, almacenamiento de los artefactos de forma ordenada utilizando repositorios compartidos o la obtención de artefactos de terceros almacenados en repositorios en Internet y, sin embargo, me he encontrado muchos problemas en lo que se supone que es un punto fuerte de Maven: la gestión de dependencias.
Aquí comienza una serie de tres entradas en las que voy a tratar de explicar como funciona la gestión de dependencias en Maven pero, sobre todo, voy a profundizar en los aspectos que no están claros en la documentación (o que no lo estaban para mi) y que he aprendido, a las malas, con la experiencia.
En esta entrada hablaré sobre los aspectos básicos de la gestión de dependencias con Maven, la segunda tratará características avanzadas y la tercera y última estará dedicada a diseño y buenas prácticas[3].
Versiones
Maven utiliza un esquema de versiones que consta de las siguientes partes:
<major version>.<minor version>.<incremental version>-<qualifier>
- Versión mayor: normalmente un cambio de versión mayor denota incompatibilidad con las versiones anteriores.
- Versión menor: se incrementa cuando se incorporan nuevas funcionalidades, pero compatibles.
- "Bug fix" o versión incremental: se cambia cada vez que se lanza una nueva versión que repara algún error.
- "Qualifier" o "Build Number": se separa por un guión y marca hitos o "millestones" previos a la distribución del artefacto, por ejemplo,
alpha,betaoGA.
Aunque este es el esquema estándar de versionado, con frecuencia encontrarás que no siempre se utilizan todas las partes de una versión y que se recurre a versiones abreviadas como 1.2, 2.0-beta, etc.
Normalmente, para determinar si una versión es superior a otra se utiliza una comparación numérica, lo cual funcionará siempre que se respete el esquema anterior. Sin embargo es frecuente encontrar versiones con partes que incluyen números además de letras y entonces Maven realizará una comparación alfanumérica que puede no ser siempre correcta. Esto es especialmente habitual en los calificadores. Por ejemplo 1.2.3-alpha-10 se considerará inferior a 1.2.3-alpha-2.
Las versiones se utilizan para etiquetar cada lanzamiento o "release" de un artefacto, son inmutables y corresponden a un conjunto de fuentes etiquetados a su vez en el control de versiones. Sin embargo, durante el desarrollo se generan artefactos temporales denominados "SNAPHOSTS" que no corresponden a un hito; simplemente se utilizan para hacer ensayos o para trabajar en equipo.
El versionado de los snapshots es parcialmente generado por Maven en base al momento de la construcción del artefacto.
El ejemplo muestra la versión de un snapshot; en este caso el snapshot corresponde a un artefacto temporal generado el día 11/2/2006 a las 13:11:14 durante el desarrollo para alcanzar la versión 1.0.1-1.
Este esquema de versionado se utiliza cuando interesa conservar varios snapshots y así poder volver a un estado anterior del desarrollo, pero también se puede configurar Maven para que solo se utilice una versión única de snapshot, en cuyo caso se sustituye el build number por el texto fijo SNAPSHOT, por ejemplo, 1.0.1-SNAPSHOT-1. Cuando Maven se configura de este modo cada snaphsot sustituye al anterior, es decir, solo se conserva una copia del snapshot correspondiente a la última construcción[4]
Dependencias
Para especificar las dependencias en Maven se utiliza la sección dependency del POM:
<project> ... <groupId>com.softwarepills</groupId> <artifactId>sample</artifactId> <version>1.0</version> ... <dependencies> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2</version> <type>jar</type> </dependency> ... </dependencies> ... </project>
Como veremos más adelante, no siempre es necesario enumerar aquí todas las dependencias, ya que nuestro POM puede adquirirlas por otros mecanismos.
Herencia
El primero de estos mecanismos es la herencia:
<project> ... <parent> <groupId>com.softwarepills</groupId> <artifactId>sample</artifactId> <version>1.0</version> </parent> <artifactId>sample-app</artifactId> <version>1.0</version> ... </project>
En el ejemplo, al declarar un padre, nuestro POM adquiere las dependencias de éste (log4j). El concepto de herencia tiene el mismo significado que en POO: el hijo hereda las características del padre.
Por el momento a nosotros solo nos interesan las dependencias, pero es necesario aclarar que también se heredan otros atributos del POM padre por lo que no siempre la herencia es el mejor método para compartir y transmitir dependencias.
Para comprobar como queda el POM incluyendo las aportaciones de los padres podemos utilizar el comando:
mvn help:effective-pom
Dependencias transitivas
Las dependencias transitivas son aquellas que se establecen indirectamente, es decir, si el artefacto A depende de B y B depende de C, entonces se establece una dependencia transitiva entre A y C.
Gracias a esta característica introducida por primera vez en Maven 2.0, podemos especificar solo las dependencias directas y dejar que Maven se encargue de recopilar el resto, o lo que es lo mismo, no tenemos que especificar todas y cada una de las dependencias transitivas en nuestro POM.
Utilizando las dependencias transitivas, Maven construye el árbol de dependencias que se utilizará más adelante con diferentes propósitos.
Para visualizar el árbol de dependencias podemos ejecutar el comando:
mvn dependency:tree
Composición
Como en POO, además de la herencia, en Maven existe otro mecanismo con el que transmitir y compartir atributos o funcionalidad: la composición.
Gracias a la transitividad de las dependencias podemos agrupar un conjunto de versiones en un POM y hacer uso de ellas en diferentes artefactos. En el siguiente ejemplo se agrupan las dependencias relacionadas con el framework Spring:
<project>
...
<groupId>com.softwarepills</groupId>
<artifactId>spring-deps</artifactId>
<version>${spring.version}</version>
<packaging>pom</packaging>
...
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
...
</dependencies>
...
<properties>
<spring.version>2.0.5</spring.version>
</properties>
...
</project>
En este caso, además,utilizamos la variable spring.version para almacenar la versión. Esta técnica hace que nuestros POMs sean más legibles y fáciles de mantener.
Luego, en uno o mas artefactos propios podríamos hacer uso del conjunto de dependencias declarando una dependencia del POM de Spring, como en el siguiente ejemplo:
<project> ... <groupId>com.softwarepills</groupId> <artifactId>spring-project</artifactId> <version>1.0</version> ... <dependencies> <dependency> <groupId>com.softwarepills</groupId> <artifactId>spring-deps</artifactId> <version>2.0.5</version> <type>pom</type> </dependency> ... </dependencies> ... </project>
El principio "prefiere la composición a la herencia" que aconseja en el diseño POO, también rige aquí. La herencia es un mecanismo muy cómodo pero también muy rígido. Si abusas de ella te puedes encontrar problemas a la hora de incorporar modificaciones sobrevenidas.
Resolución de versiones
El objetivo de la gestión de dependencias es obtener el conjunto de artefactos o classpath que se utilizará en una determinada fase de la construcción del proyecto. Hasta ahora hemos hablado del árbol de dependencias, pero es evidente que no es lo que necesitamos… un árbol tiene ramas y una misma dependencia puede aparecer en más de una rama y, lo que es peor, puede aparecer con diferente versión.
Por ejemplo, en el gráfico adjunto se muestra un árbol de dependencias en el que el artefacto E aparece en dos ramas con diferentes versiones.
La resolución de versiones consiste en obtener un classpath sin artefactos duplicados a partir del árbol de versiones. El algoritmo que utiliza Maven para determinar la versión correcta se basa en la profundidad de la dependencia dentro del árbol. Ante un conflicto, Maven siempre va a seleccionar la versión más cercana en el árbol. Así, en el ejemplo, Maven resolverá que la versión del artefacto E a incluir en el classpath será la 1.2 pues está en un nodo menos "profundo" que la versión 1.1.
En caso de que las diferentes versiones se encuentren a la misma profundidad, a falta de un criterio lógico, Maven seleccionará una cualquiera de forma arbitraria.
Hasta ahora solo hemos hablado de versiones "blandas". Con ellas damos "pistas" a Maven para que seleccione la versión apropiada pero, para acotar la resolución de versiones de forma más estricta, podemos usar rangos:
| Rango | Descripción |
(,1.0] |
x <= 1.0 |
1.0 |
Requerimiento "blando" de 1.0 (es solo una recomendación que ayuda a seleccionar la versión si cuadra con todos los rangos) |
[1.0] |
Requerimiento estricto de 1.0 |
[1.2,1.3] |
1.2 <= x <= 1.3 |
[1.0,2.0) |
1.0 <= x < 2.0 |
[1.5,) |
x >= 1.5 |
(,1.0],[1.2,) |
x <= 1.0 o x >= 1.2. Varios rangos separados por coma. |
(,1.1),(1.1,) |
Excluye la 1.1 |
Utilizando rangos en vez de versiones "blandas" limitamos las posibilidades a la hora de resolver las dependencias y, por tanto, puede llegar a darse el caso de que el classpath sea irresoluble.
Al utilizar rangos hayq ue tener cuenta que las versiones snapshot siempre se consideran inferiores a las releases, así que, por ejemplo, [1.2,) no incluye 1.2-SNAPSHOT.
Podemos comprobar como se resuelven las dependencias con el comando:
mvn dependency:resolve
Como veremos en el siguiente apartado, la resolución de versiones no es única. Durante el ciclo de vida de la construcción de un proyecto se generan diferentes árboles de dependencias que dan como resultado diferentes conjuntos de versiones para distintos contextos o "scopes".
Ámbitos de dependencia
El ámbito o "scope" de cada dependencia afecta a la construcción del proyecto en tres vertientes:
- Las dependencias se resuelven por separado en cada fase pudiendo resultar diferentes classpaths en cada una de ellas.
- Cada tarea individual, cada plugin, puede actuar de diferente forma según el scope.
- Pueden afectar a la transitividad.
Los ámbitos disponibles en Maven son:
compile(por defecto): las dependencias se incluyen en todos los classpath y se propagan a los proyectos dependientes.provided: se utiliza para indicar que la dependencia la provee el entorno de ejecución (o contenedor como JBoss) y, por tanto, no se incluye en las fases de empaquetado. Las dependencias de este tipo no son transitivas.runtime: se utiliza solo en ejecución, por tanto, se incluye en los classpath de ejecución y tests pero no en la compilación.test: solo se utiliza en las fases de compilación y ejecución de los tests.system: en concepto es similar aprovidedpero se debe indicar la ruta donde está el JAR. No se obtendrá de un repositorio y estará siempre disponible. Se utilizan para extras del JDK, JDBC, etc.import: importa las dependencias declaradas de la seccióndependencyManagementde un POM en la misma sección de otro[5].
El siguiente ejemplo, declara la dependencia de JUnit con scope provided:
<project> ... <groupId>com.softwarepills</groupId> <artifactId>sample</artifactId> <version>1.0</version> ... <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.0</version> <type>jar</type> <scope>test</scope> </dependency> ... </dependencies> ... </project>
Además, para terminar de complicar el asunto, los ámbitos de dependencia se transmiten de forma transitiva según la siguiente tabla:
| compile | provided | runtime | test | |
| compile | compile | - | runtime | - |
| provided | provided | provided | provided | - |
| runtime | runtime | - | runtime | - |
| test | test | - | test | - |
En la tabla, la dependencia entre A y B determina la fila, la dependencia entre B y C determina la columna, la intersección determina la dependencia entre A y C.
Como consejo para simplificar el diseño de tu POM, inicialmente, piensa solo en el concepto que subyace en cada sope: compile es una dependencia "normal" sin ninguna connotación especial, provided es una dependencia que aporta el contenedor y, por tanto, no tenemos que distribuirla con nuestro artefacto, etc. Con esto, tienes un buen punto de partida que probablemente será suficiente.
Si es necesario, en una fase posterior, puedes refinarlo analizando las tres vertientes antes mencionadas. Por ejemplo, cuando declaramos que una dependencia es de tipo provided obtenemos que:
- Se incluirá en todos los classpath (como si fuera compile).
- Normalmente no se empaquetará (en un WAR, por ejemplo).
- No será transitiva: si
Adepende deByBdepende deCcomoprovided,Ano depende deCa menos queCse declare como dependencia directa enA[6].
En la próxima entrada trataré el resto de elementos que intervienen en la gestión de dependencias en Maven: la sección dependencyManagement, las exclusiones y la gestión de snapshots.
- Al contrario que Apache Ant, que tiene un enfoque procedural.
- En la última entrada incluiré reseñas bibliográficas y de otros recursos en Internet.
- Todo lo que se refiere en esta serie de artículos está basado en la versión
2.1.0-M1. Como ya he dicho, Maven está en continua evolución y puede que tu versión no se comporte igual en ciertos aspectos. - En la próxima entrada explicaré en detalle la gestión de snaphots.
- Lo explicaré en la segunda entrada de esta serie
- La no transtividad de las dependencias
provided, desde mi punto de vista no tiene mucho sentido. Es algo que se está debatiendo en este momento y que probablemente cambie en próximas versiones de Maven
Entradas (RSS)
Hola ,
En primer lugar , enhorabuena por el blog .
Me gustaría preguntar por un problema que me ha surgido con Maven , en cuanto al tema de las dependencias .
El problema qu tengo es el siguiente : en primer lugar , he creado un proyecto con Maven donde todas las clases las he puesto dentro del directorio /src/main . Este proyecto depende de 2 .jar ( logj4.jar y RXTXComm.jar ) , que previamente se han incluido en el repositorio local de Maven y que en el campo scope del pom.xml los he configurado como ” testing ” .
Después hago un Run package de todo el proyecto y obtengo el .jar , pero el problema que tengo es que el .jar de todo el proyecto que he obtenido contiene solo las clases pero no contiene los .jar de los qu dependen esas clases . Es decir , qu el .jar que he creado no contiene ni logj4.jar ni RXTXComm.jar .
Parece ser que esta opción se debe configurar con el campo Scope del pom.xml. La duda que tengo es cómo tengo que configurar el pom.xml para qu al hacer un run package se genere un .jar que contenga los .jar dependientes ( logj4.jar y RXTXComm.jar ) y que se pueda utilizar en otro proyecto .
Muchas gracias de antemano
Supongo que lo que quieres es crear un JAR con tus propias clases y las de log4j, RXTX, etc.
No sé a que te refieres con “testing”. Si se trata del scope, en todo caso es “test”, y además no es el que necesitas. Tu scope sería “compile”, que es el que tienen todos los artefactos por defecto, así que lo puedes quitar.
Para que Maven te genere un JAR con tus propias clases y las de los artefactos dependientes, debes añadir:
Con esto le dices a Maven que utilice el plugin “assembly” para empaquetar todo en un solo JAR. Además mediante la entrada “execution” haces que se ejecute automáticamente cuando lanzas la fase “package” o posterior.