以前、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