Parent module y bom en maven

De ChuWiki

En maven tenemos proyectos que pueden ser "parent" de otros, también proyectos que pueden tener "modules" de otros y también lo que maven llama lista de materiales (BOM o Bill Of Materials). Vamos a hacer un montaje sencillo con todo esto, para intentar ver la potencia y posibilidades que tiene. En maven modules and parents tienes el ejemplo de todo esto.

Conceptos básicos[editar]

Imagina que estás haciendo un proyecto gigante, con montones de cosas, que convive junto con otros proyectos gigantes, que tienen otro montón de cosas.

Posiblemente, tengas librerías relativamente complejas que te haces tu mismo para esos proyectos gigantes, porque habrá bastantes cosas que quieras reutilizar en unos proyectos y otros. Por ejemplo, puedes tener una librería tuya, basada en jasper report, que te facilite generar informes pdf en tus proyectos que añada algo más de funcionalidad a jasper report y que te resulte útil. O bien una librería tuya sobre hibernate, que te facilite la vida a la hora de tratar con las bases de datos. O algunos algoritmos matemáticos que uses con frecuencia en tus proyectos.

Aparte de estas librerías, como tus proyectos son gigantes, seguramente no sea un único jar, sino que esté compuesto por varios jar: el del lado de la interfaz de usuario o frontend si es javax.swing, el del lado del servidor o backend, otro con tus estructuras o modelos de datos, otros jar con "plugins" opcionales que pueden ir o no en tu proyecto según el cliente los pague o no, etc.

Para dar soporte a toda esta complejidad, Maven tiene varias posibilidades que son útiles y son las que vamos a ver aqui

  • Parent: Si varios de tus proyectos maven tienen cosas en común (properties, depedencias, plugins, etc), en vez de escribir en cada uno de sus pom.xml todo esto, puedes escribirlo en un proyecto "padre" y luego hacer que los proyectos que necesiten esto sean "hijos" del proyecto padre. El hijo hereda todo esto que hemos comentado del padre, por lo que te ahorras escribirlo en cada proyecto hijo. Por ejemplo, todos esos proyectos van a usar logback como librería de log, JUnit como librería de test, etc. queremos que todos los proyectos sean compatibles java 8, que tengan una property "project.version" común, etc. Pues pones todas esas dependencias una sola vez en el proyecto padre y los hijos las heredarán.
  • Modulos: Si tienes varios proyectos que están muy relacionados y quieres que cuando se compila una de ellos se compilen los demás, o quieres montarlos siempre juntos en tu IDE favorito (Idea, Eclipse...), puedes hacer un proyecto que tenga modulos. Esos módulos serían los proyectos con los que quieres trabajar siempre juntos.
  • Lista de Materiales: Todos esos proyectos seguramente usen librerías comunes de terceros (por ejemplo, junit) y todos esos proyectos te interesa que tengan la misma versión de junit. Si pones el número de versión de junit en todos esos proyectos y luego necesitas cambiarlo, tendrías que recorrer todos los proyectos para buscar ese número y cambiarlo. Una opción es en un proyecto padre poner la dependencia o bien una propiedad con el número de la versión. Pero otra alternativa es usar la lista de materiales, donde se ponen las versiones de todo lo que podamos necesitar y nuestros proyectos miraran la versión en esta lista cuando necesiten una dependencia.

Vamos con detalles y ejemplos

Parent[editar]

Un proyecto parent no es más que un pom.xml que no tiene código. Su packaging en pom y lleva todas las properties, dependencias, plugins, etc que quieras. Los proyectos hijos heredarán lo que se ponga aquí. Aquí tienes un ejemplo de proyecto padre

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         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.chuidiang.examples</groupId>
    <artifactId>chuidiang-parent-example</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>chuidang-parent-example</name>
    <url>http://chuwiki.chuidiang.org</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.chuidiang.examples</groupId>
                <artifactId>chuidiang-bom-example</artifactId>
                <version>1.0-SNAPSHOT</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.16</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Fijate que hemos puesto:

  • Unas properties para indicar la versión de java que queremos, que el juego de caracteres será UTF-8, etc.
  • Con la parte de depedencyManagment no te metas de momento, pero simplemente es una forma de indicar cuales son las versiones de las dependencias que querremos en nuestros proyectos. Entramos más adelante en ello.
  • Unas dependencias concretas, que queremos que todos los proyectos hijos tengan.
  • Un annotationProcessor que queremos que tenga el compilador de java. Aunque no tiene nada que ver con todo esto, comento lo que es el annotationProcessor este. No es más que un procesado adicional que se hace con aquellas clases que llevan determinadas anotaciones. En este ejemplo, usando la librería lombok, en una atributo de una clase podemos poner una anotación @Setter y no necesitamos escribir explícitamente el método setAtributo(). El annotationProcessor de lombok se encargará de generar este método automáticamente al compilar.

