Curso de Maven: Descubre la Mejor Manera de Construir Proyectos

Tras los interesantísimos artículos sobre WordPress y Jenkins, llega el momento de conocer en detalle una de las herramientas de construcción de proyectos más extendidas de la actualidad: Maven. En particular, veremos qué es Maven, las ventajas de utilizarlo, algunos conceptos básicos (y necesarios), el manejo de dependencias, los repositorios, los plugins y, por último, un ejemplo de funcionamiento.

¿Qué es Maven?

Maven es una herramienta principalmente utilizada en el desarrollo de software Java, que aparece ante la necesidad de modelar el concepto de «proyecto» y «artefacto» de forma estándar (y todo ello independendientemente del IDE de desarrollo). Es muy similar en funcionalidad a Apache Ant, aunque tiene un modelo de configuración de construcción más simple, basado en un formato XML. Además, provee un conjunto de estándares de construcción, un modelo de repositorio de artefactos y un motor de software que administra y describe los proyectos.

Popular Java Build Tools

Con esta herramienta se simplifica enormemente la realización de tareas como borrar los «.class», compilar, generar la documentación de javadoc, el «.jar», generar documentación web con montones de informes (métricas, código duplicado, etc.), e incluso más. Con comandos simples, crea una estructura de directorios para el proyecto con sitio para los fuentes, los iconos, ficheros de configuración y datos, etc. Por otro lado, si le indicamos qué jars externos necesitamos, es capaz de ir a buscarlos a internet y descargarlos por nosotros. Además, se encarga de pasar automáticamente nuestros test de prueba cuando compilamos, e incluso nos genera un zip de distribución en el que van todos los jar necesarios y ficheros de configuración de nuestro proyecto.

Maven

En líneas generales, podemos decir que se utiliza como herramienta de SCM (Software Configuration Management) para:

  • Controlar el versionado de nuestros proyectos.
  • Manejar las dependencias con proyectos nuestros y librerías de tercero.
  • Automatizar las tareas relativas al ambiente de desarrollo y construcción del artefacto (compilación, generación de código, empaquetado, etc.).

Maven utiliza un Project Object Model (POM) para describir el proyecto de software a construir, sus dependencias de otros módulos y componentes externos, y el orden de construcción de los elementos. Además, viene con objetivos predefinidos para realizar ciertas tareas claramente definidas (como la compilación del código y su empaquetado).

Maven

Por tanto, todo proyecto incluye un archivo XML llamado «pom.xml» (de Project Object Model), siendo éste el archivo que tenemos que subir al sistema de control de versiones que tengamos. En este archivo se declara un identificador único de nuestro proyecto/artefacto, que resulta de la unión de tres identificadores:

  • groupId: representa la organización autora/dueña del artefacto («com.ibm», «org.apache», «org.jboss», etc.).
  • artifactId: nombre del proyecto/artefacto actual («http-client», «hibernate», «tomcat», «commons-collections», etc.).
  • version: número de versión del artefacto.

Por otro lado, como veremos más adelante, Maven está construido usando una arquitectura basada en plugins que permite que utilice cualquier aplicación controlable a través de la entrada estándar. En teoría, esto podría permitir a cualquiera escribir plugins para su interfaz con herramientas como compiladores, herramientas de pruebas unitarias, etc., para cualquier otro lenguaje.

¿Por qué utilizarlo?

El motivo principal del uso de Maven es que hace la vida más fácil a los desarrolladores. Cuando se tiene un proyecto Java, normalmente hay un montón de tareas que poco tienen que ver con la aplicación que se está creando, pero sí con el entorno de trabajo. Siempre es necesario crear la estructura de directorios («src», «resources», «lib», etc.), configurar los accesos a bases de datos y a otros repositorios, comprobar que existe o configurar un directorio para depositar los ficheros compilados («.class»), los «.jar», etc.

Maven lifecycle

