Mostrando las entradas con la etiqueta Jersey. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Jersey. Mostrar todas las entradas

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

domingo, 15 de noviembre de 2015

API REST usando Jersey y Jetty Embebido

Introducción

Van casi 10 años desde que Google decidió cambiar Apache Tomcat por Jetty para su App Engine. Mas que un cambio de un contenedor de Servlets por otro fue el cambio de paradigma, justo como lo dice el slogan de Jetty: "Don't deploy your application in Jetty, deploy Jetty in your application!".

Esto quiere decir que en lugar de desplegar una aplicación Web (generalmente un archivo WAR) en un contenedor previamente instalado y configurado, es el contenedor el que se incluye y se configura en la aplicación. Esto permite que la aplicación pueda copiarse en cualquier servidor (un archivo JAR) y simplemente con ejecutarla tener una réplica de la aplicación totalmente funcional, facilitando la escalabilidad horizontal.

Actualmente existen otros contenedores embebidos como Undertow o el propio Apache Tomcat (desde la versión 7 existe la opción Embedded), sin embargo Jetty sigue siendo muy popular y es por eso que en este post se mostrará cómo se puede iniciar un proyecto Java para un API RESTful usando Jersey 2  y Jetty como contenedor embebido.

Dependencias y Configuración Gradle

Nota: quienes aún no conocen del todo Gradle pueden revisar: http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html

La manera más fácil de tener un proyecto con Jersey y Jetty es usando la librería "jersey-container-jetty-servlet", la cual incluye todas las dependencias necesarias y además ofrece clases adicionales para facilitar la inicialización de la aplicación. Por ejemplo, este sería el código necesario para iniciar la aplicación:

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

import java.net.URI;
import javax.ws.rs.core.UriBuilder;
import org.eclipse.jetty.server.Server;
import org.glassfish.jersey.jetty.JettyHttpContainerFactory;
import org.glassfish.jersey.server.ResourceConfig;

public class ExampleStarter {

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

  URI baseUri = UriBuilder.fromUri("http://0.0.0.0/").port(8080).build();

  ResourceConfig config = new ResourceConfig();
  config.packages("com.blogspot.nombre_temp.jetty.jersey.example.resource");

  final Server jettyServer = JettyHttpContainerFactory.createServer(baseUri, config, false);

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

Sin embargo esto viene con un precio y es que se pierde algo de control sobre la configuración del servidor, así como de las dependencias que se tienen. Por ejemplo, la versión más reciente de "jersey-container-jetty-servlet" al momento de escribir este post es "2.22.1", pero esta incluye Jetty "9.1.1.v20140108", siendo "9.3.5.v20151012" la más reciente (más de un año de diferencia).

Es por esto que para este ejemplo se tendrán separadas las dependencias de Jetty y Jersey, facilitando un eventual cambio o migración en cualquiera de estas.

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'

sourceCompatibility = 1.8
targetCompatibility = 1.8

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

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

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

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"
}

En este caso la aplicación se compilará en un archivo JAR, como cualquier otra aplicación ejecutable Java por lo cual se incluyen los plugins "java" y "application" que se vieron en un post anterior. El plugin "release" no es necesario, pero se incluye para facilitar el versionamiento desde el principio, puede considerarse una práctica personal si se quiere.

Las dependencias "jetty-server" y "jetty-servlet" son las necesarias para Jetty, mientras que "jersey-server" y "jersey-container-servlet" son las requeridas para un proyecto que haga las veces de servidor usando Jersey (diferentes a las que se usarían en un proyecto cliente).

Adicionalmente se incluye "jersey-media-json-jackson", para que se puedan recibir y responder peticiones usando JSON. En Jersey 1.x era necesario adicionar la propiedad "com.sun.jersey.api.json.POJOMappingFeature", sin embargo en Jersey 2.x sólo se requiere adicionar esta librería.

Iniciando la Aplicación

El punto de inicio de la aplicación será un método "main", el cual tendrá el siguiente código:
package com.blogspot.nombre_temp.jetty.jersey.example;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.servlet.ServletContainer;

public class ExampleStarter {

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

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

        Server jettyServer = new Server(8080);
        jettyServer.setHandler(contextHandler);

        ServletHolder jerseyServlet = contextHandler.addServlet(ServletContainer.class, "/*");
        jerseyServlet.setInitOrder(0);
        jerseyServlet.setInitParameter(ServerProperties.PROVIDER_PACKAGES, "com.blogspot.nombre_temp.jetty.jersey.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();
        }
    }
}

A continuación una pequeña explicación por líneas:
  • Líneas 14-15: Se indica que la aplicación responderá desde la raíz (por ejemplo http://localhost:8080) y no se crearán sesiones HTTP, las cuales no son necesarias para este caso. Cabe anotar que puede dejarse un valor distinto para "ContextPath", por ejemplo "/api" indicaría que la aplicación respondería desde http://localhost:8080/api.
    Nota: un Handler es un componente de Jetty que se encarga de recibir y procesar las peticiones HTTP.
  • Línea 17: El puerto de la aplicación será 8080.
  • Líneas 20-22: Se adiciona el Servlet de Jersey que recibirá las peticiones y las llevará a las clases que se desarrollen en el proyecto (resources). Se indica también que dichas clases estarán en el paquete "com.blogspot.nombre_temp.jetty.jersey.example.resource" para que Jersey sepa en donde buscarlas.
  • Línea 25: Se inicia el servidor.
  • Líneas 27-39: Este fragmento es realmente opcional, pero se incluye para indicar cómo es posible tener un código que se ejecuta cuando se baja el servidor. Dentro de este hilo es posible liberar recursos de manera ordenada, como por ejemplo conexiones a bases de datos u otros servidores.
  • Línea 41: Detiene el hilo principal de la aplicación (main) mientras Jetty esté funcionando. Esto con el fin de evitar que por ejemplo se afecte el ciclo de vida de Jetty si el hilo principal termina de ejecutarse antes de que Jetty inicie por completo.

Un ejemplo de configuración un poco más avanzada puede ser el siguiente:

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

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;

public class ExampleStarter {

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

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

        QueuedThreadPool queuedThreadPool = new QueuedThreadPool(10, 1);
        final Server jettyServer = new Server(queuedThreadPool);

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

        ServerConnector serverConnector = new ServerConnector(jettyServer, acceptors, -1);
        serverConnector.setPort(8080);
        serverConnector.setAcceptQueueSize(10);

        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.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();
        }
    }
}

  • Líneas 19 y 20: Aquí se indica que Jetty tendrá mínimo un hilo y máximo 10 para procesar peticiones, para evitar generar demasiados hilos o tener muy pocos para atender las peticiones (en un servidor de producción posiblemente se necesitarían más hilos). Por defecto el límite es 200.
  • Líneas 22 y 26: Se usará el mismo número de núcleos de la CPU como hilos "acceptors" en Jetty para recibir las peticiones HTTP. Por defecto Jetty ya intenta estimar este número según los núcleos limitando a 4, pero en servicios con alta demanda puede que se necesite un número mayor, aunque no se recomienda tener más hilos "acceptors" que núcleos. También se indica que se tendrá una cola de 10 posiciones (acceptQueueSize), es decir un máximo de 10 peticiones en espera mientras los hilos "acceptors" están ocupados.


Por lo general las configuraciones por defecto funcionan bien, pero en caso de tener un tráfico muy alto de peticiones (o una limitación en el número de hilos) se recomienda leer la documentación de Jetty con respecto al tema: http://www.eclipse.org/jetty/documentation/current/high-load.html

Resource

Para mantener el ejemplo sencillo sólo se tendrá una clase Resource con un método, el cual indicará que la aplicación se encuentra funcionando correctamente, algo típico cuando se cuenta con una herramienta de monitoreo de aplicaciones, aunque en este caso no se retornará la clásica cadena "OK", sino un JSON con el estado para demostrar que este proyecto ya tiene soporte para JSON.

package com.blogspot.nombre_temp.jetty.jersey.example.model;

public class Status {

    private String value = "OK";

    public String getValue() {
        return value;
    }
}


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

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import com.blogspot.nombre_temp.jetty.jersey.example.model.Status;

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

    @GET
    public Status health() {
        return new Status();
    }
}

Ejecutando la Aplicación

Si se está usando un IDE, es posible iniciar la aplicación ejecutando el método "main", sin embargo al detener la aplicación es posible que no se ejecute el código de finalización, ya que los IDE terminan la ejecución del proceso directamente (kill).

Para ver que se imprima el texto "Stopping!" que se puso al finalizar la aplicación se tienen dos alternativas para ejecutar la aplicación:

