É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:

`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.

`importio.vertx.core.Vertx

importio.vertx.ext.web.Router



objectVertx3KotlinRestJdbcTutorial{



@JvmStaticfunmain(args:Array<String>){

valvertx=Vertx.vertx()

valserver=vertx.createHttpServer()

valport=9000

valrouter=Router.router(vertx)



router.get("/").handler{it.response().end("Hello world!")}



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:

`data classUser(valid:String,valfname:String,vallname:String)



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):

`classMemoryUserService():UserService{



val_users=HashMap<String,User>()



init{

addUser(User("1","user1_fname","user1_lname"))

}



overridefungetUser(id:String):Future<User>{

returnif(_users.containsKey(id))Future.succeededFuture(_users.getOrImplicitDefault(id))

elseFuture.failedFuture(IllegalArgumentException("Unknown user $id"))

}



overridefunaddUser(user:User):Future<Unit>{

_users.put(user.id,user)

returnFuture.succeededFuture()

}



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)
`objectVertx3KotlinRestJdbcTutorial{

valgson=Gson()



@JvmStaticfunmain(args:Array<String>){

valport=9000

valvertx=Vertx.vertx()

valserver=vertx.createHttpServer()

valrouter=Router.router(vertx)

router.route().handler(BodyHandler.create())

valuserService=MemoryUserService()



router.get("/:userId").handler{ctx->

valuserId=ctx.request().getParam("userId")

jsonResponse(ctx,userService.getUser(userId))

}



router.post("/").handler{ctx->

valuser=jsonRequest<User>(ctx,User::class)

jsonResponse(ctx,userService.addUser(user))

}



router.delete("/:userId").handler{ctx->

valuserId=ctx.request().getParam("userId")

jsonResponse(ctx,userService.remUser(userId))

}



server.requestHandler{router.accept(it)}.listen(port){

if(it.succeeded())println("Server listening at $port")

elseprintln(it.cause())

}

}



funjsonRequest<T>(ctx:RoutingContext,clazz:KClass<outAny>):T=

gson.fromJson(ctx.bodyAsString,clazz.java)asT





funjsonResponse<T>(ctx:RoutingContext,future:Future<T>){

future.setHandler{

if(it.succeeded()){

valres=if(it.result()==null)""elsegson.toJson(it.result())

ctx.response().end(res)

}else{

ctx.response().setStatusCode(500).end(it.cause().toString())

}

}

}

}`
### 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:

`router.get("/:userId").handler{ctx->

valuserId=ctx.request().getParam("userId")

jsonResponse(ctx,userService.getUser(userId))

}



router.post("/").handler{ctx->

valuser=jsonRequest<User>(ctx,User::class)

jsonResponse(ctx,userService.addUser(user))

}



---------->



get("/:userId"){send(userService.getUser(param("userId")))}



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

}



funRouter.get(path:String,rctx:RoutingContext.()->Unit)=get(path).handler{it.rctx()}

funRouter.post(path:String,rctx:RoutingContext.()->Unit)=post(path).handler{it.rctx()}

funRouter.put(path:String,rctx:RoutingContext.()->Unit)=put(path).handler{it.rctx()}

funRouter.delete(path:String,rctx:RoutingContext.()->Unit)=delete(path).handler{it.rctx()}



funRoutingContext.param(name:String):String=

request().getParam(name)



funRoutingContext.bodyAs<T>(clazz:KClass<outAny>):T=

GSON.fromJson(bodyAsString,clazz.java)asT



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.

Veamos la implentación del servicio usando JDBC:

`classJdbcUserService(privatevalclient:JDBCClient):UserService{



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()

}





overridefunaddUser(user:User):Future<Unit>=

client.update("INSERT INTO USERS (ID, FNAME, LNAME) VALUES (?, ?, ?)",

listOf(user.id,user.fname,user.lname))





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.

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 <code class="highlighter-rouge">Future<T> por <code class="highlighter-rouge">Promise<T>):

`data classUser(valid:String,valfname:String,vallname:String)



interfaceUserService{



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))

}





overridefunaddUser(user:User):Promise<Unit>=

client.update("INSERT INTO USERS (ID, FNAME, LNAME) VALUES (?, ?, ?)",

listOf(user.id,user.fname,user.lname)).then{}





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:

`valdbConfig=JsonObject()

.put("url","jdbc:hsqldb:mem:test?shutdown=true")

.put("driver_class","org.hsqldb.jdbcDriver")

.put("max_pool_size",30)



objectVertx3KotlinRestJdbcTutorial{



@JvmStaticfunmain(args:Array<String>){

valvertx=promisedVertx()



valclient=JDBCClient.createShared(vertx,dbConfig);

valuserService=JdbcUserService(client)



vertx.restApi(9000){



get("/:userId"){send(userService.getUser(param("userId")))}



post("/"){send(userService.addUser(bodyAs(User::class)))}



delete("/:userId"){send(userService.remUser(param("userId")))}



}

}

}`
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

}



funRouter.get(path:String,rctx:RoutingContext.()->Unit)=get(path).handler{it.rctx()}

funRouter.post(path:String,rctx:RoutingContext.()->Unit)=post(path).handler{it.rctx()}

funRouter.put(path:String,rctx:RoutingContext.()->Unit)=put(path).handler{it.rctx()}

funRouter.delete(path:String,rctx:RoutingContext.()->Unit)=delete(path).handler{it.rctx()}`
### 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.