Desarrollo de microservicios web en Java que alegrarán

Desarrollo de microservicios web en Java que alegrarán

Spark es un marco compacto para crear aplicaciones web que se ejecutan en la JVM. Viene con un servidor web integrado, Jetty , para que pueda comenzar en minutos.

Después de agregar una dependencia en 

com.sparkjava:spark-core
, todo lo que necesita hacer es escribir el esqueleto de la aplicación y estará listo y funcionando.


import static spark.Spark.*;

public class JavaMagazineApplication {
  public static void main(String... args) {
    get("/hello", (req, res) -> "Hello World");
  }
}

Puede ver un par de cosas interesantes en este pequeño fragmento.

  • Spark aprovecha las interfaces funcionales, por lo que es fácil usar expresiones lambda para manejar una solicitud.
  • Spark no requiere anotaciones en un método para mapearlo en una ruta de solicitud. En cambio, le permite crear este mapeo de una manera programática utilizando un lenguaje limpio específico de dominio (DSL).
  • No se requiere un código repetitivo para iniciar una aplicación: todo está hecho para usted.

Antes de sumergirme, necesito limpiar un poco de polvo.

A medida que los microservicios se han convertido en un patrón arquitectónico omnipresente, ha habido un interés renovado en el tamaño de las aplicaciones implementadas y su tiempo de inicio. En los últimos años, Helidon, Micronaut, Quarkus y Spring Boot han entrado en este espacio. Pero el concepto de microframeworks es más antiguo que los nuevos chicos de la cuadra. Déjame presentarte Spark.

Si busca ese nombre en la web, es probable que encuentre información relacionada con Apache Spark , el motor de análisis, lanzado inicialmente en mayo de 2014 por Matei Zaharia . Eso es algo completamente diferente. Sácalo de tu mente.

Spark declara muchas interfaces funcionales , por lo que Spark requiere Java 8 como mínimo. Todos los ejemplos de código de este artículo se pueden encontrar en GitHub . Todas estas muestras requieren Java 11 para ejecutarse; Los escribí de esa manera para ser más conciso. Puede codificar todas las mismas funciones para Java 8.

Spark está maduro, la API es estable y no se actualiza cada dos semanas. Eso ocurre aproximadamente dos veces al año. Además, el proyecto no ha recibido mucha atención últimamente, lo que puede dar la impresión de que ha sido abandonado. (En el momento de escribir este artículo, la versión actual es Spark 2.9.3, lanzada en octubre de 2020. La anterior, 2.9.2, se lanzó en julio de 2020).

Conceptos principales de Spark

Entonces, sumergámonos y veamos qué sucede debajo del capó. Spark está diseñado en torno al concepto de rutas . Una ruta no es más que una función que toma una solicitud y respuesta HTTP y devuelve contenido en forma de 

Object
que se enviará de vuelta al cliente. Ya vio que la 
Route
interfaz declara un método, lo que lo hace perfectamente adecuado para escribir funciones lambda. Aunque esto le permite escribir rutas en una línea de código, también puede escribir rutas en clases separadas, lo que facilita su prueba. Usando una referencia de método a dicha ruta, aún puede tener una lista eficiente de todas las rutas en su aplicación.


import static spark.Spark.get;
import static spark.Spark.post;

public class JavaMagazineApplication {
  public static void main(String... args) {
    var controller = new GreetingController();
    get("/hello", controller::greet);
    post("/name", controller::updateName);
  }
}

El 

greet
método en el controlador devolvería el mensaje tradicional “Hola, mundo”. El 
updateName
método permitiría al usuario reemplazar “Mundo” por otro nombre emitiendo una solicitud HTTP, de la siguiente manera, y luego recuperar el saludo nuevamente:


curl --data 'name=Java Magazine' http://localhost:4567/hello/name
curl localhost:4567/hello/simple

El uso de este DSL concisa, Spark le permite registrar rutas para todos los verbos HTTP comunes, tales como 

get
post
put
, y 
delete
.

Cualquier aplicación útil, en algún momento, se ocuparía de la entrada del usuario. Es por eso que a 

