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

No hay comentarios.:

Publicar un comentario