Header Ads Widget

Ticker

6/recent/ticker-posts

Construyendo una API REST en Java y Scala usando Play Framework - Parte 2

 

construyendo-un-resto-api-en-scala-y-java-usando-play-framework-2

En la primera parte de esta serie , presentamos Play framework , un framework de desarrollo web para desarrolladores de Java y Scala, y mostramos cómo permitía exponer una API REST básica con un esfuerzo mínimo. En esta segunda parte, exploramos algunas de las características de Play y usamos código real para ilustrar sus capacidades.

Acciones asincrónicas

Play admite servicios REST sincrónicos y asincrónicos, y es particularmente poderoso cuando se trata de solicitudes asincrónicas .

Cualquiera que sea la solicitud, una acción siempre devuelve una instancia de play.mvc.ResultEsto se puede devolver directamente al consumidor en el caso de un servicio síncrono, o se puede servir como un futuro. En este caso, producirá una respuesta solo cuando la función deseada haya terminado de computarse. Esto resulta muy útil cuando el resultado depende de una llamada a una API externa o de un cálculo prolongado.

Mientras tanto, el subproceso que estaba ejecutando el código del controlador se libera para ocuparse de otra cosa; el resto del código se manejará cuando el subproceso de cálculo haya terminado de funcionar. Esto es lo que se llama E / S sin bloqueo .

En el siguiente ejemplo, queremos calcular la puntuación para una lista de jugadores de un juego. Podría haber muchos jugadores, y dado que la función de cálculo de la puntuación es relativamente intensiva en cálculos, no sabemos cuánto durará el cálculo completo. Queremos devolver un objeto JSON con las puntuaciones una vez que las hayamos recopilado todas. [1]

def getPlayerScores(ids: List[String]) = Action.async { implicit request =>
  val futureScores = for(id <- ids) yield calculatePlayerScore(id)
 
  futureScores map { scores =>
    Ok(Json.obj("scores" -> scores.mkString(";")))
  }
}

En la primera parte de la getPlayerScoresacción anterior, el 'para comprensión' devuelve un Futuro, que es un control sobre el valor eventual de un cálculo largo (en este caso, calcular las puntuaciones de cada jugador uno por uno). La segunda parte indica al controlador que espere a que se calculen todas las puntuaciones y devuelva los valores resultantes dentro de un objeto JSON serializado.

Tenga en cuenta que como esta acción devuelve un futuro, debemos especificar el tipo de devolución como Action.async.

Aprenda a desarrollar API agnósticas de descripción con API Transformer

Servicios web asincrónicos

Play también cuenta con una poderosa biblioteca de WebServices (WS) que le permite a uno hacer sus propias llamadas externas dentro de una acción. Todas las funciones de WS son asincrónicas y no bloqueantes. [2]

def getDistance = Action.async { implicit request =>
  val origin: String = request.getQueryString.get("origin")
  val destination: String = request.getQueryString.get("destination")
  val futureDistance = WS.url(apiUrl + "?origins=" + origin + "&destinations=" + destination + "&key=" + apiKey).get
  futureDistance map { response =>
    val distanceText: String = parseGoogleJson(response.json)
    Ok(Json.obj("distance" -> distanceText))
  }
}

[3]

Asigne esta acción a un punto final HTTP. Acepta dos parámetros de URL: 'origen' y 'destino'. Si pasamos versiones codificadas con URL de 'Estocolmo + Suecia' al parámetro de origen y 'Bruselas + Bélgica' al parámetro de destino, recuperaremos la distancia entre estas dos ubicaciones de la API de matriz de distancia de Google y mostraremos el valor de la distancia de regreso a nuestro cliente dentro de una cadena JSON.

Composición futura

El ejemplo anterior no fue muy útil porque acabamos de crear un proxy para la API de Google. Intentemos hacer algo más interesante combinando varias llamadas de API salientes dentro de una sola Acción.

Supongamos que tenemos una copia de marketing sólida, como el contenido rico en palabras clave de la página de destino de un sitio web. Queremos encontrar nombres de dominio de Internet relevantes que puedan ser una buena combinación para esta propaganda de texto.

Vamos a combinar dos API del mercado de API de Mashape ( TextAnalysis y Blazing Domain Availability Search ) utilizando la biblioteca de servicios web de Play y la composición Future de Scala (lo mismo se puede lograr con Promises en Java).

Comenzaremos escribiendo dos funciones privadas que se ocuparán cada una de una de las llamadas a la API.

val mashapeApiKey = "FbkOCUKf8hMopHO5I8u2pGBUli2Dp14ZSMIjsnh0ky1T5tmpK1"

  private def getNounPhrases(text: String): Future[List[String]] = {
    val textAnalysisUrl = "https://textanalysis.p.mashape.com/textblob-noun-phrase-extraction"
    val futureKeyPhrases = WS.url(textAnalysisUrl)
 .withHeaders("X-Mashape-Key" -> mashapeApiKey,
   	   	           "Content-Type" -> "application/x-www-form-urlencoded",
"Accept" -> "application/json")
                          .post(Map("text" -> Seq(text)) )
    futureKeyPhrases map { keyPhrases =>
      (keyPhrases.json \ "noun_phrases").as[List[String]].map(phrase => phrase.replace(" ", ""))
     }
  }

La primera función llama a la API TextAnalysis y publica el contenido del texto que queremos analizar. El punto final Key Phrase Extractor analiza el texto y extrae frases nominales útiles. Para probar la disponibilidad de nombres de dominio, eliminamos los espacios en blanco de cada uno de estos valores de cadena. Tenga en cuenta que el resultado de esta función es una lista futura de cadenas.

private def getAvailableDomains(phrase: String): Future[List[String]] = {
  val domainSearchUrl = "https://domainsearch.p.mashape.com/index.php?name="
  val futureDomains = WS.url(domainSearchUrl + phrase)
                        .withHeaders("X-Mashape-Key" -> mashapeApiKey,
  					   				 "Accept" -> "application/json").get					  
  futureDomains map { domainNames =>
    val domains: Map[String, String] = domainNames.json.as[Map[String, String]]
    domains.collect {
      case (name, status) if status == "Available" => name
    }
  }
}

La segunda función toma una frase determinada y llama a Blazing Domain Availability Search. Esto probará varios nombres de dominio relacionados con la frase (.com, .net, .biz, etc.) para verificar su disponibilidad y devolverá sus hallazgos. Conservaremos solo los nombres de dominio disponibles. Una vez más, obtenemos una lista futura de cadenas.

Necesitamos poder llamar a esta última función para obtener una lista de frases nominales, por lo que crearemos una práctica función contenedora que acepte un argumento de tipo List [String]:

private def getAvailableDomains(phrases: List[String]): Future[List[String]] = {
  Future.sequence(phrases.flatMap(phrase => getAvailableDomains(phrase)))
}

Ahora tenemos todos los ingredientes básicos, solo necesitamos sumar los resultados en una acción de juego.

def getDomainNames(text: String) = Action.async { implicit request =>
for {
phrases <- getNounPhrases(text)
domains <- getAvailableDomains(phrases)
} yield {
Ok(Json.obj("available_domains" -> domains.mkString(";")))
}
}

De Scala forcomprensiones vienen aquí a mano - el código anterior llamadas de los getNounPhrasesy las getAvailableDomainsfunciones seguidas, con el resultado de la primera función como argumento a la segunda (por debajo de las cubiertas, una flatMapy una mapfunción de haber sido llamado), lo que resulta en una lista de dominio nombres que estamos sirviendo como JSON.

El ejemplo anterior ilustra cómo Play Framework y Scala permiten una definición ajustada de procesamiento relativamente complejo. Hemos creado algo bastante útil en unas pocas líneas de código.

Descarga nuestra guía de desarrollo gratuita

API de transmisión

Play permite múltiples formas de transmitir datos . Una opción es abrir un WebSocket con el servidor, pero como estamos tratando de mantener nuestra API RESTful, nos quedaremos con las Acciones en el siguiente ejemplo y usaremos la codificación de transferencia fragmentada. [4]

def videoStream = Action {
val stream = new FileInputStream("/radioactive.mp4")
val streamContent: Enumerator[Array[Byte]] = Enumerator.fromStream(stream)
Ok.chunked(streamContent)
}

En el ejemplo anterior, usamos el tipo de enumerador de Play para cortar un archivo .mp4 en trozos (matrices de bytes) y enviarlos al cliente mediante una respuesta fragmentada (tenga en cuenta que también puede devolver el archivo usando Ok.sendFile).

Persistencia

Las últimas versiones del marco de Play ya no vienen con un marco de persistencia listo para usar. Play tiene una asociación desde hace mucho tiempo con controladores para bases de datos SQL Anorm y Slick , pero cualquiera de estos debe importarse como módulos separados en SBT.