Route
tiene acceso a 
Request
. Es una abstracción sobre la solicitud HTTP que envió el cliente. Por supuesto, proporciona acceso a cookies, encabezados, parámetros de consulta y otras formas de entrada del usuario. Dado que Spark se basa en Jetty, la 
Request
clase abstrae los detalles de bajo nivel de la 
HttpServletRequest
clase de la API de Servlet. Esto también significa que Spark le permite trabajar con atributos de solicitud y administración de sesiones. Compartiré más sobre eso más tarde.

Una ruta debe devolver un valor que se envía de vuelta al cliente. Pero, ¿cómo se 

Object
envía eso por cable? Es responsabilidad de un transformador de respuesta traducirlo 
Object
en una cadena de caracteres para la respuesta. Un transformador de respuesta puede, por ejemplo, serializar un objeto en una estructura JSON utilizando la biblioteca de serialización / deserialización de Google Gson .


import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import spark.ResponseTransformer;

public class JsonTransformer implements ResponseTransformer {
   
  private final Gson gson = new GsonBuilder().create();

  @Override
  public String render(Object model) {
    return gson.toJson( model );
  }
}

Un transformador de respuesta puede transformar una respuesta, pero ¿qué sucede si desea trabajar con la solicitud antes de que llegue a la ruta? Ahí es donde entran en juego los filtros . Spark invoca sus filtros antes o después de ejecutar la solicitud a través de la ruta.

Hasta ahora, solo ha visto enrutamiento estático, donde el URI de la solicitud debe coincidir exactamente con la ruta dada (como 

/hello
). Afortunadamente, Spark le permite definir rutas de una manera más flexible. Revisemos la aplicación de saludo y asegurémonos de que el usuario pueda proporcionar un nombre personalizado pasándolo como una variable de ruta, de la siguiente manera:


import static spark.Spark.get;
import static spark.Spark.post;

public class JavaMagazineApplication {
  public static void main(final String... args) {
    var controller = new GreetingController();
    get("/hello/simple/:name", controller::greet);
  }
}

Esto le permite personalizar el saludo.


curl localhost:4567/hello/simple/Java

Pero, ¿cómo puede implementar un 

Route
cuando desea acceder a los parámetros de ruta u otras entradas del cliente? Spark le proporciona acceso.


public Object greet(Request request, Response response) {
  return "Hello, " + request.params("name");
}

Los parámetros de ruta están disponibles a través del 

params
método en la 
Request
clase. De manera similar, los parámetros de consulta (como 
?name="Java"
) están disponibles a través del 
queryMap()
método en la misma clase.

¿Y si quisiera consumir datos JSON? Por supuesto, también es posible hacer eso. Y al igual que con la producción de JSON, no es algo que Spark pueda hacer de inmediato, lo que le brinda la mayor flexibilidad para la forma en que desea hacerlo.

La forma en que normalmente lo hago es doble. Primero, tengo un método de fábrica que produce un filtro. Y segundo, configuro Spark para que lo ejecute antes de invocar una ruta en particular.

El método de fábrica tiene el siguiente aspecto:


public static Filter forType(final Class<?> type) {
  return (request, response) -> {
    var body = gson.fromJson(request.body(), type);
    request.attribute(REQUEST_DATA, body);
  };
}

Para registrar ese método con Spark, utilizo el siguiente fragmento:


before("/hello/complex", "application/json",
    JsonParsingFilter.forType(GreetingInput.class));

Por supuesto, el 

REQUEST_DATA
que uso en la 
Filter
fábrica es una constante que se puede usar para recuperar el objeto dentro de un 
Route
.


var body = (GreetingInput) request.attribute(REQUEST_DATA);

Esto puede ser un poco más complicado que lanzar una anotación sobre un método, pero le brinda un control muy detallado sobre exactamente cómo y cuándo se procesa el cuerpo de la solicitud.

También es posible escribir un poco más de código que este y hacer que el filtro inspeccione las anotaciones según el método de ruta. En ese caso, solo habría una instancia de ese filtro en tiempo de ejecución, en lugar de una instancia por ruta. Por otro lado, el filtro se volvería mucho más complejo al inspeccionar esas anotaciones.

Empaquetar una aplicación para su implementación

Ejecutar una aplicación desde un IDE acogedor está muy bien, pero no es así como ejecutar aplicaciones en producción. Entonces, ¿cómo empaquetarías una aplicación Spark? Puede optar por empaquetarlo para su implementación en un contenedor de servlets existente o en una aplicación independiente.