1) Ejecutar desde la línea de comandos (terminal) la tarea de gradle "run":

./gradlew run
:compileJava
:processResources
:classes
:run
Starting!
2015-11-14 00:58:59.394:INFO::main: Logging initialized @259ms
2015-11-14 00:58:59.493:INFO:oejs.Server:main: jetty-9.3.5.v20151012
2015-11-14 00:59:00.263:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@6f43c82{/,null,AVAILABLE}
2015-11-14 00:59:00.345:INFO:oejs.ServerConnector:main: Started ServerConnector@3e2055d6{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2015-11-14 00:59:00.346:INFO:oejs.Server:main: Started @1215ms

2) Generar los archivos para distribuir la aplicación ejecutando la tarea de gradle "build".

./gradlew 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: 7.843 secs

Al terminar, en la carpeta "build/distributions" se tendrán los archivos .zip y .tar con todos los archivos necesarios para distribuir y ejecutar la aplicación. Se puede descomprimir cualquiera de los dos y dentro de la subcarpeta "bin" se tendrán los archivos para ejecutar la aplicación. Al ejecutar el que corresponda al sistema operativo usado se tendrá lo siguiente en consola/terminal:

./jetty-jersey-example
Starting!
2015-11-09 00:04:10.030:INFO::main: Logging initialized @173ms
2015-11-09 00:04:10.087:INFO:oejs.Server:main: jetty-9.3.5.v20151012
2015-11-09 00:04:10.775:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@5340477f{/,null,AVAILABLE}
2015-11-09 00:04:10.861:INFO:oejs.ServerConnector:main: Started ServerConnector@7d9f158f{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2015-11-09 00:04:10.862:INFO:oejs.Server:main: Started @1007ms

Al abrir un navegado Web (o ejecutar un comando que procese peticiones HTTP) e ingresar la URL "http://localhost:8080/health" la respuesta debe ser el texto: {"status":"OK"}


Figura 1 - Aplicación en el navegador

Finalmente, para terminar la ejecución de la aplicación se debe regresar a la consola/terminal y presionar ctrl + c (o cmd + c en Mac) y se podrá ver que se imprime "Stopping!", como se indicó en el ShutdownHook:

Stopping!
2015-11-09 00:09:01.390:INFO:oejs.ServerConnector:Thread-7: Stopped ServerConnector@7d9f158f{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2015-11-09 00:09:01.408:INFO:oejsh.ContextHandler:Thread-7: Stopped o.e.j.s.ServletContextHandler@5340477f{/,null,UNAVAILABLE}

El código completo de la aplicación podrá descargarse desde: https://github.com/guillermo-varela/jetty-jersey-example

Aplicaciones Web

En próximos posts se mostrará también como es posible desarrollar aplicaciones web (JSP y Serlvets) mediante los plugins Jetty y Gretty de Gradle.

Referencias

http://www.eclipse.org/jetty/documentation
https://jersey.java.net

lunes, 19 de enero de 2015

Cambiar el estado de una aplicación Web

Introducción

Una aplicación Web generalmente debe disponer de algún mecanismo que permita monitorear su funcionamiento y detectar si se ha presentado un problema para su corrección lo antes posible.

Dependiendo del lenguaje y framework usado se tienen herramientas que permiten una gestión más avanzadas que otras, por ejemplo JMX en el caso de Java o GOD para Ruby on Rails.

Sin embargo una de las prácticas más comunes en las aplicaciones Web, independiente del lenguaje/framework, es tener una URL a la cual se pueda realizar una petición HTTP GET y la respuesta sea texto plano con una palabra como "OK", "UP", etc. si todo está funcionando correctamente. Esta práctica se conoce comúnmente como "Health Check" (o "Health Endpoint Monitoring Pattern" en la literatura de Microsoft) y es por eso que generalmente tienen la forma http://dominio/health.

Es una práctica muy popular no solamente por lo sencilla que puede ser de implementar e integrara en herramientas de monitoreo como New Relic o Pingdom, sino porque también es muy usada para otros propósitos, como por ejemplo en balanceadores de carga para determinar a que nodo se pueden enviar las peticiones (Amazon Route 53 entre otros).

Problema a tratar

No siempre es el caso, pero en muchas ocasiones lo que se hace para estos "Health Check" es que la URL siempre retorne el texto de éxito, con lo cual la herramienta de monitoreo o el balanceador solamente detectaría el problema cuando la aplicación Web deje de responder peticiones HTTP del todo.

Para los casos más básicos de monitoreo esto puede ser suficiente, pero existen situaciones en los cuales se requiere que la aplicación (o el nodo de la aplicación) deje de funcionar, bien sea por una migración, despliegue, etc. Algunos balanceadores de carga permiten indicar que deje se enviar peticiones a dicha instancia, pero ¿Qué tal si el balanceador no lo soporta? ¿Qué tal si la aplicación no solamente recibe peticiones HTTP del balanceador, sino que tiene procesos automáticos internos (cron jobs mediante Quartz por ejemplo)?

En ciertas ocasiones lo que se hace es esperar una ventana de tiempo en la cual no hay tareas automáticas programadas y estar pendientes de los logs de la aplicación, esperando un momento en el cual no se esté procesando nada para bajarla o reiniciarla manualmente, sin embargo esto puede generar errores por cuestiones de milisegundos y en algunos contextos ello puede ser muy perjudicial.

Lo que se desarrollará

En este post se mostrará no solamente cómo desarrollar una aplicación Web con una URL de "Health Check", sino también cómo indicar que está activa o inactiva manualmente. Aunque se usará Java y Jersey la idea es aplicable a cualquier otro lenguaje/framework.
  • http://localhost:8080/health: Un HTTP GET a esta URL retornará el estado actual de la aplicación, siendo:
    • UP: La aplicación está funcionando correctamente.
    • DOWN: La aplicación no debe procesar ninguna acción.
  • http://localhost:8080/toggleStatus: Un HTTP PUT a esta URL (mediante herramientas como cURL o Postman) cambiará el estado de la aplicación, pasando de "UP" a "DOWN" y viceversa.
El código fuente completo está disponible en: https://github.com/guillermo-varela/status-changer

Aplicación Web con Java

Paso 1: Health Check Endpoint
Para empezar se desarrollará la clase que recibirá las peticiones HTTP GET, con las anotaciones de Jersey, retornando siempre el texto "UP", indicando que la aplicación funciona normalmente.

package com.blogspot.nombre_temp.resource;

import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

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

    /**
     * Gets the current status of this application instance.
     * 
     * @return Current status of this application instance.
     */
    @GET
    public String health() {
        return "UP";
    }
}

La siguiente clase simplemente indica el/los paquetes en los que estarán los recursos RESTful que se expondrán a través de Jersey.
Nota: Esto requiere requiere soporte de Servlets 3

package com.blogspot.nombre_temp.config;
import javax.ws.rs.ApplicationPath;

import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/")
public class WebApplicationConfig extends ResourceConfig {

    public WebApplicationConfig() {
        packages("com.blogspot.nombre_temp.resource");
    }
}
Si se despliega la aplicación en un contenedor de Servlets 3 (Tomcat 7+ por ejemplo), suponiendo que el nombre del proyecto sea "status-changer", al acceder a la URL http://localhost:8080/status-changer/health el resultado sería simplemente el texto "UP", como es de esperarse.

Figura 1 - Navegador con el estado actual

Paso 2: Contenedor de Estado
Como primer paso para hacer que el estado pueda cambiarse se requiere un mecanismo que permita almacenar el estado actual y consultarlo, no solamente desde la clase que expone el servicio "/health" sino también a componentes internos de la aplicación (como los ya mencionados cron jobs).

Para mantener el ejemplo sencillo, el estado se almacenará en memoria, mediante un atributo estático de una clase, cuyo valor podrá ser accedido por cualquier otro componente de la aplicación, en cualquier hilo.

package com.blogspot.nombre_temp.util;

public class StatusHolder {

    private static volatile Status currentStatus = Status.UP;

    public static enum Status {
        UP, DOWN;
    }

    public static Status getCurrentStatus() {
        return currentStatus;
    }

    public static synchronized void setCurrentStatus(Status currentStatus) {
        StatusHolder.currentStatus = currentStatus;
    }

    /**
     * Allows changing the current status of this application instance.
     * 
     * @return Current status after the change.
     */
    public static synchronized Status toggleStatus() {
        if (currentStatus == Status.UP) {
            currentStatus = Status.DOWN;
        } else {
            currentStatus = Status.UP;
        }
        return currentStatus;
    }
}

El atributo "currentStatus" se inicializa con el valor "UP" indicando que por defecto la aplicación estará en capacidad de procesar peticiones, es estática para que su valor sea el mismo para toda la clase y usa el modificador "volatile" para garantizar que su valor es leído de manera consistente en ambientes concurrentes (multi-thread).

El enum "Status" garantiza que el estado de la aplicación solamente podrá tener dos valores: "UP" y "DOWN".

Los métodos que modifican el valor de "currentStatus" son estáticos y synchronized, para garantizar que solamente un hilo a la vez podrá editar este valor.

Paso 3: Estado dinámico externamente
Ahora que se tiene como acceder y modificar el estado actual de la aplicación, se deben exponer estas funcionalidades en el API RESTful que se inició en el paso 1, haciendo uso de la nueva clase "StatusHolder".

package com.blogspot.nombre_temp.resource;

import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import com.blogspot.nombre_temp.util.StatusHolder;

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

    /**
     * Gets the current status of this application instance.
     * 
     * @return Current status of this application instance.
     */
    @GET
    public String health() {
        return StatusHolder.getCurrentStatus().name();
    }

    /**
     * Allows changing the current status of this application instance.
     * 
     * @return Current status after the change.
     */
    @PUT
    @Path("/toggleStatus")
    public String toggleStatus() {
        return StatusHolder.toggleStatus().name();
      }
}