Tradicionalmente se ha utilizado Ant para ayudar con estas tareas con la inestimable colaboración del fichero «build.xml». Pues bien, Maven es una herramienta más evolucionada capaz de generar todas las estructuras de directorios para el proyecto. Además, podrá descargar las librerías que hagan falta. Además, se puede integrar en la mayoría de IDEs, pero si no quieres depender de otra herramienta, también puedes ejecutarlo de forma nativa en tu ordenador, descargándote el «.zip» correspondiente y ejecutándolo por línea de comandos.

Maven

Otra característica fundamental de Maven son los arquetipos. Un arquetipo es como una plantilla de proyecto, es decir, un patrón que define el tipo de proyecto. Igual que utilizamos patrones de diseño para solucionar problemas comunes en la programación, podemos utilizar patrones para el esqueleto de nuestro proyecto. Así, tenemos la posibilidad de elegir un patrón determinado, en función de nuestras necesidades, y a partir de ahí empezar a trabajar casi directamente, sin preocupaciones sobre qué librerías usar, qué versiones son compatibles y cómo crear la estructura del proyecto. Todo esto aporta consistencia a proyectos distintos, permite reutilizar o componer distintos arquetipos para hacer otro más grande si fuera necesario, y ayuda con los procesos de estandarización dentro de una misma empresa.

Maven

En definitiva, la principal ventaja de Maven es que se ahorra tiempo de configurar e implementar el entorno de desarrollo para poder centrarnos en lo realmente interesante de un proyecto Java: desarrollar y documentar el código.

Conceptos básicos

Librerías y artefactos

Para entender el funcionamiento de Maven, es fundamental conocer el concepto de «librería». No obstante, este concepto a veces es limitado. Por ejemplo, podemos querer utilizar la librería A en nuestro proyecto. Sin embargo, no nos valdrá con simplemente querer utilizar la librería, sino que además necesitaremos saber que versión exacta de ella necesitamos.

Librerías y versiones

¿Es esto suficiente? Lamentablemente no lo es, ya que una librería puede depender de otras librerías para funcionar de forma correcta. Así pues, necesitamos más información para gestionarlo todo de forma correcta.

Relaciones entre librerías

Maven solventa este problema a través del concepto de «artefacto». Un artefacto contiene las clases propias de la librería, pero además incluye toda la información necesaria para su correcta gestión (grupo, versión, dependencias, etc.).

Artefacto

El archivo «pom.xml»

Para definir un artefacto necesitamos crear un fichero «pom.xml» (Proyect Object Model), que es el encargado de almacenar toda la información que hemos comentado anteriormente. Aquí podemos ver un ejemplo:

<project xmlns=»http://maven.apache.org/POM/4.0.0″ xmlns:xsi=»http://www.w3.org/2001/XMLSchema-instance»
xsi:schemaLocation=»http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd»><modelVersion>4.0.0</modelVersion>
<groupId>com.genbetadev.proyecto1</groupId>
<artifactId>proyecto1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
</project>

La estructura de este fichero puede llegar a ser muy compleja y puede llegar a depender de otros POM. En este ejemplo, estamos viendo el fichero más sencillo posible, en el que se define el nombre del artefacto (artifactID), el tipo de empaquetado (jar) y también las dependencias que tiene (log4j). De esta manera, nuestra librería queda definida de una forma mucho más clara.

Una vez definidos correctamente todos los artefactos que necesitamos, Maven nos provee de un repositorio (ver siguiente sección) donde alojar, mantener y distribuir estos, permitiéndonos una gestión correcta de nuestra librerías, proyectos y dependencias.

Repositorios MavenOtro enfoque

¿Aún no te queda claro? Veámoslo todo según otro enfoque. Consideremos la siguiente figura (donde tenemos nuestro código fuente y generamos tres ficheros JAR):

Código fuente y 3 JARs

En primer lugar, será necesario definir un nombre único de nuestra librería a nivel mundial. Para ello, nos apoyaremos en las estructuras de paquetes java. Se recomienda el uso de la URL de nuestra página web a la hora de definir la jerarquía de paquetes. Por ejemplo, si mi URL es «webipedia.es», definiremos la estructura de paquetes como «es.webipedia». A partir de esa estructura, podemos definir las subestructuras que deseemos, por ejemplo siguiendo el patrón «es.webipedia.blog.componente». En este caso, la estructura de paquetes quedaría definida de la siguiente forma:

Estructura de carpetas basada en URL de página web

Una vez realizado este primer paso, deberemos también asignar una versión a nuestra librería. En este caso, para no complicar demasiado las cosas, vamos a suponer que la versión es la «1.0».

Nombre único y versión

Ahora bien. Nuestro ciclo de desarrollo de software no está limitado únicamente a la generación de librerías, ya que a partir de nuestro código fuente podemos querer generar un desplegable que podría ser un JAR (esto sí que sería una librería) pero que también podría ser un WAR o un EAR. Así pues, será necesario añadir también qué tipo de módulo JEE es nuestra librería, con lo que tendríamos:

Nombre único, versión y tipo de módulo JEE

Por tanto, si tuviéramos varios componentes dentro de nuestro blog, la estructura quedaría más clara de la siguiente forma:

Componentes

Como vemos, es evidente que un WAR o un EAR no encajan con el concepto de librería «standard». Así pues, será necesario definir un concepto distinto en el cual estén incluidos el nombre, la versión y el tipo del sotfware que estamos desarrollando. Este concepto es lo que en Maven se denomina «artefacto».

Por otro lado, para poder trabajar con el artefacto en cuestión, deberemos almacenar esta información en algún fichero. Es aquí donde entra en juego el archivo «pom.xml» (Project Object Model), el cual constituye el fichero fundamental de configuración y el encargado de almacenar la información anteriormente expuesta. Aquí podemos ver un ejemplo:

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.arquitecturajava.blog</groupId>
<artifactId>componenteA</artifactId>
<packaging>jar</packaging>
<version>1.0/version>
</project

En este caso, vemos una etiqueta «modelVersion», que hace referencia a la propia estructura del fichero Maven (actualmente es la 4).

Manejo de Dependencias

Además de para identificar a nuestro proyecto unívocamente, el archivo «pom.xml» se utiliza para declarar diferentes aspectos y metadata de nuestros proyectos. Un aspecto importante es que podemos declarar las dependencias respecto de otros artefactos. Consideremos el siguiente ejemplo:

<project xmlns=»http://maven.apache.org/POM/4.0.0″
xmlns:xsi=»http://www.w3.org/2001/XMLSchema-instance»
xsi:schemaLocation=»http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd»>…<dependencies>
<dependency>
<groupId>com.uqbar</groupId>
<artifactId>uqbar-class-descriptor</artifactId>
<version>1.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.uqbar</groupId>
<artifactId>uqbar-bttf</artifactId>
<version>2.2-SNAPSHOT</version>
</dependency>
</dependencies>…</project>

Aquí, estamos declarando dos dependencias a artefactos, determinando de esta manera qué pre-requisitos necesitamos para trabajar en este proyecto (para compilar). En cuanto a la siguiente imagen, vemos cómo el propio artefacto (en este caso, el API de Java 8) ya incluye el detalle de cómo incluir la dependencia en un proyecto Maven para traernoslo, ya que Maven se baja los jars de las dependencias automáticamente de internet.

Dependencia Maven

Ejecutando el siguiente goal de un plugin de Maven, podemos ver el árbol de dependencias de nuestro proyecto:

mvn dependency:tree

A continuación podemos ver un ejemplo del resultado de esta ejecución:

[INFO] edu.unsam.algo3:TestWebapp:war:1.0-SNAPSHOT
[INFO] +- junit:junit:jar:3.8.1:test
[INFO] +- org.glassfish.web:jstl-impl:jar:1.2:compile
[INFO] | +- javax.servlet:servlet-api:jar:2.5:compile
[INFO] | +- javax.servlet.jsp:jsp-api:jar:2.1:compile
[INFO] | \- javax.servlet.jsp.jstl:jstl-api:jar:1.2:compile
[INFO] +- com.thoughtworks.xstream:xstream:jar:1.2.2:compile
[INFO] | \- xpp3:xpp3_min:jar:1.1.3.4.O:compile
[INFO] \- commons-beanutils:commons-beanutils:jar:1.7.0:compile
[INFO] \- commons-logging:commons-logging:jar:1.0.3:compile