Despliegue en un contenedor existente. Recuerde que Spark incluye Jetty. Si desea implementar su aplicación en un contenedor de servlets existente, no tendría sentido empaquetar Jetty con él. De hecho, un Jetty empaquetado ni siquiera funcionaría, porque su contenedor de servlets no invocará la 

main()
función escrita anteriormente.

En su lugar, debe reescribir la clase de aplicación para implementar la 

SparkApplication
interfaz, cuyo 
init()
método es el lugar perfecto para declarar filtros, rutas y transformadores de respuesta. El 
destroy
método de esa interfaz es adecuado para limpiar cualquier recurso.

Además, debe cambiar la compilación para generar un archivo WAR en lugar de un archivo JAR, y debe excluir Jetty del paquete de la aplicación. Suponiendo que está usando Maven, así es como lo hace.


<packaging>war</packaging>

<dependencies>
  <dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-core</artifactId>
    <version>2.9.3</version>
    <exclusions>
      <!-- remove Jetty from the packaged application -->
      <exclusion>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-server</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-webapp</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.eclipse.jetty.websocket</groupId>
        <artifactId>websocket-server</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.eclipse.jetty.websocket</groupId>
        <artifactId>websocket-servlet</artifactId>
      </exclusion>
    </exclusions>
   </dependency>
</dependencies>

Implementación en un JAR independiente. Aquí hay dos opciones: un JAR grueso o un JAR delgado.

El enfoque de Fat JAR agrupa todas las clases, recursos y dependencias de terceros en un solo archivo JAR. Este es el enfoque que adopta Spring Boot, por ejemplo. Puede resultar en archivos JAR de gran tamaño, especialmente a medida que crecen sus dependencias.

Pero en la era de los contenedores, tiene sentido considerar el enfoque de JAR delgado. En este enfoque, crea un archivo JAR tradicional con solo las clases y recursos de su propia aplicación. Para empaquetar, copie todas las dependencias de terceros en una carpeta cercana al archivo JAR, como 

lib/
, y agregue una línea en el archivo de manifiesto del JAR que le indique a la JVM que agregue todas las dependencias a la ruta de clases y las resuelva desde esa 
lib/
carpeta, de la siguiente manera :


<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.0.2</version>
  <configuration>
    <archive>
      <manifest>
        <addClasspath>true</addClasspath>
        <classpathPrefix>lib/</classpathPrefix>
        <mainClass>it.mulders.spark.JavaMagazineApplication</mainClass>
      </manifest>
    </archive>
  </configuration>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-dependency-plugin</artifactId>
  <version>3.1.2</version>
  <configuration>
    <overWriteReleases>false</overWriteReleases>
    <includeScope>runtime</includeScope>
    <outputDirectory>${project.build.directory}/lib</outputDirectory>
  </configuration>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>copy-dependencies</goal>
      </goals>
    </execution>
  </executions>
</plugin>

¿Por qué es esto relevante en la era de los contenedores? Considere el siguiente fragmento de un Dockerfile :


# Add Maven dependencies (not shaded into the artifact; Docker-cached)
ADD target/lib /opt/my-application/lib
# Add the service itself
ADD target/my-application.jar /opt/my-application/ my-application.jar

Este enfoque aprovecha el sistema de archivos en capas que usa Docker . Es probable que los componentes de terceros no cambien con tanta frecuencia como la aplicación. Al colocar esos archivos en una subcarpeta y escribirlos a la vez en una capa del sistema de archivos de Docker, permite que el motor del contenedor reutilice esa capa cada vez que compila la aplicación. La capa final del contenedor Docker, con la aplicación en él, puede cambiar muchas veces, pero es pequeña: tal vez un par de KB. Eso significa que el contenedor se puede construir y desplegar rápidamente, lo que definitivamente es una victoria.

Comenzar rápido y mantenerse pequeño

Puede esperar que el tamaño de una aplicación Spark empaquetada sea pequeño. Y, de hecho, una aplicación Spark empaquetada “pesa” alrededor de 4 MB. Pero esto también beneficia el tiempo de inicio y el uso de memoria de las aplicaciones. La Tabla 1 compara Spark con algunos otros marcos.

