CLOVER🍀

That was when it all began.

ScalaのHTTP通信ライブラリ、Dispatchを使ってみる

最近GroovyのHTTPBuilderをよく使っていますが、ちょっと思うところがあってScalaでのHTTP通信ライブラリを使ってみました。そういえば、Scalaを使ってHTTP通信のコードを書くのは、初めてな気がします。

今回使うのは、有名っぽいDispatchです。

Dispatch
http://dispatch.databinder.net/Dispatch.html

バージョンは0.11ですが、以前の0.8のものはdispatch-classicと呼ばれているらしいです。

dispatch-classic
http://dispatch-classic.databinder.net/Dispatch.html

名前はちょこちょこ聞いていましたが、使う機会がなかったので今回試してみようと思います。

特徴としては、内部的にAsync Http Clientを使った非同期通信ライブラリで、結果はScalaのFutureで返ってくるスタイルを取っているようです。

Async Http Client
https://github.com/AsyncHttpClient/async-http-client

準備

依存関係の定義は、こんな感じで。

libraryDependencies ++= Seq(
  "net.databinder.dispatch" %% "dispatch-core" % "0.11.0",
  "org.scalatest" %% "scalatest" % "2.0" % "test"
)

テストコードを書くので、ScalaTestを足しています。

使ってみる

とりあえず、ドキュメントを見ながら簡単なコードを書いてみましょう。

トップページに、Getting Started的な内容が書かれています。
http://dispatch.databinder.net/Dispatch.html

src/test/scala/org/littlewings/dispatch/DispatchGettingStartedSpec.scala

package org.littlewings.dispatch

import dispatch._
import dispatch.Defaults._

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class DispatchGettingStartedSpec extends FunSpec {
  describe("dispatch getting-started spec") {
    // ここに、テストコードを書く!!
  }
}

下準備としては、以下のimport文を加えることのようです。

import dispatch._
import dispatch.Defaults._

ドキュメントでは相対importで書かれていますが、自分は相対importではあまり書かないので…。

では、Googleのトップページにアクセスするコードを書いていってみましょう。

単純な例、その1。

    it("Access Google Top#1") {
      val request = url("https://www.google.co.jp/")
      val googleTop = Http(request OK as.String)
      googleTop() should include ("<title>Google</title>")
    }

ここで、変数googleTopはScalaのFuture(というかPromise)が格納されています。同期的にやりたいのなら、何かしらの形で待ち合わせが必要ということになります。

ScalaのFutureにはapplyメソッドはありませんが、Implicit Conversionが入ってDispatchのEnrichFutureに変換されることによってapplyが呼び出せるようになっています。

なお、applyメソッドは無限に待ち合わせを行います。

  def apply() = Await.result(underlying, Duration.Inf)

話がそれましたね、では続きを。

単純な例、その2。

    it("Access Google Top#2") {
      val request = url("https://www.google.co.jp/")
      for (res <- Http(request OK as.String))
        res should include ("<title>Google</title>")
    }

for式で結果を取得できていますが、ScalaTestのコードの書き方としては意味がありません。その点については、ご注意を。

単純な例、その3。その1と変わりませんが、そのままapplyで結果を取り出しています。

    it("Access Google Top#3") {
      val request = url("https://www.google.co.jp/")
      Http(request OK as.String).apply() should include ("<title>Google</title>")
    }

先ほどから登場している、

as.String

はここで定義されているものです。

https://github.com/dispatch/reboot/tree/master/core/src/main/scala/as

また、OKというのはこちらに定義されています。

https://github.com/dispatch/reboot/blob/master/core/src/main/scala/handlers.scala

ドキュメントが充実しているわけでもないというか、Scaladocが見当たらないので適度にソースを見た方がいいですかね?

ホスト部だけを、取り出して定義することもできます。あとで、「/」を使ったリクエストの構築も記述します。

    it("Path Access Google Top") {
      val request = host("www.google.co.jp").secure
      Http(request OK as.String).apply() should include ("<title>Google</title>")
    }

