Ejemplo de multiproyecto con Gradle

Escrito por el , actualizado el .
blog-stack java planeta-codigo programacion tapestry
Comentarios

En un proyecto grande podemos tener necesidad de dividir el monolito al menos en varios módulos y yendo un paso más lejos en microservicios. La herramienta de construcción que usemos deberá de facilitarnos automatizar la tarea de construcción del código fuente de cada módulo individual, de todos a la vez o de los microservicios si tienen alguna dependencia compartida. En el artículo y ejemplo explicaré cómo usando Gradle podemos dividir el proyecto en varios módulos. No deja de ser un ejemplo pero es bastante completo y está formado por dos aplicaciones web, una librería de componentes y otra librería con el modelo de persistencia, usa Spring, jOOQ, Tapestry, PostgreSQL, Docker, Liquibase, …

Java
Gradle

Cuando una aplicación o proyecto crece en algún momento podemos tener necesidad de partir el monolito en varios módulos más pequeños y más manejables. Las arquitecturas basadas en microservicios proponen en vez de tener una aplicación grande que contenga toda la funcionalidad dividirla en varios servicios manejables, pequeños y lo más independientes posible. En el libro Building Microservices explican muy bien la idea y conceptos de los microservicios. La división de una aplicación implica tener un repositorio de código fuente para cada proyecto, probablemente algunos proyectos dependan de otros y haya alguno que sea utilizado por varios como uno de utilidades. Aunque diría que en los microservicios se prefiere en cierta medida duplicar código que compartir para que cada proyecto tenga un ciclo de vida independiente, esto permite desplegarlos individualmente, aún así podemos aceptar compartir cierto código de utilidades o componentes, necesitando que unos proyectos dependan de otros.

Tener varios proyectos con dependencias entre ellos exige de la herramienta de construcción que esto sea posible y sencillo. En este artículo comentaré como crear un proyecto compuesto de varios componentes con la herramienta de construcción Gradle. El ejemplo consistirá en dos aplicaciones web basadas en el framework web Apache Tapestry, una será la que vea el público (web) y otra de administración (back), estas dos aplicaciones compartirán una librería de componentes de Tapestry comunes como un layout (library), la cabecera y pie de las páginas de cada proyecto web, finalmente existirá una librería con métodos o servicios de utilidad comunes y el modelo de datos a persistir en una base de datos relacional usada tanto en las aplicaciones web como por la librería de componentes (core).

Necesitaremos 4 proyectos para los módulos y 5 proyectos Gradle, uno para cada módulo y otro que los englobe a todos. La estructura de directorios y archivos relativos a Gradle será la siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
MultiprojectGradle
- setttings.gradle (proyectos incluidos)
- build.gradle (cosas comunes a todos los proyectos)
- web (aplicación web pública)
  - build.gradle
- back (aplicación web privada)
  - build.gradle
- components (componentes comunes de Apache Tapestry: layout, header y footer)
  - build.gradle
- core (modelos, servicios y utilidades comunes)
  - build.gradle

En el archivo build.gradle global podemos incluir las cosas comunes a todos los proyectos como dependencias o plugins, en el archivo settings.gradle definimos de que componentes está formado el proyecto. Las cosas comunes a todos los proyectos será el uso del plugin java y eclipse, el repositorio de dependencias de mavenCentral, algunas dependencias y una tarea para instalar el wrapper de Gradle que nos servirá para usarlo sin necesidad de instalar nada (se descargarán sus binarios automáticamente).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
subprojects {
    apply plugin: 'eclipse'
    apply plugin: 'java'

    repositories {
        mavenCentral()

        // For access to Apache Staging (Preview) packages
        maven {
            name 'Apache Staging'
            url 'https://repository.apache.org/content/groups/staging'
        }
    }

    dependencies {
        compile 'org.slf4j:slf4j-api:1.7.12'
        compile 'org.apache.logging.log4j:log4j-slf4j-impl:2.3'
        compile 'org.apache.logging.log4j:log4j-api:2.3'
        compile 'org.apache.logging.log4j:log4j-core:2.3'

        testCompile 'junit:junit:4.12'
    }

    task wrapper(type: Wrapper) {
        gradleVersion = '2.7'
    }
}
1
include 'web', 'back', 'library', 'core'

Podemos ver los módulos (o proyectos como los llama Gradle) de los que se compone la aplicación y las tareas que podemos ejecutar con:

1
2
3
$ ./gradlew -daemon projects
$ ./gradlew -daemon tasks
$ ./gradlew -daemon eclipse
Comandos básicos de Gradle