Tabla 1. Comparación de Spark y otros marcos

Comparación de Spark y otros marcos

Tabla 1. Comparación de Spark y otros marcos

* La aplicación Micronaut se generó usando micronaut.io/launch/ con Micronaut 2.3.1.

** La aplicación Spring Boot se generó usando start.spring.io con Spring Boot 2.4.2.

Medí esos números en una MacBook Pro 2018. Para el tiempo de inicio, tomé el resultado del registro que genera cada aplicación. Medí el uso de memoria usando la salida del conjunto residente de la 

ps
herramienta de línea de comandos. Hice un promedio de todos los números en cinco medidas.

No he usado GraalVM para crear ejecutables nativos para esos marcos que lo admiten. Aunque hay muchas cosas en esta configuración de prueba ingenua que podría debatir, puede ver que el enfoque minimalista que toma Spark vale la pena. Tener un tiempo de inicio extremadamente corto y un uso de memoria bajo hace que Spark sea una opción interesante para entornos que exigen un uso bajo de recursos o un alto rendimiento.

He empleado Spark en algunos entornos de rendimiento crítico por esa razón, y medí los tiempos de respuesta en un punto final REST que apenas excedía el tiempo para realizar las consultas de base de datos necesarias; la sobrecarga del marco web era insignificante.

Spark y DESCANSO

Hasta ahora, he cubierto aplicaciones con una interfaz HTTP, asumiendo que sería JSON lo que envía y recibe por cable. Pero Spark puede hacer más. Puede agregar un motor de plantilla de su elección y comenzar a renderizar páginas web completas directamente desde Spark. Hay muchas opciones, que incluyen (pero no se limitan a) FreeMarker , Handlebars , Thymeleaf y Velocity . Esos motores son dependencias independientes que deberá agregar a su aplicación.

Como ejemplo, veamos cómo incrustar Thymeleaf. Después de agregar las dependencias adecuadas, puede actualizar sus definiciones de ruta de la siguiente manera:


import static spark.Spark.get;
import static spark.Spark.post;

public class JavaMagazineApplication {
  public static void main(final String... args) {
    var controller = new GreetingController();
    var thymeleaf = new ThymeleafTemplateEngine();
    get("/hello/html/:name", controller::greet, thymeleaf::render);
  }
}

Esto requiere que también actualice un poco el controlador. En lugar de devolver texto para mostrar, debe devolver un 

ModelAndView
objeto.


import spark.ModelAndView;
import spark.Request;
import spark.Response;

import java.util.Map;

public class GreetingController {
  public Object greet(Request request, Response response) {
    var model = Map.of("name", request.params("name"));
    return new ModelAndView(model, "views/welcome");
  }
}

También necesita escribir una vista usando Thymeleaf.


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
  <title>Simple Spark web application</title>
</head>

<body>

<div>
  <h1 th:text="'Welcome to Spark, ' + ${name}">
    Welcome to Spark, unknown
  </h1>
</div>

</body>

</html>

Está más allá del alcance de este artículo explicar todos los detalles de Thymeleaf. Por ahora, tenga en cuenta que el texto dentro de la 

h1
etiqueta será procesado por la aplicación de prueba usando la 
name
clave del modelo.

Conclusión

Como se mencionó anteriormente, los ejemplos de código están disponibles en este GitHub y requieren Java 11 para ejecutarse.

Spark no es una caja de herramientas completa y no pretende serlo. Más bien, debería ser una de las herramientas dentro de su caja de herramientas, y usted, como desarrollador, podrá encontrar las otras herramientas para completar su caja de herramientas.

He usado Spark en algunos proyectos que tenían requisitos específicos para tiempos de respuesta. En esas situaciones, su tamaño y el hecho de que Spark no usa mucho la reflexión lo convierten en una opción interesante.

El código Spark es muy legible y se presta bien para extensiones que el marco Spark no cubre.

Por otro lado, si su principal preocupación es ofrecer funciones rápidamente, tener que conectar herramientas y componentes juntos puede, de hecho, ralentizarlo. En tales escenarios, tener una caja de herramientas precargada con herramientas sólidas puede permitirle entregar funciones más rápido.

Fuente: https://blogs.oracle.com/javamagazine/java-spark-web-microservices