secureが付いているのは、HTTPSだからですね。

背後のAsync Http Clientのシャットダウンを行うには、HttpExecutor#shutdownを呼べばよい?

    it("Secure Access & Cleanup") {
      val request = host("www.google.co.jp").secure
      val googleTop = Http(request OK as.String)
      googleTop() should include ("<title>Google</title>")
      Http.shutdown()  // async-http-clientをシャットダウン
    }

Async Http Clientは内部的にNettyを使っているようで、Dispatchがシャットダウンする時にNettyのリソース解放は行ってくれてはいるみたいですが。

パラメータを使ったアクセスなど

では、パラメータの付与などもうちょっといろいろ試してみましょう。ドキュメント全体や、コードを参照しつつ…。

Combined Pages
http://dispatch.databinder.net/Combined+Pages.html

src/test/scala/org/littlewings/dispatch/SendParameterSpec.scala

package org.littlewings.dispatch

import dispatch._
import dispatch.Defaults._

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class SendParameterSpec extends FunSpec {
  // ここに、テストコードを書く
}

パラメータの話を最初にしたわりには、まずはパス区切りでのリクエスト構築。こんな指定方法もできるようです。

  describe("simple spec") {
    it("simple get") {
      val request = host("localhost", 8080) / "javaee6-web" / "simple"
      Http(request OK as.String).apply() should be ("Request: []")
    }
  }

ここから、サーバをローカルのJBoss AS 7.1.1に切り替えました。

では、今渡こそパラメータの送信のサンプルを記述します。

ちゃんとパラメータが送れていることを確認するために、こんなサーブレットを用意しました。

src/main/scala/org/littlewings/servlet/SimpleServlet.scala 
package org.littlewings.servlet

import scala.collection.JavaConverters._

import java.io.IOException

import javax.servlet.ServletException
import javax.servlet.annotation.{WebServlet}
import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse}

@WebServlet(value = Array("/simple"))
class SimpleServlet extends HttpServlet {
  @throws(classOf[ServletException])
  @throws(classOf[IOException])
  override protected def doGet(req: HttpServletRequest, res: HttpServletResponse): Unit =
    execute(req, res)

  @throws(classOf[ServletException])
  @throws(classOf[IOException])
  override protected def doPost(req: HttpServletRequest, res: HttpServletResponse): Unit =
    execute(req, res)

  @throws(classOf[ServletException])
  @throws(classOf[IOException])
  private def execute(req: HttpServletRequest, res: HttpServletResponse): Unit = {
    req.setCharacterEncoding("UTF-8")
    res.setCharacterEncoding("UTF-8")
    res.getWriter.write {
      "Request: " +
      req.getParameterNames.asScala.map { name => println(s"$name=${req.getParameter(name)}"); s"$name=${req.getParameter(name)}" }.mkString("[", ", ", "]")
    }
  }
}

で、JBoss ASにデプロイ。

まずは、GETの例。

  describe("dispatch query parameter spec") {
    it("send get parameter addQueryParameter") {
      val request = url("http://localhost:8080/javaee6-web/simple")
      val requestWithParams =
        request
          .addQueryParameter("name1", "value1")
          .addQueryParameter("name2", "value2")
      Http(requestWithParams OK as.String).apply() should be ("Request: [name1=value1, name2=value2]")
    }

    it("send get parameter <<? Map") {
      val request = url("http://localhost:8080/javaee6-web/simple")
      val requestWithParams = request <<? Map("name1" -> "value1", "name2" -> "value2")
      Http(requestWithParams OK as.String).apply() should be ("Request: [name1=value1, name2=value2]")
    }

    it("send get parameter with multibyte") {
      val request = url("http://localhost:8080/javaee6-web/simple")
      val requestWithParams = request <<? Map("name1" -> "値1", "name2" -> "値2")
      new String(Http(requestWithParams OK as.Bytes).apply(), "UTF-8") should be ("Request: [name1=値1, name2=値2]")
    }
  }

