CLOVER🍀

That was when it all began.

RESTEasy+Netty(Netty 3/Netty 4/Netty 4 with CDI)で遊ぶ

RESTEasyのEmbedded Containerですが、以前JDK HttpServerとUndertowを試していましたのですけれど、Nettyがいるのが最近気になっていまして、ちょっと遊んでみることにしました。

Chapter 36. Embedded Containers / 36.4. Netty

ただ、ドキュメントに載っているのはNetty 3のみで、実際にはNetty関連のRESTEasyのモジュールとしては、以下の3種類があります。

最後のは、CDIサポートが付いたものですね。

順番に、試していってみます。なお、コードはScalaで書きます。

Netty 3

最初は、Netty 3から。依存関係は、このように定義。

libraryDependencies ++= Seq(
  "org.jboss.resteasy" % "resteasy-netty" % "3.0.13.Final",
  "org.scalatest" %% "scalatest" % "2.2.5" % "test"
)

ScalaTestがいるのは、テストコード用です。

JAX-RS動作用に作成したコードは、こちら。

JAX-RS有効化。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplication.scala

package org.littlewings.javaee7.rest

import scala.collection.JavaConverters._

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

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

リソースクラス。
src/main/scala/org/littlewings/javaee7/rest/CalcResource.scala

package org.littlewings.javaee7.rest

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

@Path("calc")
class CalcResource {
  @GET
  @Path("add")
  @Produces(value = Array(MediaType.TEXT_PLAIN))
  def add(@QueryParam("a") a: Int, @QueryParam("b") b: Int): Int =
    a + b
}

テストコード。
src/test/scala/org/littlewings/javaee7/rest/RestEasyNetty3Spec.scala

package org.littlewings.javaee7.rest

import org.jboss.resteasy.plugins.server.netty.NettyJaxrsServer
import org.jboss.resteasy.spi.ResteasyDeployment
import org.scalatest.FunSpec
import org.scalatest.Matchers._

import scala.io.Source

class RestEasyNetty3Spec extends FunSpec {
  describe("RestEasyNetty3Spec") {
    it("calc/add, using Application") {
      val netty = new NettyJaxrsServer
      val deployment = netty.getDeployment
      // 以下も可
      // val deployment = new ResteasyDeployment
      deployment.setApplicationClass(classOf[JaxrsApplication].getName)
      netty.setRootResourcePath("")
      netty.setPort(8080)
      netty.setDeployment(deployment)
      netty.start()

      val source = Source.fromURL("http://localhost:8080/calc/add?a=5&b=3")
      source.mkString.toInt should be(8)
      source.close()

      netty.stop()
    }

    it("calc/add, Resource only") {
      val netty = new NettyJaxrsServer
      val deployment = netty.getDeployment
      netty.setRootResourcePath("")
      netty.setPort(8080)
      netty.setDeployment(deployment)
      netty.start()
      deployment.getRegistry.addPerRequestResource(classOf[CalcResource])

      val source = Source.fromURL("http://localhost:8080/calc/add?a=5&b=3")
      source.mkString.toInt should be(8)
      source.close()

      netty.stop()
    }
  }
}

アプリケーションをデプロイするクラスとしては、NettyJaxrsServerというものを使います。

      val netty = new NettyJaxrsServer

ResteasyDeploymentというクラスに、Applicationのサブクラスを設定したり
※ServletInitializerがないので、Applicationのサブクラスはリソースクラスの情報を返す必要があります

      val deployment = netty.getDeployment
      deployment.setApplicationClass(classOf[JaxrsApplication].getName)

リソースクラス自体を登録したりします。

      deployment.getRegistry.addPerRequestResource(classOf[CalcResource])

NettyJaxrsServerに対して、ポートやパスの設定をして、ResteasyDeploymentをデプロイします。

      netty.setRootResourcePath("")
      netty.setPort(8080)
      netty.setDeployment(deployment)
      netty.start()

で、最後にstartを呼び出す、と。

以下の記述を行う場合は、NettyJaxrsServer#startの後でなければ意味がないようです。

      deployment.getRegistry.addPerRequestResource(classOf[CalcResource])

なお、ResteasyDeploymentは今回NettyJaxrsServer#getDeploymentから取得していますが、newしても構わないようです。

val deployment = new ResteasyDeployment

NettyJaxrsServerの中を見ると、startするまではnewしたResteasyDeploymentを定義しているだけみたいですし。

Netty 4

続いて、Netty 4を使う場合。ライブラリの依存関係は、このようになります。

libraryDependencies ++= Seq(
  "org.jboss.resteasy" % "resteasy-netty4" % "3.0.13.Final",
  "io.netty" % "netty-all" % "4.0.32.Final",
  "org.scalatest" %% "scalatest" % "2.2.5" % "test"
)

resteasy-netty4以外にも、Netty自身が必要です。resteasy-netty4では、Nettyへの依存関係はprovidedになっていますので。

主な変更点は、以上です。

Netty 3向けのコードが、そのまま動作します。

まあ、作ったテストコードのクラス名を変えた程度ですね。

