CLOVER🍀

That was when it all began.

UndertowでJAX-RS(RESTEasy)とCDIを使う

以前書いた、こちらのエントリ

組み込みTomcatJAX-RS(RESTEasy)とCDIを使う
http://d.hatena.ne.jp/Kazuhira/20150308/1425780313

の、コンテナを組み込みTomcatからUndertowに変えて遊んでみたというお話。

実装する時の条件は、前と変わらず

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

といった感じです。

前に組み込みTomcatを使った時はけっこう簡単にいったのですが、Undertowに変えるとなかなかてこずりました…。

では、結果を書いていきましょう。

Mavenの定義

依存関係もろもろ。pomは、このような形で用意しました。
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.littlewings</groupId>
  <artifactId>embedded-undertow-jaxrs-cdi</artifactId>
  <packaging>jar</packaging>
  <version>0.0.1-SNAPSHOT</version>

  <dependencies>
    <dependency>
      <groupId>io.undertow</groupId>
      <artifactId>undertow-core</artifactId>
      <version>1.1.3.Final</version>
    </dependency>
    <dependency>
      <groupId>io.undertow</groupId>
      <artifactId>undertow-servlet</artifactId>
      <version>1.1.3.Final</version>
    </dependency>

    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-servlet-initializer</artifactId>
      <version>3.0.11.Final</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-cdi</artifactId>
      <version>3.0.11.Final</version>
    </dependency>

    <dependency>
      <groupId>org.jboss.weld.servlet</groupId>
      <artifactId>weld-servlet</artifactId>
      <version>2.2.9.Final</version>
    </dependency>

    <dependency>
      <groupId>javax.el</groupId>
      <artifactId>javax.el-api</artifactId>
      <version>2.2.5</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.web</groupId>
      <artifactId>javax.el</artifactId>
      <version>2.2.6</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>javax.servlet.jsp-api</artifactId>
      <version>2.3.1</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.web</groupId>
      <artifactId>javax.servlet.jsp</artifactId>
      <version>2.3.2</version>
    </dependency>

    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>2.11.6</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>1.2.3.RELEASE</version>
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>net.alchim31.maven</groupId>
        <artifactId>scala-maven-plugin</artifactId>
        <version>3.2.0</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <scalaVersion>2.11.6</scalaVersion>
          <args>
            <arg>-Xlint</arg>
            <arg>-unchecked</arg>
            <arg>-deprecation</arg>
            <arg>-feature</arg>
          </args>
          <recompileMode>incremental</recompileMode>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
</project>

基本的に、組み込みTomcatをUndertowに変えた感じなのですが、weld-servletを使う時にELとJSPを要求するようなので、仕方なく依存関係に追加しました…。

今回は、resteasy-undertowはスルー。

あと、Spring Bootのプラグインがいますが、Fat JAR化のためだけにこれがいます。

JAX-RSリソースとCDI管理Bean

前回と完全に同じソースコードです。小さいので、まとめて全部載せます。

// JAX-RS有効化
package org.littlewings.javaee7.rest

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

@ApplicationPath("rest")
class JaxrsApplication extends Application

//////////////////////////////////////////////
// JAX-RSリソース
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
package org.littlewings.javaee7.service

import javax.enterprise.context.RequestScoped

@RequestScoped
class CalcService {
  def add(a: Int, b: Int) = a + b
}

単純に、クエリパラメータを足し算するだけのクラス群です。

beans.xml

中身は不要ですが、空ファイルで必要です。

src/main/resources/META-INF/beans.xml

Undertowの起動とJAX-RSCDI統合

ここが、Tomcatとは大きく変わりました。

結果のクラスをまず載せると、このような形に。
src/main/scala/org/littlewings/javaee7/UndertowBootstrap.scala

package org.littlewings.javaee7

import scala.collection.JavaConverters._
import scala.io.StdIn

import io.undertow.{ Handlers, Undertow }
import io.undertow.server.HttpHandler
import io.undertow.server.handlers.PathHandler
import io.undertow.servlet.Servlets
import io.undertow.servlet.api.ServletContainerInitializerInfo
import io.undertow.servlet.util.DefaultClassIntrospector
import org.jboss.resteasy.plugins.servlet.ResteasyServletInitializer
import org.jboss.weld.environment.servlet.EnhancedListener

import org.littlewings.javaee7.rest._

