CLOVER🍀

That was when it all began.

JAX-RS Client(RESTEasy)にJackson Scala Moduleを適用する

以前、RESTEasyでJackson Scala Moduleを適用するエントリを書きました。

RESTEasyでJackson Scala Moduleを使用する
http://d.hatena.ne.jp/Kazuhira/20141101/1414838251

こちらはサーバーサイドだったので、今度はクライアントサイドでJackson Scala Moduleを適用してみたいと思います。

結果から言うと、サーバーサイドとほぼ同じ方法で適用できました。少しの違いはありましたけど。

準備

まずはビルド定義。
build.sbt

name := "resteasy-client-jackson2-scala"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.6"

organization := "org.littlewings"

updateOptions := updateOptions.value.withCachedResolution(true)

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

libraryDependencies ++= Seq(
  "org.jboss.resteasy" % "resteasy-client" % "3.0.11.Final",
  "org.jboss.resteasy" % "resteasy-jackson2-provider" % "3.0.11.Final",
  "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.5.2",

  "org.jboss.resteasy" % "resteasy-jdk-http" % "3.0.11.Final" % "test",
  "org.scalatest" %% "scalatest" % "2.2.5" % "test"
)

主題になっているのは「resteasy-client」、「resteasy-jackson2-provider」、「jackson-module-scala」ですが、テストコード用に「resteasy-jdk-http」とScalaTestを依存関係に追加しています。

リクエスト/レスポンスに使うCase Class

いきなりですが、もうScala前提なのでリクエスト/レスポンスはCase Classで表現しましょう。
src/main/scala/org/littlewings/javaee7/resteasy/Person.scala

package org.littlewings.javaee7.resteasy

case class Person(firstName: String, lastName: String, age: Int)

Jackson Scala Moduleを適用するための、ContextResolverの追加

サーバーサイドでやった時にもContextResolverを用意しましたが、クライアントサイドでも同じようにContextResolverを使えばOKでした。
src/main/scala/org/littlewings/javaee7/resteasy/ScalaObjectMapperProvider.scala

package org.littlewings.javaee7.resteasy

import javax.ws.rs.{Consumes, Produces}
import javax.ws.rs.core.MediaType
import javax.ws.rs.ext.{Provider, ContextResolver}

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule

@Provider
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
class ScalaObjectMapperProvider extends ContextResolver[ObjectMapper] {
  override def getContext(typ: Class[_]): ObjectMapper = {
    val objectMapper = new ObjectMapper
    objectMapper.registerModule(DefaultScalaModule)
    objectMapper
  }
}

ただ、クライアントサイドの場合はこれだけでは足りなくて、META-INF/services配下に以下のようなファイルを用意することになりました。
src/main/resources/META-INF/services/javax.ws.rs.ext.Providers

org.littlewings.javaee7.resteasy.ScalaObjectMapperProvider

なので、このファイルを用意したということは、正確にはContextResolverの@Providerアノテーションはなくてもいいんですけどね。

@Provider
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
class ScalaObjectMapperProvider extends ContextResolver[ObjectMapper] {

まあ、サーバー側で使う時はきっと付与するだろうということで。

テストしてみる

あとは、これに対するテストコードを書いてみます。せっかくなので、JAX-RSを使ったサーバーサイドを使ったテストにしました。
src/test/scala/org/littlewings/javaee7/resteasy/RestEasyJdkHttpServerSpecSupport.scala

package org.littlewings.javaee7.resteasy

import java.net.InetSocketAddress
import javax.ws.rs.core.MediaType
import javax.ws.rs.{POST, Path, Produces}

import com.sun.net.httpserver.HttpServer
import org.jboss.resteasy.plugins.server.sun.http.HttpContextBuilder
import org.scalatest.{BeforeAndAfterAll, Suite}

trait RestEasyJdkHttpServerSpecSupport extends Suite with BeforeAndAfterAll {
  val server: HttpServer = HttpServer.create(new InetSocketAddress(8080), 10)

  override def beforeAll(): Unit = {
    val contextBuilder = new HttpContextBuilder
    contextBuilder.getDeployment.getActualResourceClasses.add(classOf[PersonResource])

    val context = contextBuilder.bind(server)

    server.start()
  }

  override def afterAll(): Unit = {
    server.stop(0)
  }
}

@Path("person")
class PersonResource {
  @POST
  @Produces(Array(MediaType.APPLICATION_JSON))
  def accept(person: Person): Person =
    Person("ワカメ", person.lastName, 9)
}

テストクラスの開始時にJDKのHttpServerを起動して、終了時に停止します。

あと、リソースクラスも一応用意。

@Path("person")
class PersonResource {
  @POST
  @Produces(Array(MediaType.APPLICATION_JSON))
  def accept(person: Person): Person =
    Person("ワカメ", person.lastName, 9)
}

リクエストの内容を何も含めないのも確認としては微妙なので、Case Classの一部の項目だけ引っ張ってきました。

そして、今回作成したコードを使用したテスト。
src/test/scala/org/littlewings/javaee7/resteasy/RestEasyWithJackson2WithScalaSpec.scala

package org.littlewings.javaee7.resteasy

import javax.ws.rs.client.{Entity, ClientBuilder}

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

class RestEasyWithJackson2WithScalaSpec extends FunSpec with RestEasyJdkHttpServerSpecSupport {
  describe("RESTEasy with Jackson2 with ScalaModule") {
    it("case class") {
      val client = ClientBuilder.newBuilder.build

      try {
        val response =
          client
            .target("http://localhost:8080/person")
            .request
            .post(Entity.json(Person("カツオ", "磯野", 11)))

        response.getStatus should be(200)
        response.readEntity(classOf[Person]) should be(Person("ワカメ", "磯野", 9))

        response.close()
      } finally {
        client.close()
      }
    }
  }
}

一応、動きましたよっと。
※Jackson Scala Moduleが効いていないと、Case Classの扱いに失敗するので

これでクライアントサイドでもサーバーサイドでも、Jackson2+Jackson Scala Moduleの組み合わせが使えるようになりました。

クライアントサイド、Scalaでやる時でもDispatchからこっちに移るべきかなぁ?

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/resteasy-client-jackson2-scala