CLOVER🍀

That was when it all began.

RESTEasyをJDK付属のHTTPサーバ、Undertowで動かす

RESTEasyも、JDK付属のHTTPサーバに組み込んで動かしたり、またUndertowに組み込んで動かせるらしかったので、ちょっと試してみようかと。

Embedded Containers
http://docs.jboss.org/resteasy/docs/3.0.7.Final/userguide/html_single/index.html#RESTEasy_Embedded_Container
*あ、ドキュメントのバージョンが…

使えるサーバアダプタは、こちらのようです。Netty用とかありますね…。
https://github.com/resteasy/Resteasy/tree/master/jaxrs/server-adapters

簡単に、Groovyで。

まずは、JDK付属のHTTPサーバを使ってみましょう。
resteasy_jdk_httpserver.groovy

@Grab('org.jboss.resteasy:resteasy-jdk-http:3.0.8.Final')
import javax.ws.rs.DefaultValue
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.QueryParam
import javax.ws.rs.core.MediaType

import org.jboss.resteasy.plugins.server.sun.http.HttpContextBuilder

import com.sun.net.httpserver.HttpServer

@Path("hello")
class HelloResource {
    @GET
    @Path("index")
    @Produces(MediaType.TEXT_PLAIN)
    def index() {
       "Hello World" 
    }
}

@Path("groovy")
class GroovyResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    def index(@QueryParam("p") @DefaultValue("World") String p) {
        "Hello $p by Groovy"
    }
}

def server = HttpServer.create(new InetSocketAddress(8080), 10)

def contextBuilder = new HttpContextBuilder()

[HelloResource, GroovyResource].each {
    contextBuilder.deployment.actualResourceClasses.add(it)
}
def context = contextBuilder.bind(server)

server.start()

println("[${new Date()}] RestEasyJdkHttpd startup[${server.address}].")

// 終了処理
// contextBuilder.cleanup()
// server.stop(0)

割と簡単で、普通にJAX-RSのリソースクラスを用意します。あとは、RESTEasyが提供しているHttpContextBuilderにリソースクラスを登録してbindします。

def contextBuilder = new HttpContextBuilder()

[HelloResource, GroovyResource].each {
    contextBuilder.deployment.actualResourceClasses.add(it)
}
def context = contextBuilder.bind(server)

そしてHttpServerを開始します。

server.start()

依存するアーティファクト

@Grab('org.jboss.resteasy:resteasy-jdk-http:3.0.8.Final')

となります。

動かしてみましょう。

$ groovy resteasy_jdk_httpserver.groovy 
[Sun Aug 31 13:42:02 JST 2014] RestEasyJdkHttpd startup[/0:0:0:0:0:0:0:0:8080].

確認。

$ curl http://localhost:8080/hello/index
Hello World

$ curl http://localhost:8080/groovy
Hello World by Groovy

$ curl http://localhost:8080/groovy?p=RESTEasy
Hello RESTEasy by Groovy

大丈夫そうですね。

終了は、Ctrl-Cで。

続いてUndertowといきたいところですが、UndertowはGroovy+Grapeでは動かせませんでした。

アクセスすると、こんな感じで例外を見ます。

ERROR: UT005023: Exception handling request to /rest/groovy
java.lang.NoSuchMethodError: javax.servlet.ServletRequest.getDispatcherType()Ljavax/servlet/DispatcherType;
	at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:51)
	at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
	at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:113)
	at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:56)
〜

ServletRequestに対して、NoSuchMethodError…Groovyの持ってるServlet APIと衝突してるっぽいですね。アウトです。

というわけで

諦めるかどうかですが、せっかくなので使うところまではやろうと、Scalaで書き直し。

まずはsbtの設定から。
project/Build.scala

import sbt._
import sbt.Keys._

object BuildSettings {
  val buildOrganization = "org.littlewings"
  val buildVersion = "0.0.1-SNAPSHOT"
  val buildScalaVersion = "2.11.2"
  val buildScalacOptions = Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