El nuevo método "toggleStatus" permite acceder a la URL http://localhost:8080/status-changer/health/toggleStatus mediante una petición HTTP PUT lo cual cambiará el estado de la aplicación, como se indicó en la sección "Lo que se desarrollará", permitiendo que cualquiera que invoque el método "StatusHolder.getCurrentStatus()" obtenga el estado actualizado de la aplicación, como ahora es el caso del método "health".
Figura 2 - Cambiando el estado

Para el caso de otros componentes, como las tareas automáticas, dado que el estado se almacena de manera pública y estática, bastaría con poner una condición al inicio que impida su ejecución si "StatusHolder.getCurrentStatus()" retorna "DOWN".

Extra: Autenticación
La URL que permite el cambio de estado de la aplicación no debería ser pública, aún dentro de la misma organización, así que como paso adicional se mostrará cómo puede adicionarse autenticación.

La idea es mantener el ejemplo lo más sencillo posible, así que se usará la autenticación proporcionada por el propio contenedor de Servlets, la cual puede ser integrada fácilmente con Jersey.

Quienes requieran un sistema de autenticación más avanzados, pueden revisar las Recursos Adicionales al final del post.

Para empezar, dado que las anotaciones de Servlets 3 para seguridad solamente cubren el uso de Serlvets, será necesario crear un archivo "web.xml" el cual se indique el acceso a la URL "/health/toggleStatus" solamente estará permitido a los usuarios con rol "admin" y se requerirá autenticación tipo HTTP Basic.