Por otro lado, un detalle no menor de la resolución de dependencias, es que también funciona para las dependencias transitivas. Esto quiere decir que si nuestro proyecto A depende del proyecto B, Maven automáticamente va a bajar el jar de proyectoB, pero además, todas las dependencias que éste tenga.

Repositorios

Como sabemos, tanto nuestros nuevos proyectos como todos los de la comunidad java+maven van a tener la misma estructura e identificación, basada en la terna única «groupId + artifactId + version». Por ejemplo:

<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5.6.SEC02</version>

Si existiera alguien que guardara esas librerías y nos evitara el tener que bajarnos los jars «a mano» o versionarlos (subir al subversion o cualquier otra herramienta para versionado de código), se simplificaría bastante todo el proceso. Pues bien, ese «alguien» existe, y son los repositorios Maven. De hecho, existe un repositorio público llamado «ibiblio» o «repo1» que contiene muchísimos proyectos open-source.

Arquitectura de Repositorios Maven

La forma de utilizar estos repositorios es a través de la «herramienta Maven», que es algo así como el cliente de svn. Se trata de es una herramienta de línea de comandos, que podemos lanzar con la orden «mvn».

Ejemplo de ejecución de orden "mvn"

Por tanto, un repositorio es básicamente un lugar donde están los artefactos Maven, estructurados en cierta forma estándar que permitirá al propio Maven hacer el download específico de las dependencias. Existen tres tipos de repositorios:

  • Repositorio Local: cada desarrollador, al instalarse y usar Maven (el comando) va a tener localmente, en su máquina, un repositorio. La herramienta Maven, al resolver dependencias, buscándolas y bajándolas de internet, las va a ir cacheando (es decir, se las va a guardar para recordarlas y no tener que salir a buscarlas todo el tiempo). El repositorio por defecto se encuentra en la carpeta «$HOME/.m2/repository», pero esto se puede modificar en «$MAVEN/conf/settings.xml».
  • Repositorio Público de Internet: es el repositorio de Maven por defecto, donde la herramienta busca los artefactos. Es un servidor donde se encuentran los proyectos open source más utilizados en la comunidad java. Uno podría publicar sus artefactos allí, aunque para eso tiene que cumplir con ciertos requerimientos de licencia (software libre y open source), y además seguir un proceso para que los artefactos sean aprobados.
  • Repositorio «Empresarial»: la empresa u organización tiene un repositorio Maven propio, ya sea para publicar sus propios artefactos solo dentro de la organización (sin hacerlo público a toda internet), o bien para evitar que cada Maven de cada desarrollador acceda a internet para bajarse los artefactos. Básicamente, se instala una aplicación en un servidor, y luego se configura Maven en cada máquina de los desarrolladores para que utilicen este repositorio.

Repositorios Maven

Es importante entender esta arquitectura física, porque los repositorios son el medio que tenemos para publicar nuestros artefactos, ya sea para hacer un entregable (release) como también para hacer públicos nuestros cambios durante el desarrollo para otros desarrolladores.

Plugins

Maven está diseñado y compuesto de pequeñas partecitas o módulos llamados plug-ins, tal que la arquitectura no es rígida sino extensible. De hecho, existe un API java pública, ya que Maven está hecho en java, con la cual nosotros podemos hacer nuestros propios plugins de Maven (llamados MOJOs). De hecho, Maven se bajará los plugins automáticamente desde internet de la misma forma en que baja las dependencias por nosotros, lo cual nos va a evitar muchísimo trabajo manual.

Plugins Maven

Los plugins proveen funcionalidades en forma de comandos que se pueden invocar, llamados goals. Aquí podemos ver un ejemplo:

mvn dependency:tree

En este caso, estamos ejecutando el goal «tree» del plugin llamado «dependency». El efecto de esto es que, como veíamos también en la sección de dependencias, se mostrará el arbol de dependencias de artefactos en la consola.