object UndertowBootstrap {
  def main(args: Array[String]): Unit = {
    val contextPath = ""
    val port = 8080

    val deployment =
      Servlets
        .deployment
        .setClassLoader(getClass.getClassLoader)
        .setContextPath(contextPath)
        .setDeploymentName("jax-rs-with-cdi")
        // .addInitParameter("org.jboss.weld.environment.servlet.archive.isolation", "false")  // なくても動くみたい
        .addInitParameter("resteasy.injector.factory", "org.jboss.resteasy.cdi.CdiInjectorFactory")
        .addServletContainerInitalizer(new ServletContainerInitializerInfo(classOf[EnhancedListener],
          DefaultClassIntrospector.INSTANCE.createInstanceFactory(classOf[EnhancedListener]),
          null))
        .addServletContainerInitalizer(new ServletContainerInitializerInfo(classOf[ResteasyServletInitializer],
          DefaultClassIntrospector.INSTANCE.createInstanceFactory(classOf[ResteasyServletInitializer]),
          jaxrsClasses))

    val manager = Servlets.defaultContainer.addDeployment(deployment)
    manager.deploy()

    val serverHandler = manager.start()
    val handler: HttpHandler =
      if (contextPath.isEmpty) serverHandler
      else Handlers.path.addPrefixPath(contextPath, serverHandler)

    val server =
      Undertow
        .builder
        .addHttpListener(port, "localhost")
        .setHandler(handler)
        .build

    try {
      // Undertowの起動
      server.start()

      // Enter打ったら終了
      StdIn.readLine("> Enter stop")


    } finally {
      // Undertowの停止
      server.stop()
    }
  }

  private def jaxrsClasses: java.util.Set[Class[_]] =
    Set[Class[_]](
      classOf[JaxrsApplication],
      classOf[CalcResource]
    ).asJava
}

Tomcatの時との違いを、ちょっとずつ書いていきます。

Weldの以下の設定は、今回はなくても動いてくれました。ちょっと不思議でしたが、どういう扱いなのだろう…。

        // .addInitParameter("org.jboss.weld.environment.servlet.archive.isolation", "false")  // なくても動くみたい

RESTEasyとCDIのつなぎ込みは、Tomcatと一緒です。

        .addInitParameter("resteasy.injector.factory", "org.jboss.resteasy.cdi.CdiInjectorFactory")

で、WeldのServletContainerInitializerを、どうも自分で設定する必要があるっぽいです。これが追加されることで、BeanManagerが使えるようになります。

        .addServletContainerInitalizer(new ServletContainerInitializerInfo(classOf[EnhancedListener],
          DefaultClassIntrospector.INSTANCE.createInstanceFactory(classOf[EnhancedListener]),
          null))

BeanManagerが使用できないと、RESTEasyのCdiInjectorFactoryが初期化に失敗します。

Tomcatだと、ServiceLoaderの仕組みで頑張ってくれていたのだろうと思っていたのですが、今回ここでハマったのでTomcat実装を見てみたらServiceLoaderっぽいことを自前で実装していました…。

WeldのServletContainerInitializerが効かないということは、RESTEasyのServletContainerInitializerもダメなので、自分で登録。

        .addServletContainerInitalizer(new ServletContainerInitializerInfo(classOf[ResteasyServletInitializer],
          DefaultClassIntrospector.INSTANCE.createInstanceFactory(classOf[ResteasyServletInitializer]),
          jaxrsClasses))

UndertowのServletContainerInitializerInfoのコンストラクタの第3引数は、ServletContainerInitializer#onStartupに渡される引数そのものになります。

で、本来ここでJAX-RSのリソースクラスを登録してくれるはずなのですが、Undertowの場合スキャンしてくれる人がいないので、ここも自分で設定することに…。

  private def jaxrsClasses: java.util.Set[Class[_]] =
    Set[Class[_]](
      classOf[JaxrsApplication],
      classOf[CalcResource]
    ).asJava

なお、今回はResteasyServletInitializerを使用しましたが、後述のHammockもそうですがRESTEasyのServletを使う方法でもかまいません。TomcatでもServletは使わなかったので、ServletContainerInitializerで合わせただけの好みの話です。ただ、Servletを選んでもJAX-RSのリソースクラスはやっぱり手動で登録する必要があります。

動作確認的な

ここまで作ったら、パッケージングして

$ mvn package

起動。

java -jar target/embedded-undertow-jaxrs-cdi-0.0.1-SNAPSHOT.jar

確認。

$ curl "http://localhost:8080/rest/calc/add?a=5&b=8"
13

OKですね!

浮いているUndertowサーバは、Enterで終了します。

参考にしたもの、感想などなど

今回のソースを書くにあたって、@emaggameさんから教えていただいたHammockというものをかなり参考にしました。

Hammock, a lightweight integration of RestEasy, Undertow, and Weld.
https://github.com/johnament/hammock

前回Tomcatでやった時は、BeanManagerをJNDIリソースに登録してルックアップ可能な形で実装したのですが、今回Undertowだとそのあたりが厳しくて、やめようかなぁと思ったところに教えていただいたのがこちらでした。

最終的に、このHammockのソースとRESTEasyのCdiInjectorFactory、WeldのEnhancedListenerを見ていたら実現できそうな感じがしたので、JNDIはやめてこの形態になりました。

あと、

ということなのですが、確かにHammockの起動はWeldトリガーでした。

これ、たぶんJAX-RSリソースをスキャンするのに手っ取り早いからというのが理由のひとつな気がします…。

Hammockのソースを読んでいて、CDI#currentとかExtensionとかいろいろ参考になりました。けっこう面白かったです。

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