¿Cómo hacemos ahora para que los proyectos hijos hereden de aquí? Basta con poner en su pom.xml el tag parent, tal que así

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <parent>
        <groupId>com.chuidiang.examples</groupId>
        <artifactId>chuidiang-parent-example</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../parent</relativePath>
    </parent>
    ...
</project>

Nada especial, salvo un detalle: el relativePath.

Si el pom.xml padre lo tienes ya instalado en tu repositorio local, funciona sin necesidad de poner el relativePath. Maven sabe encontrarlo. Si no lo tienes en tu repositorio maven local, maven lo busca en el directorio padre del proyecto, es decir, en ../pom.xml. Si no está ahí, tampoco lo encontrará. Así que si ese pom.xml forma parte de tu montaje, puedes poner un relativePath para indicar donde puede encontrarlo. Tienes que tener en cuenta que si esto lo subes a un git/subversion o similar para compartir con tus compañeros de desarrollo, el pom.xml padre también debería subirse a ese mismo git, de forma que todos lo tengan en el mismo path relativo.

Modules[editar]

Imagina ahora que tienes varios proyectos/subproyectos que están muy relacionados y quieres montarlos más o menos juntos en tu IDE favorito para trabajar en todos ellos de forma más o menos conjunta y que si haces cambios en uno de ellos, los veas reflejados en los otros proyectos sin necesidad de andar haciendo un maven install.

La solución es construir un pom.xml que tenga varios tag module. En cada tag module debes poner el path relativo de los proyectos/subproyectos que quieres cargar juntos. El pom.xml puede parecerse a esto

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    ...
    <modules>
        <module>../libraries/library1</module>
        <module>subproject1</module>
        <module>subproject2</module>
    </modules>
</project>

Bien, este pom.xml arrejunta tres proyectos, cuyos path relativos son los que ahí se indican. Si montas este proyecto en tu IDE favorito, tendrá tres "módulos" o subproyectos: library1, subproject1 y subproject2.

Un detalle. Habitualmente parent y module se usan juntos. En un directorio se crea un pom.xml que tiene varios modules y en subdirectorios se crean los subproyectos cuyo parent es el de arriba. Esto es correcto, pero no es necesario. Los proyectos pueden tener parent sin que el parent los tenga como module, y al revés, puede haber un proyecto con module sin que necesariamente sea padre de sus module. Tenemos libertad para jugar con ambas cosas.


BOM: Lista de materiales[editar]

Cuando tenemos un proyecto maven grande, compuesto de muchos subproyectos, que tiran a su vez de librerías que igual también hemos hecho nosotros como proyectos maven, nos encontramos con un pequeño problema, que son las versiones de las dependencias. Por ejemplo, si usamos la librería logback, tendremos que poner la dependencia en nuestros pom, con su número de versión, tal que así

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
    <scope>runtime</scope>
</dependency>

Si solo la tenemos en un pom.xml no hay mucho problema, pero si la tenemos en varios, el problema viene si un día queremos cambiar la versión de esta dependencia por una más moderna. Tenemos que recorrer todos los pom.xml donde la usemos para ir a la más moderna.

Si usásemos algo más complicado, por ejemplo, Spring Framework, que posiblemente nos haga poner varias dependencias de distintos jar del framework, el problema es más grave, ya que son más jar, más veces que aparece la dependencia, etc.

Una opción con lo que ya sabemos en poner en un proyecto padre una property con la versión de lombok y luego, en los subproyectos, usar esa property. Pero tenemos otra opción: La lista de materiales

Una lista de materiales no es más que un fichero pom.xml, con packaging "pom", en el que se pone una dependencyManagement y ahí dentro, todas las dependencias con sus versiones. Sería algo como esto

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         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.chuidiang.examples</groupId>
    <artifactId>chuidiang-bom-example</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>chuidiang-bom-example</name>
    <url>http://chuwiki.chuidiang.org</url>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.7.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.16</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.30</version>
            </dependency>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>1.2.3</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

Ahí he puesto librerías que uso habitualmente en mis proyectos: slf4j, junit, lombok y logback-classic. El poner estas dependencias aquí no implica que los proyectos las vayan a importar todas, las necesiten o no. El proyecto tendrá que seguir poniendo la dependencia que necesite, pero se ahorra poner la versión. Si tenemos muchos proyectos/subproyectos/librerías que usen slf4j, si "importan" este pom de alguna forma (veremos ahora como hacerlo), no necesitan poner el número de versión y si queremos cambiar dicho número para todos ellos, bastaría con cambiarlo una sola vez en este pom.

¿Y cómo lo importamos?

Hay dos opciones, una es ponerlo como parent de nuestros proyectos/subproyectos. Eso está bien, pero no siempre es adecuado. Como hemos visto en el apartado anterior, un parent es una forma cómoda de que muchos proyectos hereden cosas comunes (properties, dependencias, plugins, etc) y el problema es que un proyecto solo admite un parent. Por ello, en esta lista de materiales deberíamos meter además todas aquellas cosas comunes que necesiten todos los proyectos, que no siempre tienen por qué ser lo mismo.

La otra opción, es hacer un "import" en los proyectos. En el pom.xml de nuestro proyecto sería algo de este estilo


<project>
...
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.chuidiang.examples</groupId>
                <artifactId>chuidiang-bom-example</artifactId>
                <version>1.0-SNAPSHOT</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
...
</project>

Es decir, ponemos un dependencyManagment con scope import que importa nuestra lista de materiales. A partir de aquí, en este proyecto, no necesitamos poner el número de versión en las dependencias que estén contempladas en la lista de materiales.

Y para no tener que poner este dependencyManagement en todos nuestros subproyectos dentro de un proyecto grande, podemos ponerlo solo en el parent del proyecto grande, así todos los subproyectos heredan esto. Si te fijas en el apartado que hablábamos de parent, metimos este trozo.

Para una correcta organización de esto, no deberíamos hacer un bom tocho con todo mezclado, lo suyo sería hacer uno por cada grupo de jars que estén relacionados. Por ejemplo, spring framework está compuesto por un montón de jars propios de spring framework (spring-core, spring-test, spring-beans, etc) por lo que estaría bien tener un bom con estos jar. Idem para junit, para log4j, etc. De hecho, muchos de estos frameworks tienen colgado en internet dicho bom, por lo que no necesitaríamos hacerlo a mano. Por ejemplo, en el repositorio de maven central tienes el spring-framework-bom. Si lo importas en tu proyecto, no necesitas poner las versiones en las dependencias de spring-framework que tengas.

Un ejemplo tocho[editar]

¿Qué hemos montado en modules and parents?. Bueno, pues algo quizás enrevesado, pero que intenta ver las posibilidades de esto.

En el directorio bom tienen un pom.xml con depedencyManagement. Ahí está las versiones de lo que queremos usar.

En el directoiro parent, un pom.xml padre que importa el bom anterior y que añade las dependencias comunes que siempre queremos usar en todos lados, junto con algunas properties.

En el directorio libreries hay dos librerías (library1 y librery2) que apenas tienen código, pero que pretenden ser librerías nuestras que usamos con frecuencia en nuestros proyectos y que están relacionadas. Cada librería tiene su propio pom.xml. Como queremos que usen el bom y que usen las cosas comunes que hemos definido en el parent y además queremos que al montarlas en el IDE se monten juntas, en el directorio libraries hemos creado un pom.xml con dos modules que son library1 y library2. A este pom.xml le hemos puesto como padre el parent del directorio parent. Y a las library1 y library2 le hemos puesto como parent el del directorio libraries. El siguiente diagrama muestra el montaje para que quede más claro.

Si desde tu IDE montas libreries/pom.xml, tendrás un proyecto con los dos modulos library1 y library2.

En el directorio project, algo parecido, pero relativo a un proyecto concreto con cliente y no a librerías. Esta vez ese proyecto tiene dos subproject1 y subproject2 que es código específico para ese proyecto y sólo necesitamos usar library1. Así que project/pom.xml es similar a libreries/pom.xml, pero sus module son subproject1, subproject2 y el path relativo hacia library1. Montando esto en el IDE, tendrás un proyecto con tres módulos.

Y esta es precisamente la versatilidad de todo esto. Has conseguido montar library1 en tu IDE en dos proyectos distintos, uno para hacer librerías y otro para un cliente.

De la misma forma, si con el project/pom.xml hicieras un instalador o un zip, solo incluiría library1 y no todas tus librerías (salvo que pongas dependencias de ellas).