  val buildSettings = Seq(
    organization := buildOrganization,
    version := buildVersion,
    scalaVersion := buildScalaVersion,
    scalacOptions ++= buildScalacOptions,
    incOptions := incOptions.value.withNameHashing(true)
  )
}

object RestEasyEmbedded extends Build {
  import BuildSettings._

  lazy val root =
    Project("resteasy-embedded",
            file("."),
            settings = buildSettings)
              .aggregate(jdkHttpServer, undertow)

  lazy val jdkHttpServer =
    Project("resteasy-embedded-jdkhttpserver",
            file("resteasy-embedded-jdkhttpserver"),
            settings = buildSettings
                      ++ Seq(libraryDependencies += "org.jboss.resteasy" % "resteasy-jdk-http" % "3.0.8.Final"))

  lazy val undertow =
    Project("resteasy-embedded-undertow",
            file("resteasy-embedded-undertow"),
            settings = buildSettings
                      ++ Seq(fork in run := true,  // Undertowの場合は、これが必要
                             connectInput := true,
                             libraryDependencies ++= Seq("org.jboss.resteasy" % "resteasy-undertow" % "3.0.8.Final",
                                                         "io.undertow" % "undertow-core" % "1.0.15.Final",
                                                         "io.undertow" % "undertow-servlet" % "1.0.15.Final")))
}

JDK付属のHTTPサーバの分も移植したので、マルチプロジェクト構成としました。

依存関係としては、RESTEasyのUndertowアダプタの他に、Undertow自身も必要です。

                             libraryDependencies ++= Seq("org.jboss.resteasy" % "resteasy-undertow" % "3.0.8.Final",
                                                         "io.undertow" % "undertow-core" % "1.0.15.Final",
                                                         "io.undertow" % "undertow-servlet" % "1.0.15.Final")))

Undertowですが、sbtの持っているSecurityManagerとかち合うらしく、普通に使うと

[error] (run-main-4) java.lang.RuntimeException: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "io.undertow.servlet.CREATE_INITIAL_HANDLER")
java.lang.RuntimeException: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "io.undertow.servlet.CREATE_INITIAL_HANDLER")
	at io.undertow.servlet.core.DeploymentManagerImpl.deploy(DeploymentManagerImpl.java:219)
	at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:229)
	at org.littlewings.javaee7.resteasy.RestEasyEmbeddedUndertow$.main(RestEasyEmbeddedUndertow.scala:49)
	at org.littlewings.javaee7.resteasy.RestEasyEmbeddedUndertow.main(RestEasyEmbeddedUndertow.scala)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:483)
Caused by: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "io.undertow.servlet.CREATE_INITIAL_HANDLER")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:457)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at io.undertow.servlet.handlers.ServletInitialHandler.<init>(ServletInitialHandler.java:97)
〜省略〜

スタックトレースの最後に載せているServletInitialHandlerのコンストラクタで、Permissionを見ているようです。

