CLOVER🍀

That was when it all began.

RESTEasyでJackson Scala Moduleを使用する

JAX-RSJSONを使う時に、JSON変換部にJackson、かつScala Moduleを使ってみようと思いまして。

Add-on module for Jackson to support Scala-specific datatypes
https://github.com/FasterXML/jackson-module-scala

利用するアプリケーションサーバは、WildFly 8.1.0.Finalとします。

とりあえず、まずは普通にScalaJSONを使ったJAX-RSのサンプルを書いてみます。

build.sbt

name := "resteasy-jackson2-scala"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.4"

organization := "org.littlewings"

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

incOptions := incOptions.value.withNameHashing(true)

jetty()

artifactName := { (version: ScalaVersion, module: ModuleID, artifact: Artifact) =>
  //artifact.name + "." + artifact.extension
  "javaee7-web." + artifact.extension
}

val jettyVersion = "9.2.3.v20140905"

libraryDependencies ++= Seq(
  "org.eclipse.jetty" % "jetty-webapp" % jettyVersion % "container",
  "org.eclipse.jetty" % "jetty-plus"   % jettyVersion % "container",
  "javax" % "javaee-web-api" % "7.0" % "provided"
)

xsbt-web-pluginも使用します。
project/plugins.sbt

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "1.0.0")

JAX-RSの有効化。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplication.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.ApplicationPath
import javax.ws.rs.core.Application

@ApplicationPath("rest")
class JaxrsApplication extends Application

リソースクラス。
src/main/scala/org/littlewings/javaee7/rest/HelloResource.scala

package org.littlewings.javaee7.rest

import scala.beans.BeanProperty

import javax.ws.rs.{Consumes, Path, POST, Produces}
import javax.ws.rs.core.MediaType

@Path("hello")
class HelloResource {
  @POST
  @Consumes(Array(MediaType.APPLICATION_JSON))
  @Produces(Array(MediaType.APPLICATION_JSON))
  def index(book: Book): Message = {
    val message = new Message
    message.value = s"あなたのリクエストした本の名前は、 [${book.name}] です"
    message
  }
}

class Book {
  @BeanProperty
  var name: String = _
  @BeanProperty
  var price: Int = _
}

class Message {
  @BeanProperty
  var value: String = _
}

普通に書くと、こんな感じですね。WildFlyにデプロイして、動作確認してみます。

$ cp target/scala-2.11/javaee7-web.war /path/to/wildfly-8.1.0.Final/standalone/deployments/

結果。

$ curl -X POST -d '{"name": "Scalaの書籍", "price": 500}'  -H 'Content-Type: application/json' http://localhost:8080/javaee7-web/rest/hello
{"value":"あなたのリクエストした本の名前は、 [Scalaの書籍] です"}

動いていますね。

とはいえ、ScalaでやるならCase Classを使ってみたくなるものです。

というわけで、こう変更してみます。
src/main/scala/org/littlewings/javaee7/rest/HelloResource.scala

package org.littlewings.javaee7.rest

import scala.beans.BeanProperty

import javax.ws.rs.{Consumes, Path, POST, Produces}
import javax.ws.rs.core.MediaType

@Path("hello")
class HelloResource {
  @POST
  @Consumes(Array(MediaType.APPLICATION_JSON))
  @Produces(Array(MediaType.APPLICATION_JSON))
  def index(book: Book): Message =
    Message(value = s"あなたのリクエストした本の名前は、 [${book.name}] です")
}

case class Book(name: String, price: Int)
case class Message(value: String)

すごいシンプルになった(笑)。

が、これをデプロイしても、JacksonはScalaのCase Classを理解できないので、このままでは動作しません。

$ curl -X POST -d '{"name": "Scalaの書籍", "price": 500}'  -H 'Content-Type: application/json' http://localhost:8080/javaee7-web/rest/hello
com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class org.littlewings.javaee7.rest.Book]: can not instantiate from JSON object (need to add/enable type information?)
 at [Source: io.undertow.servlet.spec.ServletInputStreamImpl@8e23352; line: 1, column: 2]

裏で思い切りコケています。

19:18:10,919 WARN  [org.jboss.resteasy.core.ExceptionHandler] (default task-9) Failed executing POST /hello: org.jboss.resteasy.spi.ReaderException: com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class org.littlewings.javaee7.rest.Book]: can not instantiate from JSON object (need to add/enable type information?)