[INFO] [dependency:tree]
[INFO] org.apache.maven.plugins:maven-dependency-plugin:maven-plugin:2.0-alpha-5-SNAPSHOT
[INFO] +- org.apache.maven.reporting:maven-reporting-impl:jar:2.0.4:compile
[INFO] | \- commons-validator:commons-validator:jar:1.2.0:compile
[INFO] | \- commons-digester:commons-digester:jar:1.6:compile
[INFO] \- org.apache.maven.doxia:doxia-site-renderer:jar:1.0-alpha-8:compile
[INFO] \- org.codehaus.plexus:plexus-velocity:jar:1.1.3:compile
[INFO] \- commons-collections:commons-collections:jar:2.0:compile

Por otro lado, además de poder ejecutar un goal explícitamente (es decir, pedirle a Maven que ejecute un comando), se puede indicar que cierto goal debe ejecutarse en cierta fase del build.

Ejemplo de Plugin Maven definido en pom.xml

Comentar también que el motor incluido en el núcleo de Maven puede dinámicamente descargar plugins de un repositorio. Además, provee soporte no solo para obtener archivos de su repositorio, sino también para subir artefactos al repositorio al final de la construcción de la aplicación, dejándola accesible para todos los usuarios. Una caché local de artefactos actúa como la primera fuente para sincronizar la salida de los proyectos a un sistema local.

Ciclo de Vida

Maven no sólo se utiliza para bajar jars de internet, sino para todo el ciclo de vida del proyecto. Lo podemos usar para compilar, para generar código (de ser necesario), para correr los tests, para empaquetar, y hasta para publicar nuestros artefactos generando releases con trazabilidad.

Ciclo de vida por defecto de Maven

Así pues, se definen un conjunto de etapas en la construcción (build) de nuestro proyecto. Aquí podemos ver un resumen:

  • generate-sources: genera el código, previo a la compilación.
  • compile: compila el código fuente (genera los ficheros «.class» compilando los fuentes «.java»).
  • test: ejecuta los tests automáticos de JUnit existentes, abortando el proceso si alguno de ellos falla.
  • package: enerar un paquete con el código (por ejemplo, el fichero «.jar» con los «.class» compilados).
  • install: hace público el paquete en nuestro repositorio local (copia el fichero «.jar» a un directorio de nuestro ordenador donde Maven deja todos los «.jar», de manera que puedan utilizarse en otros proyectos Maven en el mismo ordenador). Hace un copy al repo local «$HOME/.m2/repository/com/blah/artifactBlah/1.0.0/artifactBlah-1.0.0.jar».
  • deploy: publica el artefacto en un repositorio remoto (copia el fichero «.jar» a un servidor remoto, poniéndolo disponible para cualquier proyecto Maven con acceso a ese servidor).

Cuando se ejecuta cualquiera de los comandos, por ejemplo, si ejecutamos «mvn install», Maven irá verificando todas las fases del ciclo de vida desde la primera hasta la del comando, ejecutando solo aquellas que no se hayan ejecutado previamente. Por ejemplo, con «mvn compile» va a ejecutar las dos primeras fases. Por su parte, con «mvn test» se ejecutarán las tres primeras (sería igual a ejecutar «mvn compile» + correr los tests).

Ejemplo de ejecución de "mvn test"

También existen algunas metas (goals), que están fuera del ciclo de vida, pero que pueden ser llamadas. Estas metas pueden ser añadidas al ciclo de vida a través del Project Object Model (POM), y son, entre otras, las siguientes:

  • clean: elimina todos los «.class» y «.jar» generados. Después de este comando, se puede comenzar un compilado desde cero.
  • assembly: genera un fichero «.zip» con todo lo necesario para instalar nuestro programa java. Se debe configurar previamente en un fichero xml qué se debe incluir en ese zip.
  • site: genera un sitio web con la información de nuestro proyecto. Dicha información debe escribirse en el fichero «pom.xml» y ficheros «.apt» separados.
  • site-deploy: sube el sitio web al servidor que hayamos configurado.

Ejemplo

