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.
`git clone git@github.com:maballesteros/vertx3-kotlin-rest-jdbc-tutorial.git`
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.
server.requestHandler{router.accept(it)}.listen(port){ if(it.succeeded())println("Server listening at $port") elseprintln(it.cause()) } } }`
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:
interfaceUserService{ fungetUser(id:String):Future<User> funaddUser(user:User):Future<Unit> funremUser(id:String):Future<Unit> }`
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 <code class="highlighter-rouge">User o <code class="highlighter-rouge">Unit. En su lugar, debemos debemos suministrar algún tipo de callback o retornar un resultado futuro <code class="highlighter-rouge">Future<T> que nos permita registrarnos a resultados satisfactorios o fallidos, cuando estos ocurran.
En este paso implementaremos este servicio con un <code class="highlighter-rouge">Map mutable (un <code class="highlighter-rouge">HashMap Java):
overridefunremUser(id:String):Future<Unit>{ _users.remove(id) returnFuture.succeededFuture() } }`
Para exponer el servicio vía REST, tendremos que mapear las operaciones a sus correspondientes `GET`, `POST`, y `DELETE`.
Destacar:
la llamada a <code class="highlighter-rouge">router.route().handler(BodyHandler.create()), que que queremos poder obtener el cuerpo de la request como una <code class="highlighter-rouge">String.
el uso de <code class="highlighter-rouge">Gson para codificar / decodificar JSON
como nos suscribimos a un resultado futuro (<code class="highlighter-rouge">future.setHandler)
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:
post("/"){send(userService.addUser(bodyAs(User::class)))}`
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:
`valGSON=Gson()
funHttpServer.restAPI(vertx:Vertx,body:Router.()->Unit):HttpServer{ valrouter=Router.router(vertx) router.route().handler(BodyHandler.create())// Required for RoutingContext.bodyAsString router.body() requestHandler{router.accept(it)} returnthis }
funRoutingContext.send<T>(future:Future<T>){ future.setHandler{ if(it.succeeded()){ valres=if(it.result()==null)""elseGSON.toJson(it.result()) response().end(res) }else{ response().setStatusCode(500).end(it.cause().toString()) } } }`
### 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.
init{ client.execute(""" CREATE TABLE USERS (ID VARCHAR(25) NOT NULL, FNAME VARCHAR(25) NOT NULL, LNAME VARCHAR(25) NOT NULL) """).setHandler{ valuser=User("1","user1_fname","user1_lname") addUser(user) println("Added user $user") } }
overridefungetUser(id:String):Future<User>= client.queryOne("SELECT ID, FNAME, LNAME FROM USERS WHERE ID=?",listOf(id)){ it.results.map{User(it.getString(0),it.getString(1),it.getString(2))}.first() }
overridefunremUser(id:String):Future<Unit>= client.update("DELETE FROM USERS WHERE ID = ?",listOf(id)) }`
¿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:
`valclient=JDBCClient.createShared(vertx,JsonObject() .put("url","jdbc:hsqldb:mem:test?shutdown=true") .put("driver_class","org.hsqldb.jdbcDriver") .put("max_pool_size",30)); valuserService=JdbcUserService(client) //valuserService=MemoryUserService()`
En este tutorial emplearemos [hsqldb](http://hsqldb.org/), 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 <code class="highlighter-rouge">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 <code class="highlighter-rouge">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 <code class="highlighter-rouge">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.
Podemos entonces redefinir nuestro código sobre este patróna asíncrono. Empecemos por redefinir el API del servicio (fácil, basta cambiar <code class="highlighter-rouge">Future<T> por <code class="highlighter-rouge">Promise<T>):
fungetUser(id:String):Promise<User?> funaddUser(user:User):Promise<Unit> funremUser(id:String):Promise<Unit> }`
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:
`classJdbcUserService(privatevalclient:JDBCClient):UserService{
init{ valuser=User("1","user1_fname","user1_lname") client.execute(""" CREATE TABLE USERS (ID VARCHAR(25) NOT NULL, FNAME VARCHAR(25) NOT NULL, LNAME VARCHAR(25) NOT NULL) """) .pipe{addUser(user)} .then{println("Added user $user")} }
overridefungetUser(id:String):Promise<User?>= client.queryOne("SELECT ID, FNAME, LNAME FROM USERS WHERE ID=?",listOf(id)){ User(it.getString(0),it.getString(1),it.getString(2)) }
overridefunremUser(id:String):Promise<Unit>= client.update("DELETE FROM USERS WHERE ID = ?",listOf(id)).then{} }`
Usamos:
<code class="highlighter-rouge">.then(): cuando la siguiente acción retorna un resultado inmediato.
<code class="highlighter-rouge">.pipe(): cuando la siguiente acción retorna una <code class="highlighter-rouge">Promise<T>, y queremos encadenarnos a la resolución de esta promesa.
En las promesas de JavaScript sólo existe la operación <code class="highlighter-rouge">.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:
`funVertx.restApi(port:Int,body:Router.()->Unit){ createHttpServer().restApi(this,body).listen(port){ if(it.succeeded())println("Server listening at $port") elseprintln(it.cause()) } }
funHttpServer.restApi(vertx:Vertx,body:Router.()->Unit):HttpServer{ valrouter=Router.router(vertx) router.route().handler(BodyHandler.create())// Required for RoutingContext.bodyAsString router.body() requestHandler{router.accept(it)} returnthis }
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.