最近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を動かすことができました。