これを回避するために、forkするようにしました。

                      ++ Seq(fork in run := true,  // Undertowの場合は、これが必要
                             connectInput := true,

connectInputは、オマケで終了用です。

では、書いたコードを。
resteasy-embedded-undertow/src/main/scala/org/littlewings/javaee7/resteasy/RestEasyEmbeddedUndertow.scala

package org.littlewings.javaee7.resteasy

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

import java.util.Date

import javax.ws.rs.{ApplicationPath, DefaultValue, GET, Path, Produces, QueryParam}
import javax.ws.rs.core.{Application, MediaType}

import io.undertow.servlet.api.DeploymentInfo
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer

@ApplicationPath("rest")
class JaxrsApplication extends Application {
  override def getClasses: java.util.Set[Class[_]] =
    Set(classOf[HelloResource], classOf[ScalaResource])
      .asJava
      .asInstanceOf[java.util.Set[Class[_]]]
}

@Path("hello")
class HelloResource {
  @GET
  @Path("index")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def index: String =
    "Hello World"
}

@Path("scala")
class ScalaResource {
  @GET
  @Produces(Array(MediaType.TEXT_PLAIN))
  def index(@QueryParam("p") @DefaultValue("World") p: String): String =
    s"Hello $p by Scala"
}

object RestEasyEmbeddedUndertow {
  def main(args: Array[String]): Unit = {
    val server = new UndertowJaxrsServer

    // この場合のアクセスURLは、http://localhost:8081/di/rest/〜(リソースで指定したパス)
    val deployment = server.undertowDeployment(classOf[JaxrsApplication])
    deployment.setContextPath("/di")
    deployment.setDeploymentName("DI")
    server.deploy(deployment)

    // こちらでも可
    // この場合のアクセスURLは、http://localhost:8081/rest/〜(リソースで指定したパス)
    //server.deploy(classOf[JaxrsApplication])

    // こちらでも可
    // この場合のアクセスURLは、http://localhost:8081/root/〜(リソースで指定したパス)
    //server.deploy(classOf[JaxrsApplication], "/root")

    server.start()

    println(s"[${new Date}] RestEasyUndertowHttpd startup.")

    StdIn.readLine()

    // 終了処理
    server.stop()

    println(s"[${new Date}] RestEasyUndertowHttpd shutdown.")
  }
}

Undertowの場合は、Applicationクラスのサブクラスを用意する必要があるようです。使用するリソースクラスも定義して、用意したApplicationクラスのサブクラスから返却するようにしておきます。

続いて、UndertowJaxrsServerを作成。

    val server = new UndertowJaxrsServer

そして、用意したApplicationクラスのサブクラスをデプロイします。

デプロイの方法にはいくつかあって、UndertowのDeploymentInfoを使う方法。

    // この場合のアクセスURLは、http://localhost:8081/di/rest/〜(リソースで指定したパス)
    val deployment = server.undertowDeployment(classOf[JaxrsApplication])
    deployment.setContextPath("/di")
    deployment.setDeploymentName("DI")
    server.deploy(deployment)

コンテキストパスを「/」として、直接Applicationクラスのサブクラスをデプロイする方法。

    // こちらでも可
    // この場合のアクセスURLは、http://localhost:8081/rest/〜(リソースで指定したパス)
    server.deploy(classOf[JaxrsApplication])

コンテキストパスを指定して、Applicationクラスのサブクラスをデプロイする方法。この場合、@ApplicationPathで指定したパスは無視されるようです。

    // こちらでも可
    // この場合のアクセスURLは、http://localhost:8081/root/〜(リソースで指定したパス)
    server.deploy(classOf[JaxrsApplication], "/root")

他にもメソッドがありましたが、詳しくはUndertowJaxrsServerのAPIまで。

今回は、UndertowのDeploymentInfoを使ってみます。

ところで、Undertowのアダプタを使った場合はListenポートが8081になるようなのですが、これってどこで変えられるんでしょう…。

追記)
変更できました。

まず、デフォルトのListenポートは、UndertowJaxrsServerおよびTestPortProviderで決定され、環境変数「RESTEASY_PORT」、システムプロパティ「org.jboss.resteasy.port」の順に探し、なければ8081が選択されます。

https://github.com/resteasy/Resteasy/blob/master/jaxrs/resteasy-jaxrs/src/main/java/org/jboss/resteasy/test/TestPortProvider.java#L134

もうちょっとAPI的に設定したい場合は、Undertow.Builderを引数に取るUndertowJaxrsServer#startメソッドを使用します。

    server.start {
      io.undertow.Undertow
        .builder
        .addHttpListener(8080, "localhost")  // Listenポート、アドレスを指定して起動
    }

これで、Listenポートが指定できますね。Undertow.BuilderのsetHandlerメソッドおよびbuildメソッドは、UndertowJaxrsServer#startメソッド側で実行されます。

