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
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
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
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
Cualquier aplicación útil, en algún momento, se ocuparía de la entrada del usuario. Es por eso que a
Una ruta debe devolver un valor que se envía de vuelta al cliente. Pero, ¿cómo se
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
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
public Object greet(Request request, Response response) {
return "Hello, " + request.params("name");
}
Los parámetros de ruta están disponibles a través del
¿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
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
En su lugar, debe reescribir la clase de aplicación para implementar la
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
<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
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
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
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
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