Upgrade to Pro — share decks privately, control downloads, hide ads and more …

APIs REST con Akka y Scalatra

Nando Sola
December 18, 2014

APIs REST con Akka y Scalatra

Presentación para ScalaMadrid sobre Scalatra. Dirigida a Scala newbies, pero con experiencia en JavaEE y microframeworks en general.

http://www.meetup.com/Scala-Programming-Madrid/events/219153378/

Nando Sola

December 18, 2014
Tweet

More Decks by Nando Sola

Other Decks in Technology

Transcript

  1. Un poco de historia • 0.1.0 - October 4, 2007

    • DSL sobre Rack, ¡en un sólo fichero! • Rutas, filtros y helpers • Plantillas y archivos estáticos • Framework propio de tests • Aplicaciones modulares rubyconf2008.confreaks.com/lightweight-web-services.html sinatrarb.com
  2. Desventajas de Sinatra • Rendimiento de Ruby (MRI) • Usar

    Torquebox (basado en JRuby) • Streaming desde el servidor * • Ecosistema algo caótico (25+ págs de resultados en RubyGems) • Sinatra sólo es un DSL • Padrino.rb para construir aplicaciones más complejas • scaffolding • plugins recomendados (persistencia, websockets, etc) github.com/nandosola/trantor-stream-uploader/blob/master/README.md *
  3. – Ross Baker, Scalatra co-author “The Sinatra family of frameworks

    is compelling because they are minimal: if you already know the target language and the basics of HTTP, then you can be almost instantly productive with these frameworks.” infoq.com/news/2010/10/scalatra hackage.haskell.org/package/scotty sparkjava.com … github.com/twitter/finatra
  4. Scala + Sinatra = … • Proyecto interno de LinkedIn

    (Step) • DSL Scala sobre Servlet 3.0 • Streaming bidireccional * • IMHO Scalatra es… ¿Scadrino? • Plantillas con scalate • Asincronía con Akka y Atmosphere (websockets) • Soporte JSON con json4s • Persistencia (Slick, Riak, …) • Swagger • … groups.google.com/d/topic/scalatra-user/Inc-ubC-BUU/discussion groups.google.com/d/topic/scalatra-user/LJWjr4OaDdE/discussion * github.com/alandipert/step
  5. Primer paso • Generar scaffold con giter8 • $ g8

    nandosola/scalatra-sbt • JSON REST + Akka + SBT • ScalaTest • Jetty embebido
  6. Rutas • class MyController extends ScalatraServlet • get("/scala/:adjective"){"Scala "+params("name")} GET

    /scala/suxxorz = String • get(“/scala/*”){"Scala "+multiParams(“splat").mkString(" ")} GET /scala/is/awesome = Seq[String] • get("""^\/s(.*)/m(.*)""".r){multiParams("captures")} GET /scala/meetup = Seq[String] • CAVEAT: las rutas se evalúan desde abajo
  7. Acciones y filtros 1. Filtros before 2. Rutas y sus

    acciones • ActionResult: corresponden a respuestas HTTP • Array[Byte]: escribe a outputStream un application/octet-stream • Unit, Any: conversión a String 3. Las excepciones en el filtro before o en las rutas se pasan a la closure error. Su resultado se devuelve como acción de la ruta 4. Filtros after 5. Renderizado de la respuesta
  8. Soporte JSON • Descargar dependencias en SBT "org.scalatra" %% "scalatra-json"

    % ScalatraVersion,
 "org.json4s" %% "json4s-jackson" % "3.2.10" • Activar el soporte JSON trait MyAppServiceStack extends ScalatraServlet with JacksonJsonSupport {
 implicit val jsonFormats: Formats = DefaultFormats.withBigDecimal … before() {
 logger.trace(request.body)
 contentType = formats("json")
 } … } • Modelar el dominio en base a case classes
  9. Validaciones • Usa un patrón comando on steroids, con la

    ayuda de ScalaZ "org.scalatra" %% "scalatra-commands" % ScalatraVersion post("/todos") {
 (command[CreateTodoCommand] >> (TodoData.create(_))).fold(
 errors => halt(400, errors),
 todo => redirect("/")
 )
 } … • CAVEAT: más pensadas para validar formularios que JSON • Los objetos anidados requieren type converters • Hay Alternativas más sencillas si no se conoce ScalaZ o json4s avanzado • require() / error{ … } • github.com/wix/accord http://www.scalatra.org/2.3/guides/formats/commands.html
  10. Akka FTW • “I still recommend to ditch the application

    server” - The Klang • Modelo natural para concurrencia y distribución • Gratis: asincronía y procesamiento paralelo "com.typesafe.akka" %% "akka-actor" % akkaVersion,
 "com.typesafe.akka" % "akka-testkit_2.11" % akkaVersion % "test"
  11. Despliegue • Contenedor Java EE (WAR) • Disponible por defecto

    vía sbt package • Akka puede funcionar con JavaEE 7, pero hay que tener cuidado @Resource private ManagedExecutorService mes ActorSystem("myActorSystem", Some(config), None, Some(mes)) … implicit def executor: ExecutionContext = ExecutionContext.fromExecutorService(mes) • JAR autoejecutable • github.com/sbt/sbt-assembly • JDK 1.8 recomendado para Akka (ForkJoinPool) • Con JDK 1.7, Akka puede ir más lento
  12. Bootstrap file class ScalatraBootstrap extends LifeCycle {
 
 val logger

    = LoggerFactory.getLogger(getClass)
 
 val config = ConfigFactory.load()
 val scalatraEnv = sys.env.getOrElse("SCALATRA_ENV", "development")
 logger.info("SCALATRA_ENV is " + scalatraEnv)
 val appConf = config.getConfig("properties").getConfig(scalatraEnv)
 
 val system = ActorSystem("ThingamabobSystem", config)
 val httpPosterActor = system.actorOf(ProxyHttpClientActor
 .props(appConf.getString("upstream-uri"))
 .withRouter(SmallestMailboxPool(nrOfInstances = 2000)), "HttpPosterActor")
 
 override def init(context: ServletContext) {
 
 context.mount(new MyController(system, httpPosterActor), "/*")
 context.initParameters("org.scalatra.environment") = scalatraEnv
 }
 
 override def destroy(context:ServletContext) {
 system.shutdown()
 }
 } github.com/typesafehub/config
  13. Controlador fire and forget post("/thingamabob/V01/:thingId/?") {
 val id = params("thingId")


    val myRequest = parsedBody.extract[RawRequest]
 
 myRequest match {
 case rawItem : RawRequest =>
 try {
 upstreamActor ! rawItem.toUpstreamRequest(thingId)
 } catch {
 case e: IllegalArgumentException => logger.info("Got IllegalArgumentException for thingId " + rawItem.id + ": " + e.getMessage + " -- Ignoring")
 }
 case _ => haltHelper(ErrorResponse(400, "Malformed request"))
 }
 halt(204, Map("Connection"->"close"))
 }
  14. Akka actor object ProxyHttpClientActor {
 def props(proxyUri: String): Props =

    Props(new ProxyHttpClientActor(proxyUri))
 }
 class ProxyHttpClientActor(proxyUri: String) extends Actor with CustomSslConfiguration {
 import com.robotchrist.app.wadus.ThingamabobJsonProtocol._
 
 implicit def system: ActorSystem = context.system
 implicit def executor: ExecutionContext = system.dispatcher
 
 val log = Logging(system, classOf[ProxyHttpClientActor])
 val pipeline: HttpRequest => Future[HttpResponse] = addHeader("Accept", "application/json") ~> sendReceive
 
 def receive = {
 case payload:UpstreamFeedback => pipeline(Post(proxyUri, payload)).onComplete {
 case Success(s) => log.info("Successfully sent: " + s.toString)
 case Failure(f) => log.warning("Upstream error: " + f.getMessage)
 }
 }
 }
  15. Modelos • Petición en crudo /*
 * Classes for raw,

    unvalidated HTTP requests
 */
 case class RawRequest(id: Option[String], `type`: RawType, date: String) {
 def toUpstream(id: String) = UpstreamFeedback(id, `type`.id, date)
 def toUpstream = UpstreamFeedback(id.getOrElse(""), `type`.id, date)
 }
 case class RawType(id: String, name: Option[String]) • Respuestas /* HTTP responses */
 case class ErrorResponse(statusCode: Integer, message: String)
  16. Modelos • Clases de dominio sin validaciones complejas /*
 *

    Domain classes
 */
 
 case class Upstream(thingId: String, code:String, date: String) {
 val Pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
 val dtf: DateTimeFormatter = DateTimeFormat.forPattern(Pattern)
 
 require(thingId.matches("[A-Fa-f0-9-]{8,}"), "thingId has an invalid format")
 require(code.matches("[0-9]{4}"), "code has an invalid format")
 require( try { dtf.parseDateTime(feedbackDate); true } catch { case _: Throwable => false }, "feedbackDate has an invalid format")
 
 } 
 Esto es Java en Scala. ¿Y qué? • No hace falta saber ScalaZ para validar • Always refactor!
  17. Errores • Capturar las excepciones generadas por Jackson error {


    
 case e: MappingException =>
 logger.error("Mapping exception. The request format couldn't be recognized as valid JSON", e)
 haltHelper(ErrorResponse(400, "Couldn't parse request"))
 
 case e: Throwable =>
 logger.error("Unknown exception received", e)
 haltHelper(ErrorResponse(500, "Internal server error"))
 }
  18. Controlador batch post("/thingResolver/?") {
 
 val myRequest = parsedBody.extract[RawRequest]
 


    myRequest.things.getOrElse(myRequest.thingValues.get) match { 
 case List() => haltHelper(ErrorResponse(400, "No things provided"))
 
 case rawItems: List[RawItem] => rawItems.par.aggregate(SuccessResponse()){ (succRes, rawItem) => rawItem.asItem match {
 
 case errItem: ErrorItem => succRes.addThing(errItem)
 case reqItem: RequestItem =>
 val future = ask(repositoryActor, reqItem).mapTo[ResponseItem]
 Await.result(future, timeout.duration) match {
 case resItem => succRes.addThing(resItem)
 }
 case _ => haltHelper(ErrorResponse(400, "Unsupported thing object"))
 }, _ + _ }
 …
  19. Más cosicas • scalatra.org • Scalatra In Action • github.com/scalatra/scalatra

    • @scalatra • #scalatra en Freenode • groups.google.com/forum/#!forum/scalatra-user • skinny-framework.org