軽量 Web フレームワークで REST API を実装 - Vert.x, Gretty, Play2 Mini, Socko, Restify

個人的に REST API の実装では JAX-RSJava*1SinatraRuby) あたりを使っていますが、今回は選択肢を増やす目的で下記のようなフレームワークを試してみました。

ちなみに、今回試した Java 系のフレームワーク(Restify 以外)は内部的に Netty を使っています。

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20130106/

はじめに

今回は以下のような単純な REST API を実装する事にします。

  • /user/ で JSON 文字列 を GET
  • /user で JSON 文字列 を POST

動作確認は、以下の Ruby スクリプトで行う事にします。

client.rb
#coding:utf-8
require 'net/http'
require 'json/pure'

Net::HTTP.start('localhost', 8080) { |http|
    # GET 処理
    res = http.get('/user/1')
    puts "#{res}, #{res.code}, #{res.content_type}, #{res.body}"

    data = {
        'name' => 'test',
        'note' => 'サンプル'
    }
    # POST 処理
    res = http.post('/user', JSON.generate(data), {'Content-Type' => 'application/json'})
    puts "#{res}, #{res.code}, #{res.body}"
}

Vert.x

  • Vert.x 1.3.0

id:fits:20120513 や id:fits:20120708 で扱った Vert.x です。

Vert.x は JVM 用の Node.js ライクなサーバーフレームワークで、JavaJavaScript・CoffeeScript・Groovy・RubyPython と多様な言語をサポートしています。

RouteMatcher を使えば Sinatra のように HTTP Method と URL パターンの組み合わせで処理を実装できます。

下記サンプルは Groovy で実装しました。

vert.x/server.groovy
import org.vertx.groovy.core.http.RouteMatcher
import org.vertx.java.core.json.impl.Json

def rm = new RouteMatcher()

rm.get '/user/:id', { req ->
    def res = req.response

    res.putHeader('Content-Type', 'application/json')
    res.end Json.encode([
        id: req.params['id'],
        name: 'vert.x sample'
    ])
}

rm.post '/user', { req ->
    req.bodyHandler {
        // JSON を Map へデコード
        def data = Json.decodeValue(it.toString(), Map)
        println data

        req.response.end()
    }
}

vertx.createHttpServer().requestHandler(rm.asClosure()).listen 8080

println "server started ..."
実行と動作確認結果(サーバー側)
> vertx run server.groovy
server started ...
[name:test, note:サンプル]
動作確認結果(クライアント側)
> jruby client.rb
#<Net::HTTPOK:0x889b125>, 200, application/json, {"id":"1","name":"vert.x sample"}
#<Net::HTTPOK:0x7ff843da>, 200,

Gretty

  • Gretty 0.4.302

Gretty は Netty をベースにしたサーバーフレームワークJava・Groovy・Scala をサポートしています。

下記サンプルは Groovy++ で実装していますが、Groovy++ は今のところ Groovy 2.0 をサポートしていないようで、Groovy 1.8 で実行する必要がありました。

gretty/server.groovy
/*
 *  Groovy 1.8 で実行する必要あり
 *  Groovy 2.0 では ExceptionInitilizerError が発生
 */
@GrabResolver(name = 'gretty', root = 'http://groovypp.artifactoryonline.com/groovypp/libs-releases-local/')
@Grab('org.mbte.groovypp:gretty:0.4.302')
import static java.nio.charset.StandardCharsets.*

import static org.mbte.gretty.JacksonCategory.*
import org.mbte.gretty.httpserver.GrettyServer

GrettyServer server = []

server.groovy = [
    localAddress: new InetSocketAddress('localhost', 8080),
    '/user/:id': {
        get {
            response.json = [
                id: request.parameters['id'],
                name: 'gretty sample'
            ]
        }
    },
    '/user': {
        post {
            /*
             * request.contentText を使うとプラットフォームの
             * デフォルトエンコードが使われるようなので
             * 明示的に UTF-8 で処理
             */
            def data = fromJson(Map, request.content.toString(UTF_8))
            println data

            response.json = ''
        }
    }
]

server.start()
実行と動作確認結果(サーバー側)
> groovy server.groovy
1 06, 2013 10:25:00 午前 org.mbte.gretty.AbstractServer
情報: Started server on localhost/127.0.0.1:8080
[name:test, note:サンプル]
動作確認結果(クライアント側)
> jruby client.rb
#<Net::HTTPOK:0x12da7d5f>, 200, application/json, {"id":"1","name":"gretty sample"}
#<Net::HTTPOK:0x889b125>, 200,

Play2 Mini

  • Play2 Mini 2.0.3

Play2 Mini は Play2 をベースにした簡易フレームワークで、JavaScala をサポートしています。

Scala で URL パスを正規表現マッチさせるには Through を使います。 *2
Through は引数の種類によってブロックに渡る引数の内容(下記サンプルの groups: List[String])が異なります。

Throughの引数 ブロックの引数 Throughの引数例 URL例 ブロックの引数例(List[String])
正規表現 正規表現のグループ "/user/(.*)".r /user/a/b ["a/b"]
文字列 指定の文字列以降を "/" でスプリットしたもの "/user" /user/a/b ["a", "b"]

なお、Play2 Mini では以下のようにして実装します。

  • (1) com.typesafe.play.mini.Application トレイトを extends して route を実装
  • (2) (1) を指定した com.typesafe.play.mini.Setup を extends したグローバルパッケージの Global を作成
  • (3) play.core.server.NettyServer で実行
play-mini/Server.scala (1)
package fits.sample

