JAX-RSでJSONを使う時に、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とします。
とりあえず、まずは普通にScalaでJSONを使った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