ちょっと思い立って、なんとなくやってみたくなりまして。
組み込みTomcat上で、JAX-RSとCDIを合わせて使ってみようというお話。これを試すにあたり、条件は以下とします。
とまあ、あくまで普通のスタンドアロンなJavaアプリのノリでいきます。組み込みTomcat使うわけですし…。
で、やってみていろいろ苦労しましたが…参考にした情報も載せながら書いてきたいと思います。
ビルド定義
まずは、sbtの定義。こんな感じになりました。
build.sbt
name := "embedded-tomcat-jaxrs-cdi" version := "0.0.1-SNAPSHOT" organization := "org.littlewings" scalaVersion := "2.11.6" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") fork in run := true connectInput := true val tomcatVersion = "8.0.20" val resteasyVersion = "3.0.10.Final" libraryDependencies ++= Seq( "org.apache.tomcat.embed" % "tomcat-embed-core" % tomcatVersion, "org.apache.tomcat.embed" % "tomcat-embed-jasper" % tomcatVersion, "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % tomcatVersion, "org.jboss.resteasy" % "resteasy-servlet-initializer" % resteasyVersion, "org.jboss.resteasy" % "resteasy-cdi" % resteasyVersion, "org.jboss.weld.servlet" % "weld-servlet" % "2.2.9.Final" )
「resteasy-cdi」を付けるとweldも引っ張ってくるみたいなのですが、バージョンが2.1なのと「weld-core」と「weld-se」のみだったので、ここは「weld-servlet」と2.2系を指定。
あとは、コードを書きながら試しやすいように、forkを有効にしています。これをやらずに組み込みTomcatを2回以上起動すると、URL#setURLStreamHandlerFactoryを2回以上呼び出して失敗します…。
このあたりの依存関係の定義は、こちらを参照して設定しています。
Standalone Resteasy in Servlet 3.0 Containers(RESTEasyをServlet 3.0対応のコンテナで使う)
http://docs.jboss.org/resteasy/docs/3.0.9.Final/userguide/html_single/index.html#d4e111
Servlet containers (such as Tomcat or Jetty)(よーく見ると、weld-servlet.jarが要るよって書いています)
http://docs.jboss.org/weld/reference/latest-2.2/en-US/html_single/#_servlet_containers_such_as_tomcat_or_jetty
JAX-RSリソースとCDI管理Beanの作成
こちらは、さくっと普通のクラスを作ります。簡単に足し算の例で。
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/CalcResource.scala
package org.littlewings.javaee7.rest import javax.inject.Inject import javax.ws.rs.{ GET, Path, Produces, QueryParam } import javax.ws.rs.core.MediaType import org.littlewings.javaee7.service.CalcService @Path("calc") class CalcResource { @Inject private var calcService: CalcService = _ @GET @Path("add") @Produces(Array(MediaType.TEXT_PLAIN)) def add(@QueryParam("a") a: Int, @QueryParam("b") b: Int): Int = calcService.add(a, b) }
CDI管理Bean。
src/main/scala/org/littlewings/javaee7/service/CalcService.scala
package org.littlewings.javaee7.service import javax.enterprise.context.RequestScoped @RequestScoped class CalcService { def add(a: Int, b: Int) = a + b }
このあたりは、いたって単純に。
CDIの有効化
今回の構成だと、空でいいのでbeans.xmlが必要です。以下に作成します。
src/main/resources/META-INF/beans.xml
WEB-INF配下ではありません。
組み込みTomcatの設定と起動
では、組み込みTomcatを起動と、今回のJAX-RS(RESTEasy)とCDIを動作させるためのコードを書きます。
最終的には、こんな感じになりました。
src/main/scala/org/littlewings/javaee7/TomcatBootstrap.scala
package org.littlewings.javaee7 import scala.io.StdIn import java.io.File import org.apache.catalina.startup.Tomcat import org.apache.tomcat.util.descriptor.web.ContextResource object TomcatBootstrap { def main(args: Array[String]): Unit = { val port = 8080 val tomcat = new Tomcat // ポートはデフォルトで8080 tomcat.setPort(port) try { // ベースのディレクトリ、DocbaseはSpring Bootを参考に tomcat.setBaseDir(createTempDir("tomcat", port).getAbsolutePath) val context = tomcat.addWebapp("", createTempDir("tomcat-docbase", port).getAbsolutePath) // CDIでWEB-INF/classesに配置されていなくても対象とされる、「flat」に設定 context.addParameter("org.jboss.weld.environment.servlet.archive.isolation", "false") // RESTEasyとCDIの統合 context.addParameter("resteasy.injector.factory", "org.jboss.resteasy.cdi.CdiInjectorFactory") // 組み込みTomcatでJNDIを有効に tomcat.enableNaming() // BeanManaerをJNDIリソースとして定義 val resource = new ContextResource resource.setAuth("Container") resource.setName("BeanManager") resource.setType("javax.enterprise.inject.spi.BeanManager") resource.setProperty("factory", "org.jboss.weld.resources.ManagerObjectFactory") context.getNamingResources.addResource(resource) // Tomcatの起動 tomcat.start() // Enter打ったら終了 StdIn.readLine("> Enter stop") // 普通、待機はこっち // tomcat.getServer.await() } finally { // Tomcatの破棄と停止 tomcat.stop() tomcat.destroy() } } def createTempDir(prefix: String, port: Int): File = { val tempDir = File.createTempFile(s"${prefix}.", s".${port}") tempDir.delete() tempDir.mkdir() tempDir.deleteOnExit() tempDir } }
コメントでだいたい書いているのですが、ちょっと説明。
組み込みTomcatを使うので、まずはTomcatのインスタンスを作成。
val port = 8080 val tomcat = new Tomcat // ポートはデフォルトで8080 tomcat.setPort(port)
ポートは、デフォルトで8080です。
Tomcatのベースディレクトリ、ドキュメントベースの設定。
// ベースのディレクトリ、DocbaseはSpring Bootを参考に tomcat.setBaseDir(createTempDir(tomcat, "tomcat").getAbsolutePath) val context = tomcat.addWebapp("", createTempDir(tomcat, "tomcat-docbase").getAbsolutePath)
組み込みTomcatさんは、必ずこれらのディレクトリを要求するみたいなので、少し考えた挙句、Spring Bootを参考にすることにしました。というか、まんまです。
def createTempDir(prefix: String, port: Int): File = { val tempDir = File.createTempFile(s"${prefix}.", s".${port}") tempDir.delete() tempDir.mkdir() tempDir.deleteOnExit() tempDir }
起動すると、Linuxなら「/tmp」配下にtomcat-docbase.〜.8080、tomcat.〜.8080みたいなディレクトリができます。アプリケーションを終了すると、削除されますが。
https://github.com/spring-projects/spring-boot/blob/v1.2.2.RELEASE/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java#L142
https://github.com/spring-projects/spring-boot/blob/v1.2.2.RELEASE/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java#L160
https://github.com/spring-projects/spring-boot/blob/v1.2.2.RELEASE/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java#L381
今回はWEB-INFとか作っていませんが、それでもサーブレットコンテナ上で管理対象とするために、「flat deployment structure」という設定にします。
// CDIでWEB-INF/classesに配置されていなくても対象とされる、「flat」に設定 context.addParameter("org.jboss.weld.environment.servlet.archive.isolation", "false")
Bean Archive Isolation
http://docs.jboss.org/weld/reference/latest-2.2/en-US/html_single/#_bean_archive_isolation
これ、本来Webアプリケーションごとに管理対象を分けるためのものなんでしょうけど、こういう使い方だとまあ、いいかぁと。
RESTEasyとCDIの統合。
// RESTEasyとCDIの統合 context.addParameter("resteasy.injector.factory", "org.jboss.resteasy.cdi.CdiInjectorFactory")
Configuration with different distributions
http://docs.jboss.org/resteasy/docs/3.0.9.Final/userguide/html_single/index.html#d4e2093
ちなみに、Context#addParameterというのは、web.xmlでいうcontext-paramの追加ですね。
あとは、組み込みTomcatではデフォルトで無効になっているJNDIルックアップを有功にして
// 組み込みTomcatでJNDIを有効に
tomcat.enableNaming()
BeanManagerを、JNDIリソースに登録します。
// BeanManaerをJNDIリソースとして定義 val resource = new ContextResource resource.setAuth("Container") resource.setName("BeanManager") resource.setType("javax.enterprise.inject.spi.BeanManager") resource.setProperty("factory", "org.jboss.weld.resources.ManagerObjectFactory") context.getNamingResources.addResource(resource)
Servlet containers (such as Tomcat or Jetty) / Tomcat
http://docs.jboss.org/weld/reference/latest/en-US/html/environments.html#_tomcat
このあたりのやり方は、以前Spring Bootで試していたのでまあすんなりと。
組み込みTomcat(on Spring Boot)でJNDIリソースを扱う
http://d.hatena.ne.jp/Kazuhira/20141227/1419709045
あとは、Tomcatを起動します。
// Tomcatの起動
tomcat.start()
Webアプリケーションの設定を何もしていませんが、「resteasy-servlet-initializer」を依存関係に登録しているので、ServletContainerInitializerで自動的に頑張ってくれるみたいです。
このアプリケーションを終わらせるためには、Enterを打って終了。
// Enter打ったら終了 StdIn.readLine("> Enter stop") // 普通、待機はこっち // tomcat.getServer.await()
通常は、Server#awaitして待ち続けるものでしょうけれど。
最後にTomcatの破棄と停止をして終了です。
} finally { // Tomcatの破棄と停止 tomcat.stop() tomcat.destroy() }
動かしてみる
それでは、動かしてみます。
> run
確認。
$ curl "http://localhost:8080/rest/calc/add?a=5&b=8" 13
OKそうですね!
まとめ
というわけで、組み込みTomcat上でなんとかJAX-RSとCDIを繋げることができました。いろいろハマりましたけど…。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/embedded-tomcat-jaxrs-cdi
あとはFat JARにできればいいのですが、sbt-assemblyとかで単純にやるとMETA-INFに置いているbeans.xmlがライブラリと被ったりすることに…。
いったん、ここでおしまい。
追記)
Fat JAR化しました。sbtではなくて、Mavenになりましたけど。
組み込みTomcat+JAX-RS+CDIを、Fat JARとして動かす
http://d.hatena.ne.jp/Kazuhira/20150308/1425828178