CLOVER🍀

That was when it all began.

組み込みTomcatでJAX-RS(RESTEasy)とCDIを使う

ちょっと思い立って、なんとなくやってみたくなりまして。

組み込みTomcat上で、JAX-RSCDIを合わせて使ってみようというお話。これを試すにあたり、条件は以下とします。

  • JAX-RSの実装はRESTEasy、CDIの実装はWeld
  • WEB-INF/classesとかは作らない
  • Scalaで書く

とまあ、あくまで普通のスタンドアロン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-RSCDIを繋げることができました。いろいろハマりましたけど…。

今回作成したコードは、こちらに置いています。
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になりましたけど。

組み込みTomcatJAX-RSCDIを、Fat JARとして動かす
http://d.hatena.ne.jp/Kazuhira/20150308/1425828178