addQueryParameterメソッド、もしくは<<?でパラメータを追加することができます。

一応マルチバイトのテストもやってみたのですが、as.Stringだと、キレイに文字化けしました。
普通のサイトのコンテンツだとas.Stringでも文字化けしなかったので、今回作ったサーブレットのContent-Typeとかの指定の問題な気がします。

続いて、POSTの例。

  describe("dispatch post parameter spec") {
    it("send post addParameter") {
      val request = url("http://localhost:8080/javaee6-web/simple")
      val requestWithParams =
        request
          .POST
          .addParameter("name1", "value1")
          .addParameter("name2", "value2")
      Http(requestWithParams OK as.String).apply() should be ("Request: [name1=value1, name2=value2]")
    }

    it("send post parameter << Map") {
      val request = url("http://localhost:8080/javaee6-web/simple")
      val requestWithParams = request << Map("name1" -> "value1", "name2" -> "value2")
      Http(requestWithParams OK as.String).apply() should be ("Request: [name1=value1, name2=value2]")
    }

    it("send post parameter << raw string") {
      val request = url("http://localhost:8080/javaee6-web/simple")
      request <:< Map("Content-Type" -> "application/x-www-form-urlencoded")
      val requestWithParams =
        request.POST <:< Map("Content-Type" -> "application/x-www-form-urlencoded") << "name1=value1&name2=value2"
      Http(requestWithParams OK as.String).apply() should be ("Request: [name1=value1, name2=value2]")
    }

    it("send post parameter with multibyte") {
      val request = url("http://localhost:8080/javaee6-web/simple")
      val requestWithParams = request << Map("name1" -> "値1", "name2" -> "値2")
      new String(Http(requestWithParams OK as.Bytes).apply(), "UTF-8") should be ("Request: [name1=値1, name2=値2]")
    }
  }

POSTの場合は、addParameter、<<でMapやStringを指定したりできます。

オマケ

レスポンスの内容をStringに変換したり

as.String

Byte配列に変換しているのは

as.Bytes

こういう定義があるからです。

object Response {
  def apply[T](f: client.Response => T) = f
}

object String extends (client.Response => String) {
  def apply(r: client.Response) = r.getResponseBody
}

object Bytes extends (client.Response => Array[Byte]) {
  def apply(r: client.Response) = r.getResponseBodyAsBytes
}

で、これのUTF-8に変換する版を作れば、先ほどのnew Stringしている部分はまとめられるということ?
*ちゃんとContent-Typeとか書けば、そもそも不要だと思いますが

というわけで、こんなのを定義。

object Utf8String extends (client.Response => String) {
  def apply(r: client.Response) = new String(r.getResponseBodyAsBytes, StandardCharsets.UTF_8)
}

これを含めたテストコードを書いてみます。
src/test/scala/org/littlewings/dispatch/MultiByteParameterSpec.scala

package org.littlewings.dispatch

import java.nio.charset.StandardCharsets

import dispatch._
import dispatch.Defaults._

import com.ning.http.client

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class MultiByteParameterSpec extends FunSpec {
  describe("dispatch query parameter spec") {
    it("send get parameter with multibyte") {
      val request = url("http://localhost:8080/javaee6-web/simple")
      val requestWithParams = request <<? Map("name1" -> "値1", "name2" -> "値2")
      Http(requestWithParams OK Utf8String).apply() should be ("Request: [name1=値1, name2=値2]")
    }
  }
}

object Utf8String extends (client.Response => String) {
  def apply(r: client.Response) = new String(r.getResponseBodyAsBytes, StandardCharsets.UTF_8)
}

結果、動作しました。これで、自分でレスポンスを結果に変換する定義が書けたわけですね。

とりあえず基礎的な範囲ですが、Dispatchを動かすことができました。