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

1 comentario: