domingo, 20 de diciembre de 2015

Desarrollando un Plugin Básico de Grade

Introducción

En un post anterior se mostró cómo es posible tener diferentes tipos de construcción para un mismo proyecto Gradle, dependiendo del ambiente de despliegue, modificando el script de construcción (build.gradle). Sin embargo tener que repetir dicho código en cada proyecto es poco práctico y de hecho una práctica poco recomendada en el desarrollo de software (Duplicate Code).

Adicionalmente se tenía un inconveniente para aquellos que usan un IDE al intentar ejecutar la aplicación, ya que las propiedades que se encuentran en el classpath del proyecto en el IDE (carpeta de recursos/resources) tienen como valores los tokens, razón por la cual se requería primero construir y ejecutar mediante Gradle y luego iniciar un proceso de depuración remota (remote debug).

Es por ello que en este post se mostrará como desarrollar un plugin propio de Gradle, lo cual es una de las grandes ventajas de esta herramienta ya que no solamente se cuenta con los plugins oficiales, sino que también se pueden desarrollar los propios en Groovy, Java o Scala e inclusive incluirlos en el portal oficial de plugins de Gradle.

También se aprovechará la oportunidad para solucionar el inconveniente mencionado para quienes usan un IDE que no ejecuta las aplicaciones teniendo en cuenta las tareas de Gradle.

Básicamente lo que hará este plugin es tomar los archivos ".properties" de la carpeta de recursos del proyecto y comparar sus llaves con las que se encuentren en los archivos con el mismo nombre en la carpeta correspondiente al ambiente de despliegue a usar (y ubicación de sub-carpetas si las hay) y con las propiedades del sistema indicadas, reemplazando los valores de las llaves que coincidan, sin usar tokens (texto entre símbolos "@"). Los archivos de otro tipo serán copiados y sólo si estos usan tokens, entonces sus valores serán reemplazados usando las propiedades del sistema.

Esto permitirá que los archivos de la carpeta de recursos se puedan tener directamente los valores por defecto a usar en el proyecto (por ejemplo los del ambiente de desarrollo local) y reemplazarlos sólo al momento de generar el archivo ejecutable del proyecto para el ambiente externo (pruebas o producción), así los IDE podrán ejecutar el proyecto sin depender de Gradle.

Dado que este plugin estará basado en código que previamente se incluyó en el script de construcción, el cual usa un DSL basado en Groovy se usará dicho lenguaje.


Configuración Gradle

gradle.properties

version=0.0.1-SNAPSHOT
group=com.blogspot.nombre_temp.gradle

build.gradle

buildscript {
    repositories {
        maven {
            url 'https://plugins.gradle.org/m2/'
        }
    }
    dependencies {
        classpath 'com.gradle.publish:plugin-publish-plugin:0.9.2'
        classpath 'net.researchgate:gradle-release:2.3.4'
    }
}

apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'eclipse'
apply plugin: 'net.researchgate.release'
apply plugin: 'com.gradle.plugin-publish'

task wrapper(type: Wrapper) {
    gradleVersion = '2.9'
}

repositories {
    jcenter()
}

dependencies {
    compile gradleApi()
    compile localGroovy()
}

test {
    testLogging.showStandardStreams = true
    testLogging.exceptionFormat = 'full'
}

afterReleaseBuild.dependsOn publishPlugins

pluginBundle {
    website = 'https://github.com/guillermo-varela/gradle-replace-properties-resources-plugin'
    vcsUrl = 'https://github.com/guillermo-varela/gradle-replace-properties-resources-plugin'

    plugins {
        replacePropertiesResources {
            id = 'com.blogspot.nombre-temp.replace.properties.resources'
            displayName = 'Gradle Replace Properties Resources'
            description = 'Adds the propertyfile task from Ant to the processResources Gradle task'
            tags = ['propertyfile', 'processResources', 'environment', 'deployment environment', 'properties']
        }
    }
}

Líneas 1-11: Se incluye el repositorio oficial de plugins de Gradle y se agregan las dependencias de los plugins "Publishing Plugin" y "Gradle Release", los cuales se usarán en conjunto no sólo para controlar el versionamiento del proyecto, sino también para que cada versión sea publicada en el portal de plugins de Gradle.

Líneas 27-30: Se agregan las dependencias necesarias para el desarrollo del plugin; "localGroovy" sólo se requiere si el lenguaje a usar es Groovy.

Línea 37: Es la instrucción que permite publicar el plugin (tarea publishPlugins) luego de versionar el proyecto, integrando así los plugins "Publishing Plugin" y "Gradle Release".

Línea 39-51: Configuración del plugin "Publishing Plugin":
  • website: Sitio web de la compañía o persona que desarrolló el plugin, o página oficial del plugin.
  • vcsUrl: URL en la que puede encontrarse el versionamiento del código del plugin.
  • plugins: Es posible tener varios plugins dentro de un mismo proyecto y en esta sección se define cada uno. En este caso se tiene sólo un plugin.
  • replaceTokensResources: Identificador del bloque en la sección plugins. Este nombre sólo se usa dentro del script de construcción, no es necesario que corresponda con el identificador del plugin usado en los proyectos que lo apliquen.
  • id: Este valor sí debe corresponder con el identificador del plugin, el texto usado al aplicarlo (apply) en otros proyectos.
  • displayNamedescriptiontags: Son elementos para incluir datos informativos y facilitar que sea encontrado en las búsquedas dentro del portal de plugins.

Más información acerca de estas y otras opciones se pueden encontrar en: https://plugins.gradle.org/docs/publish-plugin

src/main/resources/META-INF/gradle-plugins/com.blogspot.nombre-temp.replace.properties.resources.properties

implementation-class=com.blogspot.nombre_temp.gradle.replace.properties.resources.ReplacePropertiesResourcesPlugin

Este archivo debe tener como nombre el identificador del plugin (el mismo valor de la propiedad "id") y aquí se indica el nombre completo de la clase que tiene el código del plugin. En caso de tener varios plugins, se tendrían varios archivos dentro de la carpeta "gradle-plugins", uno por cada plugin.

Código del Plugin

ReplacePropertiesResourcesPlugin.groovy

package com.blogspot.nombre_temp.gradle.replace.properties.resources

import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.api.InvalidUserDataException
import org.gradle.api.Plugin
import org.gradle.api.Project