まあ、今回はデフォルトの8081でいくことにします。

起動。

> run
[info] Compiling 1 Scala source to /xxxxx/resteasy-embedded-undertow/target/scala-2.11/classes...
[info] Running org.littlewings.javaee7.resteasy.RestEasyEmbeddedUndertow 
[error] 8 31, 2014 1:56:09 午後 org.jboss.resteasy.spi.ResteasyDeployment 
[error] 情報: Deploying javax.ws.rs.core.Application: class org.littlewings.javaee7.resteasy.JaxrsApplication
[error] 8 31, 2014 1:56:09 午後 org.jboss.resteasy.spi.ResteasyDeployment 
[error] 情報: Adding class resource org.littlewings.javaee7.resteasy.HelloResource from Application class org.littlewings.javaee7.resteasy.JaxrsApplication
[error] 8 31, 2014 1:56:09 午後 org.jboss.resteasy.spi.ResteasyDeployment 
[error] 情報: Adding class resource org.littlewings.javaee7.resteasy.ScalaResource from Application class org.littlewings.javaee7.resteasy.JaxrsApplication
[error] 8 31, 2014 1:56:09 午後 org.xnio.Xnio <clinit>
[error] INFO: XNIO version 3.2.0.Final
[error] 8 31, 2014 1:56:09 午後 org.xnio.nio.NioXnio <clinit>
[error] INFO: XNIO NIO Implementation Version 3.2.0.Final
[info] [Sun Aug 31 13:56:09 JST 2014] RestEasyUndertowHttpd startup.

確認。

$ curl http://localhost:8081/di/rest/hello/index
Hello World

$ curl http://localhost:8081/di/rest/scala
Hello World by Scala

$ curl http://localhost:8081/di/rest/scala?p=RESTEasy
Hello RESTEasy by Scala

こちらも大丈夫そうですね。

終了は、Enterひとつ打ってください。

[info] [Sun Aug 31 13:58:58 JST 2014] RestEasyUndertowHttpd shutdown.
[success] Total time: 173 s, completed 2014/08/31 13:58:59

ちなみに、JDK付属のHTTPサーバを使用した方は、Scalaで書くとこんな感じになりました。
resteasy-embedded-jdkhttpserver/src/main/scala/org/littlewings/javaee7/resteasy/RestEasyEmbeddedJdkHttpServer.scala

package org.littlewings.javaee7.resteasy

import scala.io.StdIn

import java.net.InetSocketAddress
import java.util.Date

import javax.ws.rs.{DefaultValue, GET, Path, Produces, QueryParam}
import javax.ws.rs.core.MediaType

import org.jboss.resteasy.plugins.server.sun.http.HttpContextBuilder

import com.sun.net.httpserver.HttpServer

@Path("hello")
class HelloResource {
  @GET
  @Path("index")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def index: String =
    "Hello World"
}

@Path("scala")
class ScalaResource {
  @GET
  @Produces(Array(MediaType.TEXT_PLAIN))
  def index(@QueryParam("p") @DefaultValue("World") p: String): String =
    s"Hello $p by Scala"
}

object RestEasyEmbeddedJdkHttpServer {
  def main(args: Array[String]): Unit = {
    val server = HttpServer.create(new InetSocketAddress(8080), 10)

    val contextBuilder = new HttpContextBuilder

    Seq(classOf[HelloResource], classOf[ScalaResource])
      .foreach(contextBuilder.getDeployment.getActualResourceClasses.add)

    val context = contextBuilder.bind(server)

    server.start()

    println(s"[${new Date}] RestEasyJdkHttpd startup[${server.getAddress}].")

    StdIn.readLine()

    // 終了処理
    contextBuilder.cleanup()
    server.stop(0)

    println(s"[${new Date}] RestEasyJdkHttpd shutdown.")
  }
}

こんなところで。

とはいえ、実際に簡単な用途で使うならGroovy+RESTEasy+JDK HTTPサーバではなかろうかという気が…。

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