Tutorial: Vertx3 + Kotlin para montar un API REST por persistencia JDBC
Éste es un tutorial para desarrolladores de nivel principiante e intermedio que deseen una rápida inmersión en la programación asíncrona con Vertx y Kotlin.
Artículo original en http://maballesteros.com/articles/vertx3-kotlin-rest-jdbc-tutorial/
Requisitos
Para el tutorial necesitarás tener instalado Java, Maven, Git, y se aconseja el uso de IntelliJ para trabajar con Kotlin.
Descargando el código
El código del tutorial está disponible en GitHub, así que crea un directorio de trabajo y clona el proyecto:
Después de clonar el proyecto, abre el pom.xml
que encontrarás en el directorio raíz en IntelliJ. Es un multiproyecto Maven, con un módulo (subproyecto) para cada paso del tutorial.
Paso 1: Arrancar un servidor HTTP sencillo con Vertx
En este primer paso simplemente vamos a mostrar lo rápido y sencillo que es tener funcionando un servidor HTTP asíncrono usando Vertx y Kotlin… incluso sin agregar azucar Kotlin.
En primer lugar, obtenermos una instancia de Vertx
, y creamos con ella un HttpServer
. El server no ha arrancado todavía, por lo que podemos configurarlo hasta adecuarlo a nuestras necesidades. En este caso, simplemente manejamos el enrutado a GET /
y retornamos un clásico Hello world!
.
Paso 2: Repositorio REST de usuarios in-memory
En el segundo paso definimos un repositorio de usuarios sencillo con el siguiente API:
Esto es, tenemos usuarios User
y un servicio con las operaciones get
, add
, y remove
para obtener, agregar, y eliminar usuarios respectivamente.
Notar que estamos en programación asíncrona, así que no podemos retornar directamente User
o Unit
. En su lugar, debemos debemos suministrar algún tipo de callback o retornar un resultado futuro Future<T>
que nos permita registrarnos a resultados satisfactorios o fallidos, cuando estos ocurran.
En este paso implementaremos este servicio con un Map
mutable (un HashMap
Java):
Para exponer el servicio vía REST, tendremos que mapear las operaciones a sus correspondientes GET
, POST
, y DELETE
.
Destacar:
- la llamada a
router.route().handler(BodyHandler.create())
, que que queremos poder obtener el cuerpo de la request como unaString
. - el uso de
Gson
para codificar / decodificar JSON - como nos suscribimos a un resultado futuro (
future.setHandler
)
Paso 3: Repositorio REST de usuarios in-memory (con definiciones REST simplificadas)
En el tercer paso sólo simplificaremos las definiciones REST. En un proyecto real pasamos tiempo mapeando servicios de negocio a endpoints REST, por lo que cuanto más sencillo sea esto mejor.
Comparemos dos ejemplos de código. El primero es del paso 2, y el segundo es lo que querríamos conseguir:
Queremos quitarnos de encima código verboso como router.
, .handler { ctx ->
, y ctx.request().getParam()
. Este código sólo ofusca/complica lo que estamos tratando de expresar en las definiciones del API REST. En este caso sencillo puede no parecer importante, pero cuando tenemos muchos paquetes de negocio, con muchos endpoints cada uno, esto cobra una gran relevancio. Así, cuanto más sencillas sean las definiciones, tanto más fácil será el mantenimiento del código.
¿Cómo logramos limpiar y transformar el código para que sea mucho más expresivo? Por su puesto, con azucar Kotlin para definir DSL (Domain Specific Languages). Puedes encontrar la idea clave en la entrada Type Safe Builders del sitio principal de Kotlin. Usando esas ideas, definimos los siguientes métodos de extensión:
Paso 4: Repositorio REST de usuarios con persistencia JDBC
En el cuarto paso agregamos persistencia JDBC. En este caso sí que vamos a agregar directamente código de insfraestructura para mantener el código simple.
Veamos la implentación del servicio usando JDBC:
¿Fácil no? Notar que debemos suministrar un cliente JDBCClient
en el momento de la construcción. Aquí está el código que agregamos en el main()
del proyecto para construir el client JDBC y conectarlo a una BBDD real:
En este tutorial emplearemos hsqldb, una base de datos Java usada con frecuencia para el testing de capas de acceso a BBDD, ya que proporciona una implementación in-memory muy útil para este objetivo.
El soporte de Vertx para JDBC no dispone de APIs tan simples como las que hemos mostrado. Los que ya conocéis JDBC, encontraréis que son similares a las primitivas básicas de JDBC, pero en asíncrono. Nuevamente, haremos uso de algunos métodos de extensión de Kotlin y algo de programación funcional para mantener las cosas simples (ver db_utils.kt).
Paso 5: Repositorio REST de usuarios con persistencia JDBC (con Promesas y más azucar Kotlin)
En el quinto paso agregamos más código de insfraestructura para simplificar más todavía, y lograr así que la bestia escale mejor cuando queramos agregar complejidad.
En ejemplos anteriores hemos usado el tipo Future<T>
proporcionado por Vertx. Éste proporciona un mecanismo familiar para suscribirnos a resultados futuros, de forma que cuando estén disponibles, podamos preguntarle si fue un éxito o falló y tomar acciones adicionales.
Pero el tipo Future<T>
carece de algunas características importantes para escalar los ejemplos sencillos que mostramos en el tutorial a algo más grande:
- Capacidad para componerse: no podemos encadenar tipos
Future<T>
, de forma que cuando termine uno empiece otro, etc. - Sincronización: no podemos esperar a que terminen varios futuros, y actuar cuando termine el último (sea cual sea).
Bueno, no estoy siendo del todo justo: sí que se puede… pero con un montón de código verboso y redundante que termina siendo inmanejable.
Entonces, ¿cuál es la alternativa? El patrón Promesa resuelve todo esto, y es el estándar de facto para manejar código asíncrono.
Primero necesitamos una implementación del patrón Promesa en Kotlin que enganche con el event loop de Vertx.
Podemos entonces redefinir nuestro código sobre este patróna asíncrono. Empecemos por redefinir el API del servicio (fácil, basta cambiar Future<T>
por Promise<T>
):
La implementación del servicio JDBC es también muy parecida. Notar el cambio en el método init()
, donde empezamos a usar las operaciones de composición .pipe()
y .then()
para encadenar acciones asíncronas de forma muy semántica y clara:
Usamos:
.then()
: cuando la siguiente acción retorna un resultado inmediato..pipe()
: cuando la siguiente acción retorna unaPromise<T>
, y queremos encadenarnos a la resolución de esta promesa.
En las promesas de JavaScript sólo existe la operación .then()
, pero al ser Java tipado desgraciadamente es necesario separar ambos casos.
El patrón promesa simplifica no sólo el código de usuario, si no también el código de insfraestructura. Puedes comparar el código de insfraestructura basado en futures para el acceso a BBDD con el basado en promesas. Como puedes ver, las promesas combinan muy bien con código funcional.
En este paso además vamos a simplificar aún más la definición del API REST:
En esta versión tenemos hemos eliminado la gran mayoría de código boilerplate a métodos de extensión:
Resumiendo
En este tutorial hemos visto cómo construir un API REST asíncrono usando Vertx y Kotlin. Empezamos con un servidor HTTP simple respondiendo “Hello world!”, y terminamos con un API REST asíncrono real que emplea buenas prácticas de Kotlin y el patrón Promisa para mantener un código sencillo y muy mantenible.