El propósito es mantener el marco principal lo más ligero posible. Para lograr una capa completamente asincrónica y sin bloqueo con Play, algunos desarrolladores renuncian a la inclusión de código de persistencia de datos dentro del marco de Play. En cambio, delegan esta responsabilidad a un backend como Akka o Finagle .

Sin embargo, para adaptarse mejor a la filosofía de E / S sin bloqueo de Play, es mejor combinarla con una base de datos que presente un controlador asíncrono y sin bloqueo. Este es el caso de MongoDB . El propio equipo de Play participó en la creación de ReactiveMongo , un controlador asincrónico para MongoDB.

Al usar Play y ReactiveMongo, el desarrollador puede optar por el diseño JSON 'de costa a costa' . En este caso, no es necesario definir clases de modelo, ya que el JSON que utiliza el cliente y que se procesa a través de Play se almacena directamente como archivos BSON en MongoDB (con posibles pasos de validación y transformación intermedios).

Implementar la negociación de contenido para la longevidad de la API web

Consejos y trucos

La curva de aprendizaje de Play no es muy empinada si ya conoces Java o Scala, pero lleva un poco más de tiempo dominar realmente sus entresijos. Aquí hay algunos consejos rápidos sobre cómo resolver problemas comunes y enriquecer su API REST con tecnología de Play.

1. Composición de la acción

Puede definir acciones personalizadas y un comportamiento común en varias acciones mediante ActionBuilder . Esto funciona creando un contenedor alrededor de la clase de acción estándar y le permite agregar registro o autenticación a ciertas solicitudes, o modificar el resultado (por ejemplo, agregando un encabezado HTTP).

2. Recuperación de excepciones

En una sección anterior vimos lo útil que podría ser la biblioteca de servicios web de Play. En muchos casos, cuando llame a API externas y otros recursos, tendrá que manejar una gran cantidad de errores y tiempos de espera.

La biblioteca WS le permite administrar los tiempos de espera fácilmente:

WS.url(“https://api.twitter.com/1.1/search/tweets.json?q=%40play+framework)
.withRequestTimeout(5000)
.get()

Scala le permite recuperarse con gracia de excepciones en Futures:

futureTweets recover {
case Exception => “no tweets could be found”
}

3. Rasgos (o validación de entrada personalizada)

Usando los rasgos de Scala, puede mezclar funciones personalizadas en sus Acciones. Para los desarrolladores de Java, los rasgos son similares a las interfaces pero con implementaciones que puede incluir en clases y objetos que presentan este rasgo.

En el siguiente ejemplo, hemos combinado un rasgo de validación en nuestro controlador para realizar una validación de entrada básica en las solicitudes entrantes.

object Application extends Controller with BasicValidator {

def getPicture(name: String) = Action {
if(BasicValidator.validate(name)) Ok("here's your pic: " + name)
else Ok("invalid picture name")
}

4. Protección CSRF

Mientras estamos en el tema de la validación de entrada y la seguridad, Play proporciona protección CSRF en forma de un filtro global que se puede aplicar a todas las solicitudes.

El filtro genera un nuevo token CSRF para todas las solicitudes GET; el token está disponible en cualquier forma que pueda enviarse en la siguiente solicitud.

Notas a pie de página:

[1] para usar Futures en tu Play Controller, necesitarás las siguientes declaraciones de importación en la parte superior de tu archivo Controller:

import play.api.libs.concurrent.Execution.Implicits._
import scala.concurrent.Future
import play.api.Play.current

[2] Para utilizar la biblioteca de servicios web de Play, debe incluir estas dos declaraciones de importación:

import play.api.libs.ws.WS
import play.api.libs.ws.WSResponse

[3] Aquí está la función parseGoogleJson que usamos para recuperar la distancia de la API de matriz de distancia de Google:

private def parseGoogleJson(json: JsValue): String = {
val rows = (json \ "rows").as[List[JsObject]]
val jsonDistance = (rows.head \ "elements").as[List[JsObject]].head
(jsonDistance \ "distance" \ "text").asOpt[String].get
}

[4] El ejemplo de transmisión requiere dos declaraciones de importación más:

import play.api.libs.iteratee.Enumerator
import java.io.FileInputStream
EDITAR: Gracias a Julien Lafont por sus sugerencias para hacer que el código de este proyecto sea más idiomático y conciso.

Publicar un comentario

0 Comentarios