En los proyectos web incluiremos como dependencias las propias de Apache Tapestry, el proyecto library y core, también aplicaremos el plugin de Tomcat para poder iniciar los proyectos con Gradle configurándolos para que cada uno se inicie en un puerto distinto 80808443 para web y 90809443 para back.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
apply plugin: 'war'
apply plugin: 'com.bmuschko.tomcat'

buildscript {
    repositories {
        mavenCentral()
        jcenter()        
    }

    dependencies {
        classpath 'com.bmuschko:gradle-tomcat-plugin:2.2.2'
    }
}

repositories {
    mavenCentral()

    // All things JBoss/Hibernate
    maven {
        name 'JBoss'
        url 'http://repository.jboss.org/nexus/content/groups/public/'
    }

    // For access to Apache Staging (Preview) packages
    maven {
        name 'Apache Staging'
        url 'https://repository.apache.org/content/groups/staging'
    }
}

dependencies {
    compile project(":core")
    compile project(":library")

    // Tapestry
    compile 'org.apache.tapestry:tapestry-core:5.4-beta-36'
    compile 'org.apache.tapestry:tapestry-webresources:5.4-beta-36'
    compile 'org.apache.tapestry:tapestry-javadoc:5.4-beta-36'
    compile 'org.apache.tapestry:tapestry-beanvalidator:5.4-beta-36'
    
    // Spring
    compile ('org.apache.tapestry:tapestry-spring:5.4-beta-36') { exclude(group: 'org.springframework') }
    compile 'org.springframework:spring-jdbc:4.2.1.RELEASE'
    compile 'org.springframework:spring-web:4.2.1.RELEASE'
    compile 'org.springframework:spring-tx:4.2.1.RELEASE'
    
    compile 'commons-dbcp:commons-dbcp:1.4'
    
    // Tomcat embedded
    tomcat 'org.apache.tomcat.embed:tomcat-embed-core:8.0.26'
    tomcat 'org.apache.tomcat.embed:tomcat-embed-logging-juli:8.0.26'
    tomcat 'org.apache.tomcat.embed:tomcat-embed-jasper:8.0.26'
}

