Golo et interopérabilité Java par l’exemple avec Jetty, SparkJava et Vert.x

J’adore dire que Golo est de la “sucrette syntaxique” pour Java. Ce que je veux dire par là c’est que Golo permet non seulement de rendre Java “plus simple” mais aussi “d’interopérer” facilement avec Java. C’était le cas depuis le début, mais depuis quelques semaines Golo a connu quelques évolutions notables sur le sujet.

Au début : les Single Method Interfaces

Golo sait bien sûr instancier toute sorte d’objets(classes) Java, de l’API Java ou en provenance de librairies(frameworks) Java existants, comme par exemple utiliser com.sun.net.httpserver.HttpServer pour faire un mini serveur http :

let server = HttpServer.create(InetSocketAddress("localhost", 8080), 0)

Mais ensuite si on veut faire bosser notre serveur, nous avons besoin de passer un objet qui implémente l’interface HttpHandler à la méthode createContext() de server, en Java nous aurions ceci :

public class Test {

    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/", new MyHandler());
        server.start();
    }

    static class MyHandler implements HttpHandler {
        public void handle(HttpExchange t) throws IOException {
            // foo
        }
    }
}

Je vous rappelle qu’à ce jour le concept classique de classe n’existe pas, mais il se trouve que Golo sait “caster” des closures en “Single Method Interfaces” (la doc est par ici : http://golo-lang.org/documentation/next/# _conversion_to_single_method_interfaces).

Du coup pour avoir notre instance de handler implémentant HttpHandler il nous suffira d’écrire : let handler = |fct| -> fct: to(HttpHandler.class)et notre mini serveur http resemblera à ceci :

module http.hello.world

import java.net.InetSocketAddress
import com.sun.net.httpserver.HttpHandler
import com.sun.net.httpserver.HttpServer

function main = |args| {

  let handler = |fct| -> fct: to(HttpHandler.class)
  
  let server = HttpServer.create(InetSocketAddress("localhost", 8080), 0)

  server: createContext("/", handler(|exchange|{
    let headers = exchange: getResponseHeaders()
    let uri = exchange: getRequestURI(): toString()
    headers: set("Content-Type", "text/html")
    let response = "<h1>Hello Golo</h1>"
    exchange: sendResponseHeaders(200, response: length())
    exchange: getResponseBody(): write(response: getBytes())
    exchange: close()
  }))

  server: start()
  #  call http://localhost:8080
}

Je veux faire pareil avec Jetty ! Vive l’AdapterFabric

La problématique dans le cas de Jetty, c’est que le serveur (org.eclipse.jetty.server.Server) a une méthode setHandler() qui “attend” un objet de type org.eclipse.jetty.server.handler.AbstractHandler (donc héritage) qui lui même implémente l’interface Handler (http://download.eclipse.org/jetty/stable-7/apidocs/org/eclipse/jetty/server/Handler.html), donc il va falloir en plus lui écrire une méthode handle().

Je le répète : le concept de classe n’existe pas en Golo, mais depuis peu nous avons l’AdapterFabric qui permet de créer des “espèces” de proxies dynamiques mais en poussant le concept un peu plus loin puis que l’on peu créer des objets dynamiques pouvant hériter de classe java, pouvant implémenter des interfaces java tout en définissant les méthodes à implémenter voire même en surchargeant les méthodes héritées.

Donc dans le cas de Jetty, il faudra définir une “configuration” pour “représenter” AbstractHandler :

#  AbstractHandler
let conf = map[
  ["extends", "org.eclipse.jetty.server.handler.AbstractHandler"],
  ["implements", map[
    ["handle", |this, target, baseRequest, request, response| {
      #  foo
    }]       
  ]]
]

Puis pour instancier notre AbstractHandler il nous suffira d’écrire ceci :

let hello_handler = AdapterFabric(): maker(conf): newInstance()

Et enfin notre serveur à base de Jetty ressemblera donc à ceci :

module hello_handler

import javax.servlet.http.HttpServletResponse
import org.eclipse.jetty.server.Server

function main = |args| {
  
  let HelloHandler = {
    let conf = map[
      ["extends", "org.eclipse.jetty.server.handler.AbstractHandler"],
      ["implements", map[
        ["handle", |this, target, baseRequest, request, response| {
          response: setContentType("text/html;charset=utf-8")
          response: setStatus(HttpServletResponse.SC_OK())
          baseRequest: setHandled(true)
          response: getWriter(): println("<h1>Golo Rocks</h1>")
        }]       
      ]]
    ]
    return AdapterFabric(): maker(conf): newInstance()
  }

  let server = Server(8080)
  server: setHandler(HelloHandler())
 
  server:start()
  server:join()

}

Remarque : l’exemple en Java est par là : http://wiki.eclipse.org/Jetty/Tutorial/Embedding_Jetty

Ça marche aussi pour les copains et vive les DSL web !

Cette nouvelle capacité de Golo (l’AdapterFabric) alliée à sa capacité à créer des DSL va nous permettre d’écrire par exemple des DSL REST “à la Node.js” à partir de frameworks Java existants, tels Spark Java (mon petit préféré) ou même Vert.x. Voici donc des exemples (qui fonctionnent) pour ces 2 frameworks :

SparkJava :

Dans le cas de Spark ce qui nous intéresse, c’est le concept de Route dont nous définirons la configuration (ci-dessous) pour avoir un objet qui hérite “dynamiquement” de spark.Route" et qui implémente handle() :

let conf = map[
  ["extends", "spark.Route"],
  ["implements", map[
    ["handle", |this, request, response| {
      return method(request, response)
    }]         
  ]]
]

Remarque : Spark fonctionne sur une base Jetty d’où la similarité avec l’exemple précédent.

Et voici notre serveur (avec le DSL dans le corps de main)

module spark_java

import com.fasterxml.jackson.databind.ObjectMapper
import spark.Request
import spark.Response
import spark.Route
import spark.Spark

import java.io.File

function toJsonString = |data| {
  let mapper = ObjectMapper()
  return mapper:writeValueAsString(data)
}

function route = |uri, method| {
  let conf = map[
    ["extends", "spark.Route"],
    ["implements", map[
      ["handle", |this, request, response| {
        return method(request, response)
      }]         
    ]]
  ]
  let Route = AdapterFabric(): maker(conf): newInstance(uri)
  return Route
}

function GET = |uri, method| {
  return spark.Spark.get(route(uri, method))
}

function POST = |uri, method| {
  return spark.Spark.post(route(uri, method))
}

function main = |args| {

  externalStaticFileLocation(File("."):getCanonicalPath() + "/public")
  setPort(8888)

  GET("/hello", |request, response| {
    return toJsonString(map[["message","Hello Golo!"]])
  }) 

  GET("/salut", |request, response| {
      return toJsonString(map[["message","Salut Golo!"]])
  })

  GET("/test/:id", |request, response| {
      return toJsonString(map[["message", request: params(":id"):toString()]])
  })

  POST("/bob", |request, response| {
    response: type("application/json")
    let resp = request: body()
    return toJsonString(resp)
  })

}

Vert.x

Et un petit dernier pour la route : la version avec Vert.x en mode “embedded” qui globalement suit une logique très proche, sauf que cette fois ci nous ne créons pas un objet qui “hérite” de quelque chose mais un objet qui “implémente dynamiquement” une interface org.vertx.java.core.Handler, donc dans ce cas là la configuration sera la suivante :

let conf = map[
  ["interfaces", ["org.vertx.java.core.Handler"]],
  ["implements", map[
    ["handle", |this, request| {
      return method(request)
    }]         
  ]]
]

Et voici donc notre serveur :

module vertx

import org.vertx.java.core.http.RouteMatcher
import org.vertx.java.core.VertxFactory
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.JsonNode

#  Json helpers
function toJson = |o| -> ObjectMapper(): valueToTree(o)
function parse = |s| -> ObjectMapper(): readValue(s, JsonNode.class)
function fromJson = |jsonNode, clazz| -> ObjectMapper(): treeToValue(jsonNode, clazz)

#  Vert.x handler
function handler = |method| {
  let conf = map[
    ["interfaces", ["org.vertx.java.core.Handler"]],
    ["implements", map[
      ["handle", |this, request| {
        return method(request)
      }]         
    ]]
  ]
  let Handler = AdapterFabric(): maker(conf): newInstance()
  return Handler
}

function main = |args| {
  
  let routeMatcher = RouteMatcher()

  # === CREATE ===
  routeMatcher: post("/hello", handler(|req| {
    req: dataHandler(handler(|buffer|{
      println(buffer: toString())

      let message = fromJson(parse(buffer: toString()), java.util.HashMap.class)
      message: put("id",java.util.UUID.randomUUID(): toString())
      req: response(): end(toJson(message): toString())
    }))
  }))

  # === GET ALL ===
  routeMatcher: get("/hello", handler(|req| {
    req: response(): end(toJson(map[["message", "hello Golo!"]]): toString())
  }))

  # === GET BY ID ===
  routeMatcher: get("/hello/:id", handler(|req| {
    req: response(): end(toJson(map[["message", "hello Golo! -> " + req: params(): get("id")]]): toString())
  }))

  #  Catch all - serve the index page and static assets
  routeMatcher: getWithRegEx(".*", handler(|req| {
    if req: uri(): equals("/") {
        req: response(): sendFile("public/index.html")
    } else {
        req: response(): sendFile("public"+req: uri())
    }
  }))

  let vertx = VertxFactory.newVertx()
  vertx:createHttpServer():requestHandler(routeMatcher):listen(8888)

  readln("listening ...")

}

Conclusion

C’était certes rapide, mais vous avez pu voir qu’il était extrêmement facile de se plugger à des frameworks existants et d’en simplifier l’utilisation.

Et bon WE à tous :)

blog comments powered by Disqus

Related posts