<web-app version="3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

 <display-name>status-changer</display-name>

 <security-constraint>
  <web-resource-collection>
   <web-resource-name>Change Current Status</web-resource-name>
   <url-pattern>/health/toggleStatus</url-pattern>
  </web-resource-collection>
  <auth-constraint>
   <role-name>admin</role-name>
  </auth-constraint>
 </security-constraint>

 <security-role>
  <role-name>admin</role-name>
 </security-role>

 <login-config>
  <auth-method>BASIC</auth-method>
 </login-config>
</web-app>

En caso de usar Tomcat, la información de los usuarios se almacena por defecto en el archivo "$CATALINA_BASE/conf/tomcat-users.xml" y se pueden indicar estos datos a manera de ejemplo:

<role rolename="admin"/>
<role rolename="user"/>
<user username="admin1" password="admin1" roles="admin"/>
<user username="user1" password="user1" roles="user"/>

Al tratar de cambiar el estado como "user1", se obtendrá se rechazará la petición.
Figura 3 - Cambio de estado prohibido

Nota: En caso de usar una instancia de Tomcat a través del IDE Eclipse, no debe editarse directamente el archivo "tomcat-users.xml" directamente en la carpeta de instalación de Tomcat ni en la carpeta del workspace actual (temp0, temp1, dependiendo de cuantos servidores se tenga), sino que se debe editar el archivo que aparece dentro de la vista "Project Explorer" en el proyecto "Servers". Esto debido a que Eclipse sobrescribirá lo que se ponga en la capeta del workspace con lo que se tenga en "Servers":
Figura 4 - Archivo de usuarios de Tomcat en Eclipse

Recursos Adicionales