ここで、JacksonにScalaフレンドリーになってもらうために、JacksonのScala Moduleを使用します。

build.sbtの依存関係に、Jackson Scala Moduleを追加。

libraryDependencies ++= Seq(
  "org.eclipse.jetty" % "jetty-webapp" % jettyVersion % "container",
  "org.eclipse.jetty" % "jetty-plus"   % jettyVersion % "container",
  "javax" % "javaee-web-api" % "7.0" % "provided",
  "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.4.3"
)

JAX-RSのContextResolverインターフェースを実装したクラスを作成し、ここでObjectMapperを作成するようにします。
src/main/scala/org/littlewings/javaee7/rest/ScalaObjectMapperProvider.scala

package org.littlewings.javaee7.rest

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

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
  }
}

この時、DefaultScalaModuleを足しておきます。

蛇足)
@Providerアノテーションを付与せずに、以下のファイルを作成しても動作します
src/main/resources/META-INF/services/javax.ws.rs.ext.Providers

org.littlewings.javaee7.rest.ScalaObjectMapperProvider

中身には、作成したクラスの名前を書いておきます。今回は、このファイルは作成せずにアノテーションで頑張ります。

それから再度パッケージングしてデプロイ。動作確認。

ところが、WildFly 8.1.0.Finalの素のままだと、今回作成したWARファイルはJSONを解釈する時にエラーになります。

19:25:34,897 ERROR [io.undertow.request] (default task-1) UT005023: Exception handling request to /javaee7-web/rest/hello: java.lang.NoSuchMethodError: com.fasterxml.jackson.databind.introspect.POJOPropertyBuilder.addField(Lcom/fasterxml/jackson/databind/introspect/AnnotatedField;Lcom/fasterxml/jackson/databind/PropertyName;ZZZ)V
	at com.fasterxml.jackson.module.scala.introspect.ScalaPropertiesCollector.com$fasterxml$jackson$module$scala$introspect$ScalaPropertiesCollector$$_addField(ScalaPropertiesCollector.scala:109) [jackson-module-scala_2.11-2.4.3.jar:]
	at com.fasterxml.jackson.module.scala.introspect.ScalaPropertiesCollector$$anonfun$_addFields$2$$anonfun$apply$11.apply(ScalaPropertiesCollector.scala:100) [jackson-module-scala_2.11-2.4.3.jar:]
	at com.fasterxml.jackson.module.scala.introspect.ScalaPropertiesCollector$$anonfun$_addFields$2$$anonfun$apply$11.apply(ScalaPropertiesCollector.scala:99) [jackson-module-scala_2.11-2.4.3.jar:]
	at scala.Option.foreach(Option.scala:256) [scala-library-2.11.4.jar:]

これは、RESTEasyのJackson依存がWildFly 8.1.0.Finalに同梱のものだと2.3.2で、Jackson Scala Moduleが依存しているJacksonが2.4系なことが原因な気がします。

むー、小癪な…。

そこで、RESTEasyのサイトより最新(?)の3.0.9.Finalをダウンロードしてきて、1度WildFlyをシャットダウンした後に、WildFlyのモジュールディレクトリに展開します。

$ cd /path/to/wildfly-8.1.0.Final/modules/system/layers/base
$ yes | unzip resteasy-jboss-modules-wf8-3.0.9.Final.zip

RESTEasy 3.0.9.Final以上であれば、Jacksonは2.4系を使用しています。

resteasy-jboss-modules-wf8-3.0.9.Final.zipは、resteasy-jaxrs-3.0.9.Final-all.zipを展開するとその中に含まれています。

zipファイルの展開時に、なんか重複したファイルがありましたが、今回は強引に上書きしてしまいました…。

再度WildFlyを起動して、実行。

$ curl -X POST -d '{"name": "Scalaの書籍", "price": 500}'  -H 'Content-Type: application/json' http://localhost:8080/javaee7-web/rest/hello
{"value":"あなたのリクエストした本の名前は、 [Scalaの書籍] です"}

今度はOKでした!!

ちょっとハマりましたが、なんとかJackson Scala ModuleをRESTEasyと合わせて動かせました。予定外に、JAX-RSの勉強にもなりましたけれど。

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

追記)
クライアントサイドも書きました。

JAX-RS Client(RESTEasy)にJackson Scala Moduleを適用する
http://d.hatena.ne.jp/Kazuhira/20150523/1432394997