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が選択されます。
もうちょっと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