class RestEasyNetty4Spec extends FunSpec {
  describe("RestEasyNetty4Spec") {

Netty 4+CDI

最後は、Netty 4にCDIを加えたもの。

libraryDependencies ++= Seq(
  "org.jboss.resteasy" % "resteasy-cdi" % "3.0.13.Final",
  "org.jboss.resteasy" % "resteasy-netty4-cdi" % "3.0.13.Final",
  "io.netty" % "netty-all" % "4.0.32.Final",
  "org.jboss.weld.se" % "weld-se" % "2.3.1.Final",
  "org.scalatest" %% "scalatest" % "2.2.5" % "test"
)

依存関係に、resteasy-cdiとWeld SEを追加しました。

Weld SE??って感じではありますが、Weld Servletを使うのは、今回はパスしました…。

このエントリを書いた当時は使い方をちょっと間違っていたのですが、@RequestScopedが使えるようです。
@SessionScopedはダメな気がします…
※2016/12/12追記

CDIを使うということで、JAX-RS用の各種クラスはこのように修正しました。

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

リソースクラス。@RequestScopedなクラスとし、別途CDI管理Beanをインジェクションしています。
src/main/scala/org/littlewings/javaee7/rest/CalcResource.scala

package org.littlewings.javaee7.rest

import javax.enterprise.context.RequestScoped
import javax.inject.Inject
import javax.ws.rs.core.MediaType
import javax.ws.rs.{GET, Path, Produces, QueryParam}

@Path("calc")
@RequestScoped
class CalcResource {
  @Inject
  private var calcService: CalcService = _

  @GET
  @Path("add")
  @Produces(value = 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/rest/CalcService.scala

package org.littlewings.javaee7.rest

import javax.enterprise.context.ApplicationScoped

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

あと、beans.xmlも必要です。
src/main/resources/META-INF/beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                           http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="annotated">
</beans>

で、テストコードはこのようになりました。
src/test/scala/org/littlewings/javaee7/rest/RestEasyNetty4CdiSpec.scala

package org.littlewings.javaee7.rest

import javax.enterprise.inject.spi.CDI

import org.jboss.resteasy.cdi.{CdiInjectorFactory, ResteasyCdiExtension}
import org.jboss.resteasy.plugins.server.netty.cdi.CdiNettyJaxrsServer
import org.jboss.resteasy.spi.ResteasyDeployment
import org.jboss.weld.environment.se.Weld
import org.scalatest.FunSpec
import org.scalatest.Matchers._

import scala.io.Source

class RestEasyNetty4CdiSpec extends FunSpec {
  describe("RestEasyNetty4CdiSpec") {
    it("calc/add, using Application") {
      val weld = new Weld
      weld.initialize()

      val cdiExtension = CDI.current().select(classOf[ResteasyCdiExtension]).get

      val netty = new CdiNettyJaxrsServer
      val deployment = netty.getDeployment
      // 以下も可
      // val deployment = new ResteasyDeployment
      deployment.setActualResourceClasses(cdiExtension.getResources)
      deployment.setInjectorFactoryClass(classOf[CdiInjectorFactory].getName)
      deployment.setActualProviderClasses(cdiExtension.getProviders)
      netty.setRootResourcePath("")
      netty.setPort(8080)
      netty.setDeployment(deployment)
      netty.start()

      val source = Source.fromURL("http://localhost:8080/calc/add?a=5&b=3")
      source.mkString.toInt should be(8)
      source.close()

      netty.stop()

      weld.shutdown()
    }

    it("calc/add, Resource only") {
      val weld = new Weld
      weld.initialize()

      val cdiExtension = CDI.current().select(classOf[ResteasyCdiExtension]).get

      val netty = new CdiNettyJaxrsServer
      val deployment = netty.getDeployment
      deployment.setActualResourceClasses(cdiExtension.getResources)
      deployment.setInjectorFactoryClass(classOf[CdiInjectorFactory].getName)
      netty.setRootResourcePath("")
      netty.setPort(8080)
      netty.setDeployment(deployment)
      netty.start()

      val source = Source.fromURL("http://localhost:8080/calc/add?a=5&b=3")
      source.mkString.toInt should be(8)
      source.close()

      netty.stop()

      weld.shutdown()
    }
  }
}

最初と最後に、Weld SEの初期化/終了が入りました。

      val weld = new Weld
      weld.initialize()

      weld.shutdown()

resteasy-netty4-cdiのテストコードをマネてですが、Applicationのサブクラスやリソースクラスの収集は、ResteasyCdiExtensionクラスを利用しています。
また、NettyJaxrsServerに代わりCdiNettyJaxrsServerを使用してサーバーを起動することで、@RequestScopedが使えるようになります。

      val cdiExtension = CDI.current().select(classOf[ResteasyCdiExtension]).get

      val netty = new CdiNettyJaxrsServer
      val deployment = netty.getDeployment
      // 以下も可
      // val deployment = new ResteasyDeployment
      deployment.setActualResourceClasses(cdiExtension.getResources)
      deployment.setInjectorFactoryClass(classOf[CdiInjectorFactory].getName)
      deployment.setActualProviderClasses(cdiExtension.getProviders)

CdiInjectorFactoryクラスを設定しておくことも必要です。

これで、簡易的なJAX-RSCDIの実行環境のできあがりです。

まとめ

RESTEasyを、Netty 3、4で動かすサンプルを書いてみました。ドキュメントの紹介はNetty 3でしたが、依存関係をちょっと調整するだけでNetty 4にも乗せられ、さらにCDIも使えますよ、と。

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