/**
 * This Gradle plugin replaces the values in ".properties" files inside the project's resources with values from files with the same name inside a different
 * name after the "processResources" Gradle task, which is used during the build process for projects with code to be executed in the JVM (like Java and Groovy).
 *
 * For files other than ".properties" they will also be copied and filtered using using the ReplaceTokens feature from Ant.
 * Any text between "@" symbols (tokens) will be replaced with a value from a system property with the same key as the surrounded text.
 */
class ReplacePropertiesResourcesPlugin implements Plugin {
    @Override
    public void apply(Project project) {

        project.afterEvaluate { p ->
            def processResourcesTask = p.tasks.findByName('processResources')

            if (processResourcesTask == null) {
                println "processResources task not found. Make sure to apply the plugin that has it before (for example 'java' or 'groovy')."
            } else {
                processResourcesTask.inputs.properties System.properties
                def environment = System.properties.env

                if (environment != null) {
                    def configEnvironmentFolder = p.properties.configEnvironmentFolder ? p.properties.configEnvironmentFolder : 'config'
                    def environmentFolder = p.file("$configEnvironmentFolder/$environment")

                    if (!p.file(configEnvironmentFolder).exists()) {
                        throw new InvalidUserDataException("Configuration environment folder not found: $configEnvironmentFolder")
                    }
                    if (!environmentFolder.exists()) {
                        throw new InvalidUserDataException("Environment folder not found: $configEnvironmentFolder/$environment")
                    }

                    // Executed only if the configuration files or the system properties changed from previous execution
                    processResourcesTask.inputs.dir p.file("$configEnvironmentFolder/$environment")

                    processResourcesTask.doLast {
                        println "***********************************************************"
                        println "Using environment: $environment"
                        println "***********************************************************"

                        // Copy all resources files, except ".properties"
                        p.fileTree(dir: "$configEnvironmentFolder/$environment" , exclude: '**/*.properties').each { file ->
                            def fileRelativePath = environmentFolder.toURI().relativize( file.toURI() ).path

                            p.sourceSets.each { source ->
                                // Gets the corresponding file in the resources build folder
                                def ouputFile = p.file("$source.output.resourcesDir/$fileRelativePath")
                                if (ouputFile.exists()) {
                                    p.copy {
                                        into ouputFile.parent
                                        from(file) {
                                            filter(ReplaceTokens, tokens: System.properties)
                                        }
                                    }
                                }
                            }
                        }

                        p.fileTree(dir: "$configEnvironmentFolder/$environment" , include: '**/*.properties').each { file ->
                            def fileRelativePath = environmentFolder.toURI().relativize( file.toURI() ).path

                            p.sourceSets.each { source ->
                                // Gets the corresponding file in the resources build folder
                                def ouputFile = p.file("$source.output.resourcesDir/$fileRelativePath")
                                if (ouputFile.exists()) {
                                    def environmentProperties = new Properties()

                                    file.withInputStream {
                                        environmentProperties.load(it);
                                    }

                                    // Overwrites the values in the file with the ones given from command line arguments -Dkey
                                    System.properties.each { key, value ->
                                        if (environmentProperties.containsKey(key)) {
                                            environmentProperties.put(key, value)
                                        }
                                    }

                                    // Replaces all values on "ouputFile" with the ones contained in "environmentProperties"
                                    environmentProperties.each { propKey, propValue ->
                                        p.ant.propertyfile(file: ouputFile) {
                                            entry(key: propKey, type: 'string', operation: '=', value: propValue)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Línea 15: Se implementa la interfaz Plugin, la cual se requiere para el desarrollo de nuevos plugins.

Línea 17: Implementación del método "apply", el cual se ejecuta al aplicar el plugin a un proyecto, recibiendo precisamente como parámetro el proyecto en que se aplica.

Línea 19: Mediante un Closure se indica el código a ejecutar luego de que el script de construcción del proyecto ha sido evaluado y se han aplicado las configuraciones iniciales. El código de este plugin se deja dentro de este bloque para asegurar que al momento de buscar la tarea "processResources" el plugin que la adicione ("java", "groovy", o cualquier otra) ya se encuentre aplicado y la tarea haga parte del proyecto.

Línea 25: Adición de las propiedades del sistema a las entradas (inputs), para ejecutar la tarea "processResources" sólo cuando se cambien estos datos (adicional a los archivos de recursos) y así conservar la Construcción Incremental.

Línea 26: Se asigna el ambiente a usar indicado como parámetro (propiedad del sistema).

Línea 28: Si no se indicó un ambiente, no se continua con la ejecución de este plugin, usando entonces los valores de las propiedades que se encuentran directamente en la carpeta de recursos.

Línea 29: Con esta línea se permite cambiar el nombre de la carpeta en la cual se encontrarán las sub-carpetas de cada ambiente agregando la propiedad extra "configEnvironmentFolder", bien sea dentro del archivo "build.gradle" o en "gradle.properties" del proyecto en que se aplique el plugin.

Líneas 32-37: Se valida que las carpetas de configuración y de ambientes existan. Esta es una mejora con respecto al script del post anterior, ya que no se está dependiendo de una lista.

Línea 40: Adición de la carpeta del ambiente usado a las entradas (inputs), para ejecutar la tarea "processResources" sólo cuando se cambie su estructura o el contenido de sus archivos, adicional a las entradas previamente agregadas.

Línea 42: Se usa el bloque "doLast" de la tarea "processResources" para que el código sea ejecutado únicamente cuando se ejecute esta tarea (no en la fase de configuración) y después de que se copien los archivos de recursos (por defecto en "src/main/resources") a la carpeta de construcción (por defecto en "build/resources/main").

Línea 48: Iteración de los archivos que no son ".properties" ubicados en la carpeta del ambiente de despliegue a usar.

Línea 49: Se obtiene la ruta relativa del archivo iterado con respecto al proyecto. Esto con el fin de poder determinar si por ejemplo un archivo "src/main/resources/logback.xml" tiene su contra-parte en la carpeta del ambiente, por ejemplo "config/prod_1/logback.xml". No se valida sólo con el nombre del archivo para permitir tener sub-carpetas.

Línea 51: Iteración de las carpetas de código fuente (SourceSets) y recursos del proyecto. Por defecto se tienen "main" y "test" pero pueden añadirse más según se necesite en el proyecto.

Líneas 53-54: La instrucción "source.output.resourcesDir" obtiene la carpeta de salida (en la que se copian al compilar y construir el proyecto) de los recursos del SourceSet iterado. Por ejemplo, al iterar "main", la carpeta de salida de los recursos sería "build/resources/main". Luego se adiciona la ruta del archivo de propiedades iterado desde la línea 48 y se verifica si este existe la carpeta de salida.

Líneas 55-61: Se copia el archivo desde su carpeta origen (ambiente seleccionado) hacia la carpeta de salida de los recursos, reemplazando los tokens que dicho archivo pueda tener. Si el archivo no tiene tokens, se copia el archivo sin modificaciones.

Línea 65: Iteración de los archivos ".properties" que se encuentran en la carpeta del ambiente de despliegue a usar.

Línea 66: Se obtiene la ruta relativa del archivo iterado con respecto al proyecto.

Línea 68: Iteración de las carpetas de código fuente (SourceSets) y recursos del proyecto.

Líneas 74-76: Se cargan las propiedades que se encuentran en el archivo del ambiente usado.

Líneas 79-83: Las propiedades anteriormente cargadas se sobrescriben con las propiedades del sistema que tengan la misma llave.

Líneas 86-90: En aquel post anterior se estaba usando el filtro ReplaceTokens de Ant para reemplazar los tokens de los archivos de recursos procesados. En este plugin se usa en cambio se usa la tarea "PropertyFile" (también de Ant) para tomar las propiedades del archivo que fue copiado en la carpeta de salida y reemplazar sus valores con los indicados en el archivo correspondiente en la carpeta del ambiente o en las propiedades del sistema. De esta manera el archivo de propiedades original no se modifica y no requiere tokens en su contenido, permitiendo tener los valores del ambiente por defecto allí sin afectar la construcción para otros ambientes.


En general este código es muy similar al del post anterior con un par de mejoras.

Nota: Este plugin no tiene una clase para definir una tarea propia, ya que lo que se hace en este caso es modificar la tarea ya existente "processResources". Para más información sobre el desarrollo de tareas dentro de un plugin de Gradle puede consultarse la documentación oficial.

Pruebas Locales del Plugin

Antes de publicar el plugin, este puede aplicarse y probarse en un proyecto Gradle localmente. Para ello puede usarse la tarea "install" del plugin "maven" con lo cual se instala el JAR del proyecto en el repositorio local de Maven.


gradlew.bat install
:compileJava UP-TO-DATE
:compileGroovy
:processResources
:classes
:jar
:groovydoc
:publishPluginGroovyDocsJar
:publishPluginJar
:javadoc UP-TO-DATE
:publishPluginJavaDocsJar
:install

BUILD SUCCESSFUL

Una vez hecho esto puede incluirse en un proyecto Gradle. En este caso se usará una copia del proyecto que usaba el script original: https://github.com/guillermo-varela/gradle-replace-properties-resources-plugin-demo

Para usar el plugin desde el repositorio local se debe cambiar el archivo "build.gradle" de la siguiente manera:

buildscript {
    repositories {
        mavenLocal()
        maven {
            url 'https://plugins.gradle.org/m2/'
        }
    }
    dependencies {
       classpath 'net.researchgate:gradle-release:2.3.4'
       classpath 'com.blogspot.nombre-temp.replace.properties.resources:0.0.1-SNAPSHOT'
    }
}

plugins {
  id 'java'
  id 'application'
  id 'net.researchgate.release' version '2.3.4'
  id 'com.blogspot.nombre-temp.replace.properties.resources' version '0.0.1-SNAPSHOT'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8
compileJava.options.encoding = 'UTF-8'

mainClassName = 'com.blogspot.nombre_temp.jetty.jersey.multi.project.example.ExampleStarter'

jar {
    manifest {
        attributes 'Implementation-Title': 'Jetty and Jersey Multi Environment Example', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.9'
}

repositories {
    jcenter()
}

dependencies {
    compile "org.eclipse.jetty:jetty-server:$jettyVersion"
    compile "org.eclipse.jetty:jetty-servlet:$jettyVersion"

    compile "org.glassfish.jersey.core:jersey-server:$jerseyVersion"
    compile "org.glassfish.jersey.containers:jersey-container-servlet:$jerseyVersion"
    compile "org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion"

    compile "commons-configuration:commons-configuration:1.10"
}

Las diferencias principales, con respecto a la versión original, están en las líneas 3, 10 y 17, en las cuales se adiciona el repositorio de Maven local como fuente de plugins, se agrega la dependencia al plugin que se está probando y se aplica.

También puede verse que esta versión del archivo es mucho más corta y fácil de leer al no tener que modificar la tarea "processResources", porque ello ya es realizado en el nuevo plugin.

Para probar rápidamente el funcionamiento del plugin puede ejecutarse simplemente la tarea "processResources" y los archivos de recursos procesados estarán en la carpeta "build/resources".


gradlew.bat processResources
:processResources
***********************************************************
Using environment: local
***********************************************************

BUILD SUCCESSFUL

build/resources/main/application.properties

app.instance.name=Jetty-Server
app.instance.number=1


gradlew.bat processResources -Denv=prod2
:processResources
***********************************************************
Using environment: prod2
***********************************************************

BUILD SUCCESSFUL

build/resources/main/application.properties

app.instance.name=Jetty-Server
app.instance.number=2


gradlew.bat processResources -Denv=prod2 -Dapp.instance.number=5
:processResources
***********************************************************
Using environment: prod2
***********************************************************

BUILD SUCCESSFUL

build/resources/main/application.properties

app.instance.name=Jetty-Server
app.instance.number=5

Publicación del Plugin

Para publicar el plugin en el portal oficial, luego de tener configurado "Publishing Plugin", se debe crear una cuenta y seguir las instrucciones en https://plugins.gradle.org/docs/submit para obtener un "API key" y "API secret".

Dado que se tienen integrados plugins "Publishing Plugin" y "Gradle Release", simplemente al ejecutar la tarea de Gradle "release", se versiona el proyecto en el repositorio de GitHub y se carga el plugin compilado al portal de plugins. Este plugin puede encontrarse en: https://plugins.gradle.org/plugin/com.blogspot.nombre-temp.replace.properties.resources

Nota: Al momento de escribir este post el plugin "Publishing Plugin" tiene un error ya reportado, en el que al usar un archivo "gradle.properties" en el proyecto no se toman los valores de "$USER_HOME/.gradle/gradle.properties". Para solucionar esto, sin incluir la llave y contraseña de Gradle en el archivo del proyecto, se puede ejecutar la tarea de Gradle con dichos valores:

gradlew.bat release -Dgradle.publish.key=xxxxxxx -Dgradle.publish.secret=yyyyyyy

Aplicando el Plugin

Una vez se tiene el plugin publicado en el portal de Gradle es posible modificar el archivo "build.gradle" del proyecto con el que se hizo la prueba local, para que use la versión publicada:

plugins {
  id 'java'
  id 'application'
  id 'net.researchgate.release' version '2.3.4'
  id 'com.blogspot.nombre-temp.replace.properties.resources' version '0.0.4'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8
compileJava.options.encoding = 'UTF-8'

mainClassName = 'com.blogspot.nombre_temp.jetty.jersey.multi.project.example.ExampleStarter'

jar {
    manifest {
        attributes 'Implementation-Title': 'Jetty and Jersey Multi Environment Example', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.9'
}

repositories {
    jcenter()
}

dependencies {
    compile "org.eclipse.jetty:jetty-server:$jettyVersion"
    compile "org.eclipse.jetty:jetty-servlet:$jettyVersion"

    compile "org.glassfish.jersey.core:jersey-server:$jerseyVersion"
    compile "org.glassfish.jersey.containers:jersey-container-servlet:$jerseyVersion"
    compile "org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion"

    compile "commons-configuration:commons-configuration:1.10"
}

Las pruebas sobre el funcionamiento del plugin pueden ser las mismas que se hicieron localmente. El proyecto usando este plugin publicado puede descargarse en: https://github.com/guillermo-varela/gradle-replace-properties-resources-plugin-demo

Nota: En este caso el plugin desarrollado aparece con versión "0.0.4" debido a que se realizaron un par de mejoras a la primera versión publicada, pero el portal de Gradle no permite subir cambios sobre la misma versión del plugin sino que esta debe incrementarse.

Ejecutando la Aplicación desde un IDE

Como se comentó al inicio, este plugin permite que al ejecutar el proyecto desde un IDE (sin ejecutar las tareas de Gradle) puedan usarse los valores de un ambiente local de desarrollo, lo cual facilita la ejecución y depuración al no tener que configurar desde el IDE la ejecución de Gradle y la depuración remota.

Figura 1 - Aplicación ejecutada desde Eclipse sin ejecutar Gradle

Referencias

https://docs.gradle.org/current/userguide/custom_plugins.html
https://docs.gradle.org/current/userguide/custom_tasks.html
https://plugins.gradle.org/docs/publish-plugin
https://docs.gradle.org/current/userguide/build_lifecycle.html
https://docs.gradle.org/current/userguide/java_plugin.html
https://docs.gradle.org/current/javadoc/org/gradle/language/jvm/tasks/ProcessResources.html

Más sobre Gradle

http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html

domingo, 13 de diciembre de 2015

Acelerando las Construcciones de Gradle usando Gradle Daemon

Introducción

Los proyectos grandes pueden tomar bastante tiempo para compilarse y construir los paquetes finales (JAR, WAR, EAR, etc.), dada la gran cantidad de líneas de código, módulos y de dependencias que puedan tener.

Gradle mediante la Construcción Incremental ha ayudado a disminuir dichos tiempos al ejecutar cada tarea del proceso de construcción sólo cuando detecta cambios en los datos de entrada de dichas tareas (archivos, parámetros, etc.), sin embargo ahora se cuenta con una herramienta dentro de Gradle que ayuda a disminuir aún más dichos tiempos: Gradle Daemon.

Básicamente se trata de un proceso demonio (daemon) que luego de la primera construcción de un proyecto Gradle permanece en el sistema esperando la próxima construcción para así no tener que volver a cargar en memoria todos los componentes de Gradle necesarios, todo esto sin que se requiera de mayor configuración o gestión del proceso por parte del desarrollador.

El proceso permanece en espera por aproximadamente 3 horas, luego de las cuales si no se ha realizado ninguna construcción con Gradle se terminará automáticamente. También vale la pena aclarar que si la configuración del proyecto (cambio de versión de Java, codificación del texto, etc.) el proceso que hizo la construcción anterior a dichos cambios ya no se considerará compatible y Gradle automáticamente iniciará un nuevo proceso demonio.

Habilitando Gradle Daemon

Cuando se está usando un IDE con integración con Gradle ya se tiene habilitado por defecto el uso de Gradle Daemon, sin embargo para las construcciones realizadas desde línea de comandos (terminal) existen 4 maneras de habilitarlo:
  • Agregar el siguiente valor a la variable de entorno del sistema operativo "GRADLE_OPTS": -Dorg.gradle.daemon=true
  • Agregar el siguiente valor al archivo "<GRADLE_USER_HOME>/gradle.properties" (GRADLE_USER_HOME normalmente es "<carpeta_usuario>/.gradle/", por ejemplo "/home/usuario/.gradle/" o "C:\Users\usuario\.gradle\"): org.gradle.daemon=true
  • A nivel sólo del proyecto, agregar lo siguiente en el archivo "gradle.properties": org.gradle.daemon=true
  • A nivel sólo de la construcción específica, agregando el siguiente parámetro en el comando: --daemon


Deshabilitando Gradle Daemon

Para las construcciones iniciadas usando comandos Gradle Daemon no se encuentra habilitado, así que si no se ha indicado explícitamente que se quiere usar dicho proceso, con alguno de los métodos mencionados en el punto anterior, no se requiere ninguna acción.

En caso de tener alguna de las anteriores configuraciones o estar usando un IDE, puede indicarse el valor "false" en alguna de las 3 primeras configuraciones o indicar en el comando el parámetro "--no-daemon", el cual tiene precedencia sobre los demás.

Un ejemplo de cuando puede ser necesario deshabilitar este proceso es cuando se usa un servidor de integración continua, ya que en primer lugar no debería ser tan frecuente la construcción del proyecto (o por lo menos no tanto como en un ambiente de desarrollo) y también es un caso en el que se quiere que la construcción se realice de manera aislada, sin depender de ejecuciones previas para obtener resultados que sólo dependan de los cambios actuales en el proyecto, bien sea resultado de ejecución de pruebas o generación del archivo compilado con el proyecto.

Usando Gradle Daemon

Para mantener el ejemplo simple se usará como base el API REST desarrollado en un post anterior.

Como se tiene inicialmente, sin Gradle Daemon se obtiene el siguiente resultado:

gradlew.bat build
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:startScripts
:distTar
:distZip
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

Total time: 6.924 secs

Luego al agregar "org.gradle.daemon=true" al archivo "gradle.properties" y borrar el contenido de la carpeta "build" para que realice la construcción de nuevo:

gradlew.bat build
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:startScripts
:distTar
:distZip
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

Total time: 3.146 secs

Puede verse que se tiene una diferencia casi del 50% en el tiempo. Es posible que las primeras construcciones usando Gradle Daemon no tengan una diferencia en tiempo tan grande, pero el equipo de Gradle ha documentado que generalmente después de la quinta o décima vez la diferencia empieza a notarse más, debido a que la optimización de código que hace la máquina virtual es progresiva, no simultánea.

Más Información

Mayores detalles acerca de Gradle Daemon pueden encontrarse en la documentación oficial: https://docs.gradle.org/current/userguide/gradle_daemon.html

Existe también una funcionalidad adicional de Gradle que permite mejorar los tiempos de construcción: Construcción en Paralelo. Esta opción permite que los proyectos multi-módulo puedan construir sus módulos simultáneamente, en lugar de secuencialmente, sin embargo sólo debe usarse en los casos en que los módulos sean independientes entre sí (Decoupled Projects). Más información puede encontrarse  también en la documentación oficial de Gradle: https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:parallel_execution

También pueden encontrarse otros posts acerca de Gradle en: http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html

domingo, 6 de diciembre de 2015

Proyectos Gradle con Múltiples Ambientes (Filtrado de Archivos de Recursos)

Introducción

Como se ha visto en post anteriores, Gradle es una herramienta que permite compilar, construir y ejecutar aplicaciones de diferentes tipos como aplicaciones de escritorio, scripts, APIs RESTful o aplicaciones web., bien sea en Java o en alguno de los otros lenguajes de programación soportados.

En esta ocasión se mostrará cómo es posible tener diferentes tipos de construcción para un mismo proyecto, dependiendo del ambiente de despliegue: local, pruebas, producción, etc.

En proyectos pequeños y/o personales es muy común que se tenga un solo ambiente de despliegue, ya que se usa la misma base de datos, servidores, entre otras configuraciones. Pero en proyectos corporativos lo más común en cambio es que durante la fase de desarrollo por ejemplo se use una base de datos diferente a la que usan los usuarios reales de la aplicación, se tengan capacidades del servidor diferentes, entre otras diferencias, con el fin de afectar lo menos posible a los usuarios, personal de pruebas (QA) y que los propios desarrolladores tengan mayor libertad para trabajar.

Para mantener el ejemplo simple se usará como base el API REST desarrollado en un post anterior. En aquel ejemplo el número del puerto, la cantidad hilos del servidor, la cola de espera y la respuesta del llamado "health" tienen valores fijos en el propio código (hard code). Estos datos ahora serán tomados de archivos de configuración, cuyos valores dependerá de los parámetros usados al momento de construir el proyecto con Gradle.

Se tendrán 4 ambientes posibles: "local", "qa", "prod1", "prod2". Hay dos ambientes para producción, ya que en la actualidad es muy frecuente encontrar que los proyectos se despliegan en dos (o más) servidores de producción al mismo tiempo, con el fin de tener escalabilidad horizontal.

El proyecto completo se puede descargar desde: https://github.com/guillermo-varela/jetty-jersey-multi-env-example

Archivos de Configuración de la Aplicación

Estarán ubicados dentro de la carpeta de recursos del proyecto ("src/main/resources" por defecto).

\---src
    \---main
        +---java
        \---resources
                application.properties
                server.properties

Dependiendo de los gustos personales de cada desarrollador, equipo de desarrollo o framework usado es posible que se tenga toda la configuración necesaria en un solo archivo (por ejemplo en Spring Boot se tiene en application.properties), como también se tiene la posibilidad de usar varios archivos con la configuración de cada componente de la aplicación por separado.

En este ejemplo se tendrán dos archivos, ya que el procedimiento a mostrar sirve para uno o varios archivos: "application.properties" con configuraciones generales de la aplicación y "server.properties" con configuraciones relacionadas directamente con el servidor/contenedor web.

application.properties

app.instance.name=Jetty-Server
app.instance.number=@app.instance.number@

server.properties

server.port=8080
server.max.queued.thread.pool=@server.max.queued.thread.pool@
server.accept.queue.size=@server.accept.queue.size@

Los valores que se encuentran entre símbolos "@" serán los reemplazados al momento de construir la aplicación usando un proceso de filtrado en los archivos (muy similar al Filtering de Maven). La razón de usar "@" es que Gradle usará el filtro ReplaceTokens de Ant para filtrar/procesar los archivos, el cual ya usaba dicho formato para definir los tokens a reemplazar.

Los valores de "app.instance.name" y "server.port" tienen valores fijos, por lo cual estos no serán modificados en la construcción.

Nota: Es muy importante no tener propiedades con la misma llave en archivos diferentes, ya que los archivos serán procesados usando todos las mismas variables, es decir, si se tiene una llave "test" en los dos archivos y se quiere que su valor sea reemplazado en la construcción, el valor final será el mismo en ambos archivos. Se recomienda tener un estándar para las llaves en cada archivo, como se tiene en este caso.

Archivos de Configuración por Ambiente

En la raíz del proyecto se tendrá la carpeta "config", la cual a su vez tendrá una sub-carpeta por cada ambiente:
+---local
|       application.properties
|       server.properties
|
+---prod1
|       application.properties
|       server.properties
|
+---prod2
|       application.properties
|       server.properties
|
\---qa
        application.properties
        server.properties

application.properties - local

app.instance.number=1

server.properties - local

server.max.queued.thread.pool=8
server.accept.queue.size=10

application.properties - qa

app.instance.number=1

server.properties - qa

server.max.queued.thread.pool=20
server.accept.queue.size=100

application.properties - prod1

app.instance.number=1

server.properties - prod1

server.max.queued.thread.pool=50
server.accept.queue.size=200

application.properties - prod2

app.instance.number=2

server.properties - prod2

server.max.queued.thread.pool=50
server.accept.queue.size=200

Nota: Como puede verse, en estos archivos sólo se indican las propiedades que requieren modificarse en los archivos del proyecto, no es necesario volver a indicar las propiedades que ya tienen valor fijo.

Dependencias y Configuración Gradle

gradle.properties

version=1.0.0-SNAPSHOT

jettyVersion=9.3.5.v20151012
jerseyVersion=2.22.1

build.gradle

plugins {
  id 'net.researchgate.release' version '2.0.2'
}

apply plugin: 'java'
apply plugin: 'application'
compileJava.options.encoding = 'UTF-8'

sourceCompatibility = 1.8
targetCompatibility = 1.8

mainClassName = 'com.blogspot.nombre_temp.jetty.jersey.multi.project.example.ExampleStarter'

jar {
    manifest {
        attributes 'Implementation-Title': 'Jetty and Jersey Multi Environment Example', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.9'
}

// ******************** Configuration based on the environment ********************

def setEnvironment() {
    // For production use this argument: -Denv=prod1 or -Denv=prod2
    ext.environment = System.properties.env ? System.properties.env : 'local'

    if (!['local', 'qa', 'prod1', 'prod2'].contains(ext.environment)) {
        throw new GradleException("Invalid environment: $ext.environment")
    } 
}

setEnvironment()

processResources {
    // Executed only if the configuration files and/or the system properties changed from previous execution
    inputs.dir file("config/$environment")
    inputs.properties System.properties

    doFirst {
        println "***********************************************************"
        println "Using environment: $environment"
        println "***********************************************************"

        // Gets configuration values according to the environment being built
        def environmentProperties = new Properties()

        file("config/$environment").listFiles().each { file ->
            file.withInputStream{
                environmentProperties.load(it);
            }
        }

        // Overwrites the values in the file with the ones given from command line arguments -Dkey
        System.properties.each { key, value ->
            if (environmentProperties.containsKey(key)) {
                environmentProperties.put(key, value)
            }
        }

        // Replaces all values with @name@ in the "src/main/resources" files with the ones in "environmentProperties"
        filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: environmentProperties)
    }
}

repositories {
    jcenter()
}

dependencies {
    compile "org.eclipse.jetty:jetty-server:$jettyVersion"
    compile "org.eclipse.jetty:jetty-servlet:$jettyVersion"

    compile "org.glassfish.jersey.core:jersey-server:$jerseyVersion"
    compile "org.glassfish.jersey.containers:jersey-container-servlet:$jerseyVersion"
    compile "org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion"

    compile "commons-configuration:commons-configuration:1.10"
}

Las principales diferencias que se tienen con respecto al ejemplo base son:

Líneas 26-33: Nuevo método dentro de la configuración Gradle el cual permite identificar el ambiente para el cual se está construyendo el proyecto. Existen dos maneras de indicar propiedades para la construcción mediante Gradle:

  • Propiedades del proyecto: usando parámetros con el formato -Pllave=valor y se obtienen mediante "project.properties".
  • Propiedades del sistema: usando parámetros con el formato -Dllave=valor y se obtienen mediante "System.properties".
En este caso se están usando propiedades del sistema, ya que es así como el plugin Gradle para Jenkins (un servidor de Integración Continua muy popular) indica los parámetros al crear una construcción parametrizada.

De esta manera, para indicar que la construcción se realizará para cada uno de los ambientes se indica como parámetro:

  • local (desarrollo): -Denv=local
  • qa (pruebas): -Denv=qa
  • prod1 (nodo 1 de producción): -Denv=prod1
  • prod2 (nodo 2 de producción): -Denv=prod2
Si no se indica ninguno, se asumirá el ambiente local, pero si se indica un nombre de ambiente inválido se lanzará una excepción impidiendo la construcción (línea 31).

Línea 35: Se invoca la ejecución del método anteriormente definido. La razón de tener esto como un método en lugar de una tarea de Gradle es para que este sea ejecutado en la fase de configuración (antes de ejecutar las tareas) y la variable "environment" quede disponible para todas las tareas con el valor correcto.

Líneas 37-66processResources es la tarea del plugin Java de Gradle que se encarga de copiar los archivos de la(s) carpeta(s) de recursos a la carpeta en la que se construye el archivo compilado (JAR, WAR, etc.). Lo que se hace en este fragmento es adicionar a dicha tarea la lógica necesaria para reemplazar los tokens en los archivos de configuración.

  • Líneas 39-40: Se indica que los archivos que se encuentran en la carpeta de configuración del ambiente usado, así como las propiedades del sistema son entradas necesarias para la ejecución de la tarea. Esto permite que sólo se ejecute la tarea si Gradle detecta que desde la última ejecución se han modificado los archivos o los parámetros usados, permitiendo construcciones más rápidas si no se han cambiado los datos (Construcción Incremental).
  • Línea 42: Inicia el bloque "doFirst" en el cual va el código que se ejecuta en cuanto inicia la ejecución de "processResources", esto si los datos de entrada indicados anteriormente han tenido cambios. Este bloque también garantiza que el código contenido no se ejecutará durante la fase de configuración.
  • Líneas 48-54: Se crea la variable "environmentProperties" en la cual se almacenan todas las propiedades definidas en los archivos de configuración del ambiente seleccionado, lo cual se hace recorriendo los archivos de la carpeta de dicho ambiente, leyendo su contenido y agregándolo a la variable.
  • Líneas 57-61: Adicional a tener las configuraciones en los archivos de cada ambiente, también es relativamente común tener valores sensibles que no pueden/deben estar disponibles dentro del código del proyecto (como por ejemplo credenciales a bases de datos de producción). En este fragmento lo que se hace es sobrescribir los valores de las propiedades en los archivos con los que se indiquen como propiedades del sistema (mediante parámetros); así por ejemplo si se indica como parámetro "-Dserver.max.queued.thread.pool=600" no importará el valor del archivo del ambiente seleccionado, el valor usado será 600.
  • Línea 64: Se usa el filtro de Ant ReplaceTokens para reemplazar los tokens en los recursos que se incluirán en la aplicación.
Línea 80: Las propiedades de estos archivos podrían obtenerse en el código de la aplicación mediante la clase Properties que ya se encuentra disponible en Java, sin embargo se opta por usar la librería Apache Commons Configuration ya que provee funcionalidades adicionales como obtener los valores usando un tipo de dato específico (en lugar de sólo String), indicar valores por defecto o actualizar los valores según alguna condición.

Nota: aunque al momento de escribir este post, la versión de esta librería en estado "stable" tiene poco más de 2 años de antigüedad (1.10), ya se está trabajando en la versión 2.0 la cual es un re-diseño de la librería.

Accediendo a las Configuraciones

Para permitir que en todo el código de la aplicación se tenga acceso a los valores de los archivos de configuración se tendrá una clase que inicialice las configuraciones y permita acceder a estas.

Clase ConfigurationProvider

package com.blogspot.nombre_temp.jetty.jersey.multi.project.example.util;

import java.io.File;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;

public class ConfigurationProvider {

    private ConfigurationProvider() {}

    private static PropertiesConfiguration serverConfiguration;
    private static PropertiesConfiguration applicationConfiguration;

    public static void startConfiguration() throws ConfigurationException {
        ClassLoader classLoader = ConfigurationProvider.class.getClassLoader();

        serverConfiguration = new PropertiesConfiguration();
        serverConfiguration.setFile(new File(classLoader.getResource("server.properties").getFile()));
        serverConfiguration.setReloadingStrategy(new FileChangedReloadingStrategy());
        serverConfiguration.load();

        applicationConfiguration = new PropertiesConfiguration();
        applicationConfiguration.setFile(new File(classLoader.getResource("application.properties").getFile()));
        applicationConfiguration.setReloadingStrategy(new FileChangedReloadingStrategy());
        applicationConfiguration.load();
    }

    public static PropertiesConfiguration getServerConfiguration() {
        return serverConfiguration;
    }

    public static PropertiesConfiguration getApplicationConfiguration() {
        return applicationConfiguration;
    }
}

La inicialización de las configuraciones se hace en el método "startConfiguration" en lugar de hacerlo la primera vez al usar "getServerConfiguration" o "getApplicationConfiguration" ya que al ser una aplicación potencialmente con múltiples usuarios concurrentes (2 ó más al tiempo) se debe garantizar que dicha inicialización se realice una sola vez, lo cual puede hacerse ejecutando "startConfiguration" al mismo tiempo en que se inicia el servidor Jetty Embebido (o en una clase que implemente ServletContextListener en aplicaciones web).

La inicialización única también podría garantizarse usando patrones como por ejemplo "Double-checked locking", pero el uso de "locks" o bloques "synchronized" no solamente haría más lenta la ejecución para el primer usuario de la aplicación, sino también para los demás primeros usuarios concurrentes mientras esperan que dicho proceso termine. Es por esto que inicializar las configuraciones junto con la aplicación se hace más conveniente.

Clase ExampleStarter

package com.blogspot.nombre_temp.jetty.jersey.multi.project.example;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.servlet.ServletContainer;
import com.blogspot.nombre_temp.jetty.jersey.multi.project.example.util.ConfigurationProvider;

public class ExampleStarter {

    public static void main(String[] args) throws ConfigurationException {
        System.out.println("Starting!");

        ConfigurationProvider.startConfiguration();
        PropertiesConfiguration serverConfiguration = ConfigurationProvider.getServerConfiguration();

        ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
        contextHandler.setContextPath("/");

        QueuedThreadPool queuedThreadPool = new QueuedThreadPool(serverConfiguration.getInt("server.max.queued.thread.pool"), 1);
        final Server jettyServer = new Server(queuedThreadPool);

        int acceptors = Runtime.getRuntime().availableProcessors();

        ServerConnector serverConnector = new ServerConnector(jettyServer, acceptors, -1);
        serverConnector.setPort(serverConfiguration.getInt("server.port"));
        serverConnector.setAcceptQueueSize(serverConfiguration.getInt("server.accept.queue.size"));

        jettyServer.addConnector(serverConnector);
        jettyServer.setHandler(contextHandler);

        ServletHolder jerseyServlet = contextHandler.addServlet(ServletContainer.class, "/*");
        jerseyServlet.setInitOrder(0);
        jerseyServlet.setInitParameter(ServerProperties.PROVIDER_PACKAGES, "com.blogspot.nombre_temp.jetty.jersey.multi.project.example.resource");

        try {
            jettyServer.start();

            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    try {
                        System.out.println("Stopping!");

                        jettyServer.stop();
                        jettyServer.destroy();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });

            jettyServer.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Las principales diferencias que se tienen con respecto al ejemplo base son:

Línea 20: Inicialización de la configuración usando la clase "ConfigurationProvider".
Línea 21: Obtención de la configuración del servidor (desde el archivo "server.properties") en una instancia de la clase "PropertiesConfiguration" de Apache Commons Configuration.
Líneas 26, 32, 33: Se usa el método "getInt" para obtener los valores numéricos de la configuración del servidor (máximo de hilos, puerto y capacidad de la cola de aceptors respectivamente).

Cabe anotar que también se tienen métodos en "PropertiesConfiguration" para obtener valores directamente en otros tipos de datos como getBigDecimalgetBooleangetDoublegetFloatgetListgetLonggetStringArraygetString.

Dichos métodos están sobrecargados (overloading) teniendo una definición que recibe sólo un parámetro (la llave de la configuración/propiedad) y otra que además recibe un valor por defecto en caso no encontrar la propiedad. Si se usa sólo la llave pero esta no existe en la configuración se lanzará una "NoSuchElementException", aunque en el caso de getString y otros que retornen instancias de objetos (en lugar de primitivos) por defecto se retorna null si la propiedad no existe, pero se puede lanzar la excepción si se cambia la bandera "throwExceptionOnMissing".

Clase HealthResource

package com.blogspot.nombre_temp.jetty.jersey.multi.project.example.resource;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.commons.configuration.PropertiesConfiguration;
import com.blogspot.nombre_temp.jetty.jersey.multi.project.example.util.ConfigurationProvider;

@Path("/health")
@Produces(MediaType.APPLICATION_JSON)
public class HealthResource {

    @GET
    public String health() {
        PropertiesConfiguration appConfiguration = ConfigurationProvider.getApplicationConfiguration();
        String instanceName = appConfiguration.getString("app.instance.name");
        String instanceNumber = appConfiguration.getString("app.instance.number");

        return String.format("%s_%s: OK", instanceName, instanceNumber);
    }
}

Aquí se está accediendo a la configuración de la aplicación (application.properties) y se está formateando la cadena de respuesta, así en el ambiente local será "Jetty-Server_1: OK", mientras que en el nodo 2 de producción será "Jetty-Server_2: OK".

Ejecutando la Aplicación

Si se está usando un IDE y se intenta ejecutar la aplicación desde este (usando el método main) es posible que se presenten problemas ya que si el IDE no ejecuta las tareas de Gradle antes, los valores de los archivos de configuración no serán reemplazados.

Figura 1 - Aplicación ejecutada desde Eclipse sin ejecutar Gradle

Es por esto que en este caso sí se hace necesario que la aplicación se ejecute, bien sea con la tarea "run" de Gradle o generando los ejecutables de la aplicación, como se indica al final del post con el ejemplo original, bien sea desde la línea de comandos (terminal) o desde el IDE (según la integración con Gradle usada)

gradlew.bat run
:compileJava
:processResources
***********************************************************
Using environment: local
***********************************************************
:classes
:run
Starting!
2015-12-06 17:25:59.875:INFO::main: Logging initialized @260ms
2015-12-06 17:25:59.973:INFO:oejs.Server:main: jetty-9.3.5.v20151012
2015-12-06 17:26:01.049:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@54e041a4{/,null,AVAILABLE}
2015-12-06 17:26:01.144:INFO:oejs.ServerConnector:main: Started ServerConnector@72ade7e3{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2015-12-06 17:26:01.145:INFO:oejs.Server:main: Started @1531ms

Al ejecutarse la tarea "processResources" puede verse que se imprime "Using environment: local", el cual es el ambiente por defecto como indicó anteriormente. Al finalizar el servidor (ctrl + c o cmd + c en Mac desde Terminal) y ejecutar nuevamente "run" la tarea "processResources" no se ejecuta ya que no se encuentran cambios en los archivos de configuración ni en los parámetros usados.

gradlew.bat run
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
Starting!
2015-12-06 17:33:37.766:INFO::main: Logging initialized @265ms
2015-12-06 17:33:37.888:INFO:oejs.Server:main: jetty-9.3.5.v20151012
2015-12-06 17:33:38.680:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@54e041a4{/,null,AVAILABLE}
2015-12-06 17:33:38.787:INFO:oejs.ServerConnector:main: Started ServerConnector@72ade7e3{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2015-12-06 17:33:38.787:INFO:oejs.Server:main: Started @1288ms


Figura 2 - Aplicación ejecutada en el ambiente local

Si se ejecuta nuevamente "run" pero cambiando el ambiente a producción nodo 2 (prod2) se podrá ver que esta vez sí se ejecuta la tarea "processResources" y la respuesta del llamado "health" es diferente, indicando que ahora se tomaron los parámetros de la carpeta "prod2".

gradlew.bat run -Denv=prod2
:compileJava UP-TO-DATE
:processResources
***********************************************************
Using environment: prod2
***********************************************************
:classes
:run
Starting!
2015-12-06 17:39:50.299:INFO::main: Logging initialized @292ms
2015-12-06 17:39:50.380:INFO:oejs.Server:main: jetty-9.3.5.v20151012
2015-12-06 17:39:51.071:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@54e041a4{/,null,AVAILABLE}
2015-12-06 17:39:51.159:INFO:oejs.ServerConnector:main: Started ServerConnector@72ade7e3{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2015-12-06 17:39:51.160:INFO:oejs.Server:main: Started @1153ms

Figura 3 - Aplicación ejecutada en el ambiente producción nodo 2

Como se mencionó anteriormente, también es posible sobrescribir los valores de los archivos de configuración si se indican como parámetros. Por ejemplo si se quiere indicar que el número del nodo es ahora "5" se adiciona "-Dapp.instance.number=5".

gradlew.bat run -Denv=prod2 -Dapp.instance.number=5
:compileJava UP-TO-DATE
:processResources
***********************************************************
Using environment: prod2
***********************************************************
:classes
:run
Starting!
2015-12-06 17:55:54.951:INFO::main: Logging initialized @275ms
2015-12-06 17:55:55.056:INFO:oejs.Server:main: jetty-9.3.5.v20151012
2015-12-06 17:55:55.782:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@54e041a4{/,null,AVAILABLE}
2015-12-06 17:55:55.881:INFO:oejs.ServerConnector:main: Started ServerConnector@72ade7e3{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2015-12-06 17:55:55.882:INFO:oejs.Server:main: Started @1207ms

Figura 4 - Sobrescribiendo el número de la instancia/nodo como parámetro

Finalmente, para generar los archivos ejecutables de la aplicación según el ambiente, basta con ejecutar la tarea de Gradle "build" con el parámetro "env" necesario, así por ejemplo para generar el del ambiente de pruebas sería "build -Denv=qa".

gradlew.bat build -Denv=qa
:compileJava UP-TO-DATE
:processResources
***********************************************************
Using environment: qa
***********************************************************
:classes
:jar
:startScripts
:distTar
:distZip
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

Total time: 7.064 secs

El JAR del proyecto (sin las dependencias externas) se generará en "build/libs", mientras que los archivos comprimidos en TAR y ZIP (según se requiera) con el JAR de la aplicación, las librerías externas y los scripts de ejecución estarán en "build/distributions".

Conclusiones

Como se pudo demostrar, aunque el filtrado de los recursos (archivos de configuración) no es algo que venga por defecto en las construcciones de Gradle (como sí lo es en parte en Maven), las propias características de Gradle permiten que sea muy fácil no solamente de adicionar sino también de personalizar, bien sea mediante una estructura de carpetas/archivos diferente, ambientes de despliegue según el proyecto o sobrescribiendo valores desde los parámetros, conservando una de las fortalezas de Gradle, la Construcción Incremental.

Esto quizás es un poco más de trabajo para quienes estamos acostumbrados a ejecutar (y depurar - debug) las aplicaciones desde el propio IDE, especialmente cuando este no tiene una integración entre Gradle y sus opciones nativas para ejecutar aplicaciones (como sí la tiene por ejemplo Android Studio). Sin embargo como se mostró en los posts sobre "Gradle Integration for Eclipse" y "Buildship", al menos en Eclipse no requiere tampoco de un mayor esfuerzo tener un ambiente de desarrollo con las ventajas tanto de Gradle como del IDE (en aquellos casos Eclipse).

Referencias

https://maven.apache.org/shared/maven-filtering
https://docs.gradle.org/current/userguide/working_with_files.html#N11189
https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Copy.html
https://dzone.com/articles/resource-filtering-gradle

Más sobre Gradle

http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html