Consideremos el siguiente archivo «pom.xml»:

<project xmlns=»http://maven.apache.org/POM/4.0.0″
xmlns:xsi=»http://www.w3.org/2001/XMLSchema-instance»
xsi:schemaLocation=»http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd»><modelVersion>4.0.0</modelVersion><groupId>com.uqbar</groupId>
<artifactId>uqbar-commons</artifactId>
<version>1.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Uqbar Commons Project</name>
</project>

Podemos ejecutar un goal básico de Maven en la misma carpeta del proyecto, ejecutando «mvn clean compile». Esta ejecución va a producir en la consola mensajes parecidos a estos:

[INFO] Scanning for projects…
[INFO]
[INFO] ————————————————————————
[INFO] Building TestWebapp Maven Webapp 1.0-SNAPSHOT
[INFO] ————————————————————————
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-clean-plugin/2.4.1/maven-clean-plugin-2.4.1.pom
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-clean-plugin/2.4.1/maven-clean-plugin-2.4.1.pom (5 KB at 2.5 KB/sec)
[INFO]
[INFO] — maven-clean-plugin:2.4.1:clean (default-clean) @ TestWebapp —
Downloading: http://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/2.0.5/plexus-utils-2.0.5.pom
Downloaded: http://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/2.0.5/plexus-utils-2.0.5.pom (4 KB at 5.3 KB/sec)
[INFO]
[INFO] — maven-resources-plugin:2.5:resources (default-resources) @ TestWebapp —
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/maven-profile/2.0.6/maven-profile-2.0.6.pom
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/maven-profile/2.0.6/maven-profile-2.0.6.pom (2 KB at 3.2 KB/sec)
[debug] execute contextualize
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 0 resource
[INFO]
[INFO] — maven-compiler-plugin:2.3.2:compile (default-compile) @ TestWebapp —
Downloading: http://repo.maven.apache.org/maven2/org/apache/maven/maven-toolchain/1.0/maven-toolchain-1.0.pom
Downloaded: http://repo.maven.apache.org/maven2/org/apache/maven/maven-toolchain/1.0/maven-toolchain-1.0.pom (4 KB at 5.6 KB/sec)
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 23 source files to /media/07995b1d-8f74-4fa0-9e9b-1c889f7fe5d9/development/data/repos/utn-tadp-projects/ajedrez/trunk/target/classes
[INFO] ————————————————————————
[INFO] BUILD SUCCESS
[INFO] ————————————————————————
[INFO] Total time: 48.519s
[INFO] Finished at: Wed Sep 05 22:46:10 ART 2012
[INFO] Final Memory: 6M/15M
[INFO] ————————————————————————

Para empezar, hemos de entender los mensajes que aparecen al principio y al final, con esos separadores con caracteres ASCII («———«). Así, al principio Maven nos dirá qué va a hacer y sobre qué proyecto (el pom.xml del directorio actual), y al final nos va a indicar el resultado («SUCCESS» o «FAILED»). En nuestro ejemplo, Maven ejecutó dos tareas (o, como se llaman en Maven, goals). Estas tareas se las habíamos indicado previamente como parámetros:

  1. clean: borra cualquier archivo generado previamente, como por ejemplo los «.class» resultado de la compilación. En la práctica lo que hace es borrar la carpeta «target» de nuestro proyecto, que es a donde va a parar todo el contenido generado.
  2. compile: compila al directorio target.

En el medio, vemos un montón de mensajitos que arrancan con «Downloading: …» y «Downloaded: …». Lo que hizo Maven ahí fue leer nuestro «pom.xml» y averiguar así de qué otros proyectos/librerías/artefactos depende. Por cada uno de estos:

  1. Fue a buscar a un repositorio en internet el «pom.xml» de cada una de esas dependencias (por eso se ven algunos «Downloading: http://url.pom»).
  2. Fue a buscar el archivo jar de esta dependencia también a internet.
  3. Leyó el pom y así pudo averiguar a su vez de qué otros proyectos depende éste, y con cada uno de estos volvió a hacer lo mismo.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *


error: