Vert-X + Groovy: Faire un framework pour paresseux
Ce qui est intĂ©ressant avec Groovy, câest la âmeta programmationâ, câest Ă dire sa capacitĂ© Ă ajouter des comportements Ă des classes existantes (mais ce nâest pas que ça). Aujourdâhui voyons donc comment âcommencerâ un framework pour les âfaignassesâ.
Tout dâabord je remercie https://twitter.com/glaforge qui mâa permis de faire les choses dans les rĂšgles de lâart.
Mon objectif est de simplifier le code Ă Ă©crire, et dans un 1er temps je suis parti sur le concept dâExpandoMetaClass http://www.groovy-lang.org/metaprogramming.html# metaprogramming_emc (Jâajoute des mĂ©thodes aux classes de Vert-X au run-time).
Mais ma problĂ©matique, Ă©tait de âcharger/grefferâ ses nouvelles mĂ©thodes de la façon la plus transparente possible. Avec le principe dâExpandoMetaClass, il faut explicitement âexĂ©cuterâ les âgreffonsâ dans le code. Et si on souhaite modulariser ses extensions, il faudra crĂ©er une classe dans un package avec une mĂ©thode qui exĂ©cute les extensions et appeler explicitement cette mĂ©thode:
class Augmentations {
static pimpMyClasses() {
String.metaClass.yo { ->
return "yo " + delegate
}
}
}
Puis dans mon code jâappelerais:
Augmentations.pimpMyClasses()
ça fonctionne, mais jâaurais bien aimĂ© que mes classes soient âaugmentĂ©esâ de maniĂšre transparentes. Et câest lĂ que Guillaume mâa mis sur la piste des Extension Modules http://www.groovy-lang.org/metaprogramming.html# _extension_modules. En gros, câest le moyen de charger automatiquement vos extensions sans avoir Ă le faire de maniĂšre explicite dans votre code.
Mise en oeuvre dâun module dâextension
Pour cela il vous faut danns votre projet un répertoire (et sous-répertoires) /resources/META-INF/services
dans lequel vous aurez un fichier org.codehaus.groovy.runtime.ExtensionModule
my-app/
âââ src/
| âââ main/
| âââ groovy/
| | âââ Starter.groovy
| âââ resources/
| âââ META-INF/
| âââ services/
âââ org.codehaus.groovy.runtime.ExtensionModule
Dans ce fichier nous allons dĂ©finir oĂč sont nos extensions:
moduleName=Some extensions for my framework
moduleVersion=1.0-wip
extensionClasses=my.extensions.WebExtensions
Ensuite créez une classe WebExtensions
dans un package my.extensions
et créons nos extensions.
Extensions de classes
1Ăšre extension: param
Lorsque je veux rĂ©cupĂ©rer le paramĂštre dâune requĂȘte de type GET
avec Vert-x, je dois Ă©crire:
String name = context.request().getParam("name").toString()
Et que je crĂ©e ma classe dâextensions comme ceci:
package my.extensions
import io.vertx.groovy.ext.web.RoutingContext
import io.vertx.groovy.ext.web.handler.StaticHandler
class WebExtensions {
static Object param(RoutingContext self, String paramName) {
return self.request().getParam(paramName)
}
}
Maintenant je pourrais récupérer mes paramÚtres comme cela:
String name = context.param("name").toString()
Je suis dâaccord, je nâai pas gagnĂ© grand chose, donc allons un peu plus loin.
Nouvelle extension: sendJson
Pour renvoyer du Json Ă mon navigateur, avec vert-x je dois Ă©crire:
context
.response()
.putHeader("content-type", "application/json")
.end(Json.encodePrettily([
"message":"Hi!",
"name": "Bob Morane"
]))
si jâajoute cette mĂ©thode Ă ma classe dâextensions:
//import io.vertx.core.json.Json
static void sendJson(RoutingContext self, content) {
self.response()
.putHeader("content-type", "application/json")
.end(Json.encodePrettily(content))
}
Maintenant je pourrais Ă©crire:
context.sendJson([
"message":"Hi!",
"name": "Bob Morane"
])
Dans le mĂȘme esprit, je voudrais pouvoir rĂ©cupĂ©rer les donnĂ©es Json lors dâun POST
.
Nouvelle extension: bodyAsJson
Normalement je dois Ă©crire ceci:
Json.decodeValue(context.getBodyAsString(), Object.class)
si jâajoute cette mĂ©thode Ă ma classe dâextensions:
static Object bodyAsJson(RoutingContext self, klass) {
return Json.decodeValue(self.getBodyAsString(), klass)
}
Maintenant je pourrais Ă©crire:
def obj = context.bodyAsJson(Object.class)
Mais allons encore un peu plus loin
Nouvelles extensions: GET
et POST
Actuellement je définis mes routes comme ceci:
router.get("/api/hi/:name").handler({ context ->
// foo
})
router.post("/api/humans").handler({ context ->
// foo
})
si jâajoute ces mĂ©thodes Ă ma classe dâextensions:
//import io.vertx.groovy.ext.web.Router
static void GET(Router self, String uri, handler) {
self.get(uri).handler(handler)
}
static void POST(Router self, String uri, handler) {
self.post(uri).handler(handler)
}
Maintenant je pourrais Ă©crire:
router.GET("/api/hi/:name", { context ->
// foo
}
router.POST("/api/humans", { context ->
// foo
}
Et enfin une derniĂšre pour la route: start
Pour dĂ©marrer mon serveur http et lui expliquer oĂč sont mes assets statiques, je fais ceci:
router.route("/*").handler(StaticHandler.create())
server.requestHandler(router.&accept).listen(8080)
si jâajoute ceci Ă ma classe dâextensions:
/*
import io.vertx.groovy.core.http.HttpServer
import io.vertx.groovy.ext.web.handler.StaticHandler
*/
static void start(HttpServer self, Router router, Integer port, String staticPath) {
println("HttpServer is listening on " + port)
router.route(staticPath).handler(StaticHandler.create())
self.requestHandler(router.&accept).listen(port)
}
Maintenant je démarre mon serveur comme cela:
server.start(router, 8080, "/*")
Donc âŠ
Au final le code de notre projet devrait ressembler Ă ceci
def server = vertx.createHttpServer()
def router = Router.router(vertx)
router.route().handler(BodyHandler.create())
router.GET("/api/hi/:name", { context ->
context.sendJson([
"message":"Hi!",
"name": context.param("name").toString()
])
})
router.POST("/api/humans", { context ->
def obj = context.bodyAsJson(Object.class)
obj.id = new Random().nextInt(100)
context.sendJson(obj)
})
router.POST("/api/2/humans", { context ->
Human bob = context.bodyAsJson(Human.class)
bob.id = new Random().nextInt(100)
context.sendJson(bob)
})
server.start(router, 8080, "/*")
Du bon code de faignasse, facile Ă lire et Ă©crire ;)
@+
Tweet