import com.typesafe.play.mini._
import play.api.mvc._
import play.api.mvc.Results._
import play.api.libs.json._

object Server extends Application {
    def route = Routes(
        Through("/user/([^/]*)".r) { groups: List[String] =>
            Action {
                val id :: Nil = groups

                Ok(Json.toJson {
                    Map(
                        "id" -> id,
                        "name" -> "play-mini sample"
                    )
                })
            }
        },
        {
            case POST(Path("/user")) => Action { req =>
                val data = req.body.asJson
                data.foreach(println)

                Ok("")
            }
        }
    )
}
play-mini/Global.scala (2)
object Global extends com.typesafe.play.mini.Setup(fits.sample.Server)
play-mini/build.sbt (3)
scalaVersion := "2.9.2"

resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"

libraryDependencies += "com.typesafe" %% "play-mini" % "2.0.3"

mainClass in (Compile, run) := Some("play.core.server.NettyServer")

デフォルトのポート番号 9000 ではなく 8080 で実行するため、sbt run 時に -Dhttp.port=8080 を指定します。

実行と動作確認結果(サーバー側)
> sbt -Dhttp.port=8080 run
・・・
[info] play - Listening for HTTP on port 8080...
{"name":"test","note":"サンプル"}
動作確認結果(クライアント側)
> jruby client.rb
#<Net::HTTPOK:0x7ff843da>, 200, application/json, {"id":"1","name":"play-mini sample"}
#<Net::HTTPOK:0x1d7ae341>, 200,

Socko

  • Socko 0.2.3

Socko は Netty と Akka をベースとした Web サーバーフレームワークです。
Akka の Actor として処理を実装するので、多少コード量が多くなります。

socko/Server.scala
package fits.sample

import scala.util.parsing.json.{JSON, JSONObject}

import org.mashupbots.socko.events.HttpRequestEvent
import org.mashupbots.socko.routes.{Routes, GET, POST}
import org.mashupbots.socko.webserver.{WebServer, WebServerConfig}

import akka.actor.{Actor, ActorSystem, Props}

object Server extends App {
    class UserGetHandler extends Actor {
        def receive = {
            case req: HttpRequestEvent =>
                val path = req.request.endPoint.path
                val id = path.replace("/user/", "").split("/").head

                req.response.write(
                    JSONObject(
                        Map(
                            "id" -> id,
                            "name" -> "socko sample"
                        )
                    ).toString(), 
                    "application/json"
                )
                context.stop(self)
        }
    }

    class UserPostHandler extends Actor {
        def receive = {
            case req: HttpRequestEvent =>
                val content = req.request.content.toString
                val data = JSON.parseFull(content)

                data.foreach(println)

                req.response.write("")
                context.stop(self)
        }
    }

    val actorSystem = ActorSystem("SampleActorSystem")

    val routes = Routes({
        case GET(req) => actorSystem.actorOf(Props[UserGetHandler]) ! req

        case POST(req) if req.endPoint.path == "/user" =>
            actorSystem.actorOf(Props[UserPostHandler]) ! req
    })

    val server = new WebServer(WebServerConfig(port = 8080), routes, actorSystem)
    server.start()

    Runtime.getRuntime.addShutdownHook(new Thread() {
        override def run { server.stop() }
    })
}
socko/build.sbt
scalaVersion := "2.9.2"

resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"

libraryDependencies += "org.mashupbots.socko" %% "socko-webserver" % "0.2.3"

mainClass in (Compile, run) := Some("fits.sample.Server")
実行と動作確認結果(サーバー側)
> sbt run
・・・
[info] Running fits.sample.Server
11:14:00.300 [run-main] INFO  o.m.socko.webserver.WebServer - Socko server 'WebServer' started on localhost:8080
11:14:21.032 [New I/O  worker #1] DEBUG o.m.socko.webserver.RequestHandler - HTTP EndPoint(GET,localhost:8080,/user/1) CHANNEL=-384208271
11:14:21.098 [New I/O  worker #1] DEBUG o.m.socko.webserver.RequestHandler - HTTP EndPoint(POST,localhost:8080,/user) CHANNEL=-384208271
Map(name -> test, note -> サンプル)
動作確認結果(クライアント側)
> jruby client.rb
#<Net::HTTPOK:0x7ff843da>, 200, application/json, {"id" : "1", "name" : "socko sample"}
#<Net::HTTPOK:0x6eddcf85>, 200,

Restify

  • Restify 1.4.2

Restify は Express によく似た Node.js 用のフレームワークで、REST API の実装に特化しています。

最新バージョンは Restify 2.0.4 でしたが、Windows OS 上の npm install に失敗するので、今回は古いバージョンを使っています。

app.coffee
restify = require 'restify'

server = restify.createServer()

server.use restify.bodyParser()

server.get '/user/:id', (req, res, next) ->
    res.json
        id: req.params.id
        name: 'restify sample'

    next()

server.post '/user', (req, res, next) ->
    data = JSON.parse req.body
    console.log data

    res.json null
    next()

server.listen 8080, -> console.log "server started ..."
実行と動作確認結果(サーバー側)
> coffee app.coffee
server started ...
{ name: 'test', note: 'サンプル' }
動作確認結果(クライアント側)
> jruby client.rb
#<Net::HTTPOK:0x14980563>, 200, application/json, {"id":"1","name":"restify sample"}
#<Net::HTTPOK:0x42eded9>, 200,

*1:Jersey, RESTEasy 等

*2:Java で実装する場合は JAX-RS と同様にアノテーション(@URL)で URL のパスを指定するようです