tomcat {
    httpPort = 8080
    httpsPort = 8443
    ajpPort = 8009
    stopPort = 8081
    enableSSL = true
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
apply plugin: 'war'
apply plugin: 'com.bmuschko.tomcat'

buildscript {
    repositories {
        mavenCentral()
        jcenter()        
    }

    dependencies {
        classpath 'com.bmuschko:gradle-tomcat-plugin:2.2.2'
    }
}

dependencies {
    compile project(":core")
    compile project(":library")

    // Tapestry
    compile 'org.apache.tapestry:tapestry-core:5.4-beta-36'
    compile 'org.apache.tapestry:tapestry-webresources:5.4-beta-36'
    compile 'org.apache.tapestry:tapestry-javadoc:5.4-beta-36'
    compile 'org.apache.tapestry:tapestry-beanvalidator:5.4-beta-36'
    
    // Spring
    compile ('org.apache.tapestry:tapestry-spring:5.4-beta-36') { exclude(group: 'org.springframework') }
    compile 'org.springframework:spring-jdbc:4.2.1.RELEASE'
    compile 'org.springframework:spring-web:4.2.1.RELEASE'
    compile 'org.springframework:spring-tx:4.2.1.RELEASE'
    
    compile 'commons-dbcp:commons-dbcp:1.4'
    
    // Tomcat embedded
    tomcat 'org.apache.tomcat.embed:tomcat-embed-core:8.0.26'
    tomcat 'org.apache.tomcat.embed:tomcat-embed-logging-juli:8.0.26'
    tomcat 'org.apache.tomcat.embed:tomcat-embed-jasper:8.0.26'
}

tomcat {
    httpPort = 9080
    httpsPort = 9443
    ajpPort = 9009
    stopPort = 9081
    enableSSL = true
}

En el proyecto library incluiremos los componentes que podemos reutilizar en cualquiera de los proyectos, será una librería de componentes de Apache Tapestry. Esta librería de componentes no es más que un archivo jar, cada proyecto que necesite utilizarlos basta con que lo incluya como una dependencia.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
dependencies {
    compile project(":core")

    // Tapestry
    compile 'org.apache.tapestry:tapestry-core:5.4-beta-36'
    compile 'org.apache.tapestry:tapestry-webresources:5.4-beta-36'
    compile 'org.apache.tapestry:tapestry-javadoc:5.4-beta-36'
}

jar {
    manifest {
        attributes("Tapestry-Module-Classes": "io.github.picodotdev.gradle.library.services.LibraryModule")
    }
}

En el último proyecto core incluiremos una clase de utilidad con los típicos métodos estáticos, incluiremos un servicio que nos facilitará la persistencia y una clase de modelo a persistir en una base de datos PostgreSQL generada con jOOQ como alternativa a Hibernate, añadiendo o eliminando instancias persistidas son visualizadas desde el proyecto web y back.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import org.jooq.util.GenerationTool
import org.jooq.util.jaxb.Configuration
import org.jooq.util.jaxb.CustomType
import org.jooq.util.jaxb.Database
import org.jooq.util.jaxb.ForcedType
import org.jooq.util.jaxb.Generate
import org.jooq.util.jaxb.Generator
import org.jooq.util.jaxb.Jdbc
import org.jooq.util.jaxb.Target

buildscript {
    repositories {
        mavenCentral()
    }
    
    dependencies {
        classpath 'org.jooq:jooq-codegen:3.6.2'
        classpath 'org.postgresql:postgresql:9.4-1202-jdbc41'
    }
}

dependencies {
    compile 'org.apache.commons:commons-lang3:3.4'
    compile 'joda-time:joda-time:2.3'
    
    compile 'org.jooq:jooq:3.6.2'
    compile 'org.jooq:jooq-meta:3.6.2'

    runtime 'org.postgresql:postgresql:9.4-1202-jdbc41'
    
}

task generateModels << {
    Configuration configuration = new Configuration()
        .withJdbc(new Jdbc()
            .withDriver('org.postgresql.Driver')
            .withUrl('jdbc:postgresql://localhost:5432/app')
            .withUser('sa')
            .withPassword('sa'))
        .withGenerator(new Generator()
            .withGenerate(new Generate()
                .withInterfaces(true)
                .withRelations(true))
            .withName('org.jooq.util.DefaultGenerator')
            .withDatabase(new Database()
                .withCustomTypes([
                    new CustomType()
                        .withName('org.joda.time.DateTime')
                        .withConverter('io.github.picodotdev.gradle.core.jooq.DateTimeConverter')
                        
                ])
                .withForcedTypes([
                    new ForcedType()
                        .withName('org.joda.time.DateTime')
                        .withTypes('TIMESTAMP')                     
                ])
                .withName('org.jooq.util.postgres.PostgresDatabase')
                .withIncludes('.*')
                .withExcludes('')
                .withInputSchema('gradle'))
            .withTarget(new Target()
                .withPackageName('io.github.picodotdev.gradle.core.models')
                .withDirectory('src/main/java')));

    GenerationTool.main(configuration)
}

task updateDatabase(type:Exec) {
    commandLine './liquibase.sh', 'update'
}

Para arrancar los proyectos web deberemos inicializar la base de datos. Con docker-compose y el archivo descriptor de Docker) iniciamos el contenedor de Docker con la base de datos PostgreSQL. La base de datos deberemos crearla manualmente pero el esquema donde se guardarán los datos los crearemos con Liquibase que nos permite hacer modificaciones a una BBDD, deberemos tenerlo instalado y su comando incluido en el PATH del sistema para este ejemplo.

1
2
3
4
5
6
7
[picodotdev@archlinux postgres]$ docker-compose up
$ docker exec -it postgres_postgres_1 bash
$ psql -U sa
CREATE DATABASE app;
\c app
\q
[picodotdev@archlinux core]$ ./gradlew updateDatabase

Una vez tenemos en cada directorio los archivos build.gradle y el resto de archivos que necesite cada proyecto (archivos .java, .tml, …) podemos construir los módulos a la vez o de forma individual. Podemos iniciar los proyectos web y acceder a ellos con el navegador con:

1
2
3
4
5
[picodotdev@archlinux MultiprojectGradle]$ ./gradlew build
[picodotdev@archlinux MultiprojectGradle]$ cd back
[picodotdev@archlinux back]$ ./gradlew tomcatRun
[picodotdev@archlinux MultiprojectGradle]$ cd web
[picodotdev@archlinux web]$ ./gradlew tomcatRun
Iniciando aplicación backoffice

Creando un producto desde la consola de PostgreSQL y refrescando la página en el proyecto de web o back veremos que se visualizan sus datos.

1
insert into gradle.item ("dateCreate", name, stock) values (now(), 'A laptop', '3');
Aplicación backoffice

El código fuente completo del ejemplo puedes verlo y descargarlo desde su repositorio de GitHub.

Portada libro: PlugIn Tapestry

Libro PlugIn Tapestry

Si te interesa Apache Tapestry descarga gratis el libro de más de 300 páginas que he escrito sobre este framework en el formato que prefieras, PlugIn Tapestry: Desarrollo de aplicaciones y páginas web con Apache Tapestry, y el código de ejemplo asociado. En el libro comento detalladamente muchos aspectos que son necesarios en una aplicación web como persistencia, pruebas unitarias y de integración, inicio rápido, seguridad, formularios, internacionalización (i18n) y localización (l10n), AJAX, ... y como abordarlos usando Apache Tapestry.