CLOVER🍀

That was when it all began.

UndertowでWebSocketを使って遊ぶ

WebSocketを軽く触ってみようと思いまして。

WebSocketのAPIは、Java EE 7にJSR-356があるので、こちらを使って試すことを前提に
考えたいと思います。

The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 356

また、実装にはUndertowを使います。これでかなりてこずりましたが…。

Undertow · JBoss Community

お題は、echo、クライアントとサーバーそれぞれをUndertowとJSR-356のAPIを使って実装します。

準備

プロジェクトは、クライアントとサーバーでそれぞれ別にしました。
build.sbt

name := "undertow-websocket"

lazy val commonSettings = Seq(
  version := "0.0.1-SNAPSHOT",
  organization := "org.littlewings",
  scalaVersion := "2.12.1",
  scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature"),
  updateOptions := updateOptions.value.withCachedResolution(true)
)

lazy val server = (project in file("server")).
  settings(
    name := "undertow-websocket-server",
    commonSettings,
    libraryDependencies ++= Seq(
      "io.undertow" % "undertow-websockets-jsr" % "1.4.10.Final"
    )
  )

lazy val client = (project in file("client")).
  settings(
    name := "undertow-websocket-client",
    commonSettings,
    libraryDependencies ++= Seq(
      "io.undertow" % "undertow-websockets-jsr" % "1.4.10.Final"
    )
  )

ドキュメントは、Undertowのものを参考に…したかったのですが、ほとんどなにも書いていません。

Undertow

仕方がないので、このあたりを参考に。

java - How to do websockets in embedded undertow? - Stack Overflow

undertow/JSRWebSocketServer.java at 1.4.10.Final · undertow-io/undertow · GitHub

https://github.com/undertow-io/undertow/blob/1.4.10.Final/websockets-jsr/src/test/java/io/undertow/websockets/jsr/test/TestMessagesReceivedInOrder.java

そういえば、ドキュメントは1.3系ですけど、今の1.x系は1.4が最新なんですね。

サーバー側の実装

JSR-356のAPIを使ったサーバー側のエンドポイントの実装サンプルは調べるとけっこう出てくるので、簡単に。
server/src/main/scala/org/littlewings/javaee7/websocket/EchoServer.scala

package org.littlewings.javaee7.websocket

import javax.websocket._
import javax.websocket.server.ServerEndpoint

import org.jboss.logging.Logger

@ServerEndpoint("/echo")
class EchoServer {
  val logger: Logger = Logger.getLogger(getClass)

  @OnMessage
  def onMessage(session: Session, msg: String): Unit = {
    logger.infof("receive message = %s", msg)
    session.getBasicRemote.sendText(s"[$msg]")
  }

  @OnOpen
  def onOpen(session: Session, config: EndpointConfig): Unit =
    logger.infof("session open")

  @OnClose
  def onClose(session: Session, reason: CloseReason): Unit =
    logger.infof("close, reason = %s", reason.getReasonPhrase)

  @OnError
  def onError(session: Session, cause: Throwable): Unit =
    logger.errorf(cause, "error")
}

@ServerEndpointで、WebSocketのエンドポイントであることと、パスを指定します。

@ServerEndpoint("/echo")
class EchoServer {

@OnOpen、@OnClose、@OnErrorで、接続時、クローズ時、エラー発生時のイベントを受け取ります。

メッセージは@OnMessageで受け付け、今回はテキストメッセージを「[]」をくっつけて送り返すようにしています。

  @OnMessage
  def onMessage(session: Session, msg: String): Unit = {
    logger.infof("receive message = %s", msg)
    session.getBasicRemote.sendText(s"[$msg]")
  }

続いて、起動クラス。ここが1番てこずったというか、情報がなかったというか。
server/src/main/scala/org/littlewings/javaee7/websocket/WebSocketServer.scala

package org.littlewings.javaee7.websocket

import java.time.LocalDateTime

import io.undertow.servlet.Servlets
import io.undertow.servlet.api.DeploymentInfo
import io.undertow.websockets.jsr.WebSocketDeploymentInfo
import io.undertow.{Handlers, Undertow}

import scala.io.StdIn

object WebSocketServer {
  def main(args: Array[String]): Unit = {
    val webSocketDeploymentInfo =
      new WebSocketDeploymentInfo()
        .addEndpoint(classOf[EchoServer])

    val builder =
      new DeploymentInfo()
        .setClassLoader(getClass.getClassLoader)
        .setContextPath("/")
        .setDeploymentName("myapp.war")
        .addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, webSocketDeploymentInfo)

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

    val path =
      Handlers
        .path
        .addPrefixPath("/", manager.start())

    val undertow =
      Undertow
        .builder
        .addHttpListener(8080, "localhost")
        .setHandler(path)
        .build()

    undertow.start()

    StdIn.readLine(s"[${LocalDateTime.now}] Please Enter, Stop.")

    undertow.stop()
  }
}

JSR-356のAPIを使って実装したWebSocketエンドポイントを、Undertowにどうデプロイすればいいのかがわからずに
だいぶ困っていましたが、最終的にこんな感じになりました。

    val webSocketDeploymentInfo =
      new WebSocketDeploymentInfo()
        .addEndpoint(classOf[EchoServer])

    val builder =
      new DeploymentInfo()
        .setClassLoader(getClass.getClassLoader)
        .setContextPath("/")
        .setDeploymentName("myapp.war")
        .addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, webSocketDeploymentInfo)

WebSocketDeploymentInfoを作成してエンドポイントを追加し、DeploymentInfoのServletContextの属性として設定すれば
OKみたいです。

あとはServletを使っていた時みたいにHandlerとして設定して、起動するだけですね。

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

    val path =
      Handlers
        .path
        .addPrefixPath("/", manager.start())

    val undertow =
      Undertow
        .builder
        .addHttpListener(8080, "localhost")
        .setHandler(path)
        .build()

    undertow.start()

ここまでで、サーバー側はおしまい。

クライアント側

あんまり情報がなさそうな、JSR-356のクライアント側。

こちらは、こんな感じの実装に。
client/src/main/scala/org/littlewings/javaee7/websocket/EchoClient.scala

package org.littlewings.javaee7.websocket

import javax.websocket._

import org.jboss.logging.Logger

@ClientEndpoint
class EchoClient {
  val logger: Logger = Logger.getLogger(classOf[EchoClient])

  @OnMessage
  def onMessage(session: Session, msg: String): Unit = {
    logger.infof("received from server: %s", msg)
  }

  @OnOpen
  def onOpen(session: Session, config: EndpointConfig): Unit =
    logger.infof("session open")

  @OnClose
  def onClose(session: Session, reason: CloseReason): Unit =
    logger.infof("close, reason = %s", reason.getReasonPhrase)

  @OnError
  def onError(session: Session, cause: Throwable): Unit =
    logger.errorf(cause, "error")
}

@ClientEndpointをクラスに付与している以外は、ほとんどサーバー側のエンドポイントと変わりません。

起動用のクラスは、こちら。
client/src/main/scala/org/littlewings/javaee7/websocket/WebSocketClient.scala

package org.littlewings.javaee7.websocket

import java.net.URI
import javax.websocket.{CloseReason, ContainerProvider}

import scala.io.StdIn

object WebSocketClient {
  def main(args: Array[String]): Unit = {
    val container = ContainerProvider.getWebSocketContainer
    val session = container.connectToServer(classOf[EchoClient], URI.create("ws://localhost:8080/echo"))

    Iterator
      .continually(StdIn.readLine("enter text> "))
      .takeWhile(_.trim != "exit")
      .filter(!_.trim.isEmpty)
      .foreach { message => session.getBasicRemote.sendText(message) }

    session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "byebye!!"))
  }
}

ContainerProviderからWebSocketContainerを取得し、あとは作成したクライアントのクラスとサーバー側のエンドポイントを指定して、
WebSocketのSessionを取得します。

あとはプロンプトっぽいものを簡単に作って、入力された文字列をサーバーに送信、「exit」と入力されると
終了するようにしています。

動作確認

それでは、試してみましょう。

まずはサーバー側を起動。

> run
[info] Running org.littlewings.javaee7.websocket.WebSocketServer 
2 11, 2017 4:22:50 午後 org.xnio.Xnio <clinit>
INFO: XNIO version 3.3.6.Final
2 11, 2017 4:22:50 午後 org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.3.6.Final
2 11, 2017 4:22:50 午後 io.undertow.websockets.jsr.Bootstrap handleDeployment
WARN: UT026009: XNIO worker was not set on WebSocketDeploymentInfo, the default worker will be used
2 11, 2017 4:22:50 午後 io.undertow.websockets.jsr.Bootstrap handleDeployment
WARN: UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
2 11, 2017 4:22:50 午後 io.undertow.websockets.jsr.ServerWebSocketContainer addEndpointInternal
INFO: UT026003: Adding annotated server endpoint class org.littlewings.javaee7.websocket.EchoServer for path /echo
[2017-02-11T16:22:51.123] Please Enter, Stop.

クライアント側も起動。

> run
[info] Running org.littlewings.javaee7.websocket.WebSocketClient 
2 11, 2017 4:23:09 午後 org.xnio.Xnio <clinit>
INFO: XNIO version 3.3.6.Final
2 11, 2017 4:23:09 午後 org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.3.6.Final
2 11, 2017 4:23:09 午後 io.undertow.websockets.jsr.ServerWebSocketContainer addEndpointInternal
INFO: UT026004: Adding annotated client endpoint class org.littlewings.javaee7.websocket.EchoClient
2 11, 2017 4:23:10 午後 org.littlewings.javaee7.websocket.EchoClient onOpen
INFO: session open
enter text> 

サーバー側にも、セッションをオープンしたログが出ます。

[2017-02-11T16:22:51.123] Please Enter, Stop.2 11, 2017 4:23:10 午後 org.littlewings.javaee7.websocket.EchoServer onOpen
INFO: session open

クライアント側で、いくつか入力。

enter text> Hello WebSocket!!
enter text> 2 11, 2017 4:23:53 午後 org.littlewings.javaee7.websocket.EchoClient onMessage
INFO: received from server: [Hello WebSocket!!]

enter text> こんにちは、世界
enter text> 2 11, 2017 4:24:00 午後 org.littlewings.javaee7.websocket.EchoClient onMessage
INFO: received from server: [こんにちは、世界]

サーバー側の様子。

2 11, 2017 4:23:53 午後 org.littlewings.javaee7.websocket.EchoServer onMessage
INFO: receive message = Hello WebSocket!!
2 11, 2017 4:23:59 午後 org.littlewings.javaee7.websocket.EchoServer onMessage
INFO: receive message = こんにちは、世界

終了。

enter text> exit
2 11, 2017 4:24:26 午後 org.littlewings.javaee7.websocket.EchoClient onClose
INFO: close, reason = byebye!!

サーバー側も、セッション完了。

2 11, 2017 4:24:26 午後 org.littlewings.javaee7.websocket.EchoServer onClose
INFO: close, reason = byebye!!

OKそうです。

まとめ

Undertowを使って、Java EEのWebSocket API(JSR-356)を試してみました。

Echoくらいなら、JSR-356自体は簡単ですが、Undertowでの設定にだいぶてこずりました…。
まあ、動いたので良しとしましょう。

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

JAX-RS(RESTEasy/Undertow)で、Swaggerを使ってREST APIのドキュメントを生成する

Swaggerというものがあるのはなんとなく知っていたのですが、使ったことがなかったので
試してみます。

The Best APIs are Built with Swagger Tools | Swagger

Swaggerで始めるモデルファーストなAPI開発

あんまりちゃんと調べたことなかったのですが、JSONYAMLを使ってRESTful APIドキュメンテーション
コード、実際のAPIの間をもつ仕組みみたいですね。

今回は、SwaggerのJAX-RS用のモジュールを使ってSwagger Spec(JSON)を生成し、Swagger UIで見るところまで
やってみます。

参考)
JAX-RS+Glassfish+SwaggerでシンプルにはじめるAPIドキュメンテーション

JAX-RS の REST API ドキュメントを Swagger を使って生成する - なにか作る

また、構成は

とします。

Swagger Coreおよび、SwaggerのJAX-RS用のモジュールはこちら。

GitHub - swagger-api/swagger-core: Examples and server integrations for generating the Swagger API Specification, which enables easy access to your REST API

https://github.com/swagger-api/swagger-core/tree/master/modules/swagger-jaxrs

Swagger JAX-RSについては、こちらの情報とサンプルを参考にしています。

Swagger Core JAX RS Project Setup 1.5.X · swagger-api/swagger-core Wiki · GitHub

swagger-samples/java/java-resteasy at master · swagger-api/swagger-samples · GitHub

準備

ビルド定義。
build.sbt

name := "resteasy-swagger"

version := "0.0.1-SNAPSHOT"

organization := "org.littlewings"

scalaVersion := "2.12.1"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

updateOptions := updateOptions.value.withCachedResolution(true)

libraryDependencies ++= Seq(
  "org.jboss.resteasy" % "resteasy-undertow" % "3.0.19.Final",
  "org.jboss.resteasy" % "resteasy-jackson2-provider" % "3.0.19.Final",
  "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.8.6",
  "io.undertow" % "undertow-core" % "1.4.10.Final",
  "io.undertow" % "undertow-servlet" % "1.4.10.Final",
  "io.swagger" % "swagger-jaxrs" % "1.5.12" exclude("javax.ws.rs", "jsr311-api")
)

Swagger関係のものは「swagger-jaxrs」ですが、JAX-RS 1系の依存が入っているようなので除去しています。
Undertowで動かす時は、これが残っていると困ったことになりました。

あとは、RESTEasyでJackson 2、Jackson 2のScala用のモジュール、Undertowに燗する依存関係です。

JAX-RSリソースクラス(REST API)の作成

まずは、対象となるJAX-RSリソースクラス(REST API)を作っていきます。

特にJSONにはこだわらない足し算、掛け算を行うリソースクラス。
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}

import io.swagger.annotations.{Api, ApiOperation}

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

  @GET
  @Path("multiply")
  @Produces(Array(MediaType.TEXT_PLAIN))
  @ApiOperation(value = "calc multiply")
  def multiply(@QueryParam("a") a: Int, @QueryParam("b") b: Int): Int =
    a * b
}

Swaggerに燗する@Api、@ApiOperationを付与しています。@ProducesでMediaTypeをきっちり指定しておくと
Swaggerが生成するSpecにも反映してくれます。

@Apiで指定しているのは、APIに対するタグになります。

@Api("calc")

続いて、JSONを扱うリソースクラス。お題は書籍とし、データはインメモリで持つこととします。
src/main/scala/org/littlewings/javaee7/rest/BookResource.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.core.{Context, MediaType, Response, UriInfo}
import javax.ws.rs._

import io.swagger.annotations.{Api, ApiOperation}

import scala.collection.JavaConverters._

object BookResource {
  private[rest] val books: scala.collection.mutable.Map[String, Book] =
    new java.util.concurrent.ConcurrentHashMap[String, Book]().asScala
}

@Path("book")
@Api("book")
class BookResource {
  @GET
  @Produces(Array(MediaType.APPLICATION_JSON))
  @ApiOperation(value = "find all books", response = classOf[Seq[Book]])
  def fildAll: Seq[Book] =
    BookResource.books.values.toVector

  @GET
  @Path("{isbn}")
  @Produces(Array(MediaType.APPLICATION_JSON))
  @ApiOperation(value = "find book", response = classOf[Book])
  def find(@PathParam("isbn") isbn: String): Book =
    BookResource.books.get(isbn).orNull

  @PUT
  @Path("{isbn}")
  @Consumes(Array(MediaType.APPLICATION_JSON))
  @Produces(Array(MediaType.APPLICATION_JSON))
  @ApiOperation("register book")
  def register(book: Book, @Context uriInfo: UriInfo): Response = {
    BookResource.books.put(book.isbn, book)
    Response.created(uriInfo.getRequestUriBuilder.build(book.isbn)).build
  }
}

先ほどと少し変えているのは@ApiOperationのresponseにメソッドの戻り値の型を指定しているところで、ここを指定して
おくとSwaggerが生成するJSONの方にもこの情報が伝わるようになります。

  @GET
  @Path("{isbn}")
  @Produces(Array(MediaType.APPLICATION_JSON))
  @ApiOperation(value = "find book", response = classOf[Book])
  def find(@PathParam("isbn") isbn: String): Book =
    BookResource.books.get(isbn).orNull

また、BookクラスはCase Classとして作成しましたが、JavaBeans(getter/setter)でないとSwaggerが型を理解してくれない
みたいなので、@BeanPropertyを付与しています。

src/main/scala/org/littlewings/javaee7/rest/Book.scala 
package org.littlewings.javaee7.rest

import scala.beans.BeanProperty

case class Book(@BeanProperty isbn: String, @BeanProperty title: String, @BeanProperty price: Int)

ここまでで、JAX-RSリソースクラスの作成は完了です。

Swaggerの設定をする

SwaggerのJAX-RS用のモジュールでは、JAX-RSのApplicationクラスのサブクラス内にSwagger関係の設定を
実装します。

今回作成したのは、こちら。
src/main/scala/org/littlewings/javaee7/rest/JaxrsActivator.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.{ApplicationPath, Path}
import javax.ws.rs.core.Application

import io.swagger.jaxrs.config.BeanConfig
import io.swagger.jaxrs.listing.{ApiListingResource, SwaggerSerializers}
import org.reflections.Reflections

import scala.collection.JavaConverters._

@ApplicationPath("api")
class JaxrsActivator extends Application {
  val beanConfig = new BeanConfig
  beanConfig.setVersion("1.0.0");
  beanConfig.setSchemes(Array("http"))
  beanConfig.setHost("localhost:8080")
  beanConfig.setBasePath(getClass.getAnnotation(classOf[ApplicationPath]).value)
  beanConfig.setResourcePackage(classOf[JaxrsActivator].getPackage.getName)
  beanConfig.setScan(true)

  override def getClasses: java.util.Set[Class[_]] = {
    val resourceClasses: Set[Class[_]] =
      Set.empty ++
        new Reflections(classOf[JaxrsActivator].getPackage.getName)
          .getTypesAnnotatedWith(classOf[Path])
          .asScala
    val swaggerClasses = Set[Class[_]](
      classOf[ApiListingResource],
      classOf[SwaggerSerializers]
    )

    (resourceClasses ++ swaggerClasses).asJava
  }
}

Swaggerが生成する対象のAPI群に燗する基本的な設定は、こちらで行います。

  val beanConfig = new BeanConfig
  beanConfig.setVersion("1.0.0");
  beanConfig.setSchemes(Array("http"))
  beanConfig.setHost("localhost:8080")
  beanConfig.setBasePath(getClass.getAnnotation(classOf[ApplicationPath]).value)
  beanConfig.setResourcePackage(classOf[JaxrsActivator].getPackage.getName)
  beanConfig.setScan(true)

basePathとresourcePackageは、ハードコードしてもよかったのですが、今回は@ApplicationPathの値を引っこ抜いたのと
作成したクラスを全部同じパッケージに置いているのでこんな感じではしょりました。

Application#getClassesでは、SwaggerのApiListingResourceクラスとSwaggerSerializersクラスを入れて返す
必要があります。

  override def getClasses: java.util.Set[Class[_]] = {
    val resourceClasses: Set[Class[_]] =
      Set.empty ++
        new Reflections(classOf[JaxrsActivator].getPackage.getName)
          .getTypesAnnotatedWith(classOf[Path])
          .asScala
    val swaggerClasses = Set[Class[_]](
      classOf[ApiListingResource],
      classOf[SwaggerSerializers]
    )

    (resourceClasses ++ swaggerClasses).asJava
  }

こうなると、自分が作成したリソースクラスも手動で登録することになるので、Reflectionsで
引っこ抜きました。

Reflectionsは、Swagger JAX-RSの依存関係に含まれています。

Jackson Scala Moduleの設定

Case Classを使ったり、SeqをJAX-RSリソースクラスで使ってしまっているので、Jackson 2に対するScala用のモジュールの
設定が必要になります。

こんなクラスを実装して
src/main/scala/org/littlewings/javaee7/rest/ScalaObjectMapperProvider.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.{Consumes, Produces}
import javax.ws.rs.core.MediaType
import javax.ws.rs.ext.{ContextResolver, Provider}

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule

@Provider
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
class ScalaObjectMapperProvider extends ContextResolver[ObjectMapper] {
  override def getContext(typ: Class[_]): ObjectMapper = {
    val objectMapper = new ObjectMapper
    objectMapper.registerModule(DefaultScalaModule)
    objectMapper
  }
}

Service Providerの設定を用意しておきます。
src/main/resources/META-INF/services/javax.ws.rs.ext.Providers

org.littlewings.javaee7.rest.ScalaObjectMapperProvider

起動クラス

最後に、起動用のクラスを作成します。UndertowにJAX-RSアプリケーションをデプロイします、と。
src/main/scala/org/littlewings/javaee7/rest/Server.scala

package org.littlewings.javaee7.rest

import java.time.LocalDateTime
import javax.servlet.DispatcherType

import io.undertow.Undertow
import io.undertow.servlet.Servlets
import io.undertow.servlet.api.FilterInfo
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer

import scala.io.StdIn

object Server {
  def main(args: Array[String]): Unit = {
    val server = new UndertowJaxrsServer
    val deployment = server.undertowDeployment(classOf[JaxrsActivator])
    deployment.setContextPath("")
    deployment.setDeploymentName("resteasy-swagger")
    server.deploy(deployment)

    server.start(Undertow
            .builder
            .addHttpListener(8080, "localhost"))

    StdIn.readLine(s"[${LocalDateTime.now}] Server startup. enter, stop.")

    server.stop()
  }
}

起動すると、Enterを入力するまで浮いているサーバーになります。

確認

それでは、確認してみましょう。Undertowで作ったサーバーを起動します。

> run
[info] Running org.littlewings.javaee7.rest.Server 
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002225: Deploying javax.ws.rs.core.Application: class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002200: Adding class resource org.littlewings.javaee7.rest.CalcResource from Application class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002200: Adding class resource org.littlewings.javaee7.rest.BookResource from Application class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002200: Adding class resource io.swagger.jaxrs.listing.ApiListingResource from Application class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002205: Adding provider class io.swagger.jaxrs.listing.SwaggerSerializers from Application class org.littlewings.javaee7.rest.JaxrsActivator
2 11, 2017 9:13:32 午後 org.xnio.Xnio <clinit>
INFO: XNIO version 3.3.6.Final
2 11, 2017 9:13:32 午後 org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.3.6.Final
[2017-02-11T21:13:33.221] Server startup. enter, stop.

curlで「http://localhost:8080/api/swagger.json」にアクセスすると、生成されたSwagger Specを確認することができます。
URLは、「コンテキストパスまで〜/[BeanConfig#basePath]/swagger.json」みたいですね。

$ curl -i 'http://localhost:8080/api/swagger.json'
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: application/json
Content-Length: 2087
Date: Sat, 11 Feb 2017 12:13:59 GMT

{"swagger":"2.0","info":{"version":"1.0.0"},"host":"localhost:8080","basePath":"/api","tags":[{"name":"book"},{"name":"calc"}],"schemes":["http"],"paths":{"/book":{"get":{"tags":["book"],"summary":"find all books","description":"","operationId":"fildAll","produces":["application/json"],"parameters":[],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Seq"}}}}},"/book/{isbn}":{"get":{"tags":["book"],"summary":"find book","description":"","operationId":"find","produces":["application/json"],"parameters":[{"name":"isbn","in":"path","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Book"}}}},"put":{"tags":["book"],"summary":"register book","description":"","operationId":"register","consumes":["application/json"],"produces":["application/json"],"parameters":[{"in":"body","name":"body","required":false,"schema":{"$ref":"#/definitions/Book"}}],"responses":{"default":{"description":"successful operation"}}}},"/calc/multiply":{"get":{"tags":["calc"],"summary":"calc multiply","description":"","operationId":"multiply","produces":["text/plain"],"parameters":[{"name":"a","in":"query","required":false,"type":"integer","format":"int32"},{"name":"b","in":"query","required":false,"type":"integer","format":"int32"}],"responses":{"200":{"description":"successful operation","schema":{"type":"integer","format":"int32"}}}}},"/calc/add":{"get":{"tags":["calc"],"summary":"calc add","description":"","operationId":"add","produces":["text/plain"],"parameters":[{"name":"a","in":"query","required":false,"type":"integer","format":"int32"},{"name":"b","in":"query","required":false,"type":"integer","format":"int32"}],"responses":{"200":{"description":"successful operation","schema":{"type":"integer","format":"int32"}}}}}},"definitions":{"Seq":{"type":"object","properties":{"traversableAgain":{"type":"boolean"},"empty":{"type":"boolean"}}},"Book":{"type":"object","properties":{"isbn":{"type":"string"},"title":{"type":"string"},"price":{"type":"integer","format":"int32"}}}}}

Swagger UIを使う

でも、これだとよくわからないのでSwagger UIを使ってビジュアルに見てみます。

GitHub - swagger-api/swagger-ui: Swagger UI is a collection of HTML, Javascript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API.

Swagger UIを使う方法はいくつかあるみたいですが、今回はWildFly Swarmに組み込まれたものを使いました。

Swagger UI Server

ダウンロードして、起動。ポートは9000としました。

$ wget http://repo2.maven.org/maven2/org/wildfly/swarm/servers/swagger-ui/2017.2.0/swagger-ui-2017.2.0-swarm.jar
$ java -Dswarm.http.port=9000 -jar swagger-ui-2017.2.0-swarm.jar

この状態で、http://localhost:9000/swagger-ui/にアクセスすると、Swagger UIの画面を見ることができます。

ここで、画面上部のテキストフィールドに先ほど生成したSwagger SpecのURL(http://localhost:8080/api/swagger.json)を指定すれば
いいのですが、そのままだと動きません。

「CORSの設定してないんじゃない?」って言われているので、設定しましょう。

以下のサンプルにもあったCORSの設定を、ほぼそのまま流用。

swagger-samples/java/java-resteasy at master · swagger-api/swagger-samples · GitHub

こうなりました。
src/main/scala/org/littlewings/javaee7/rest/CorsFilter.scala

package org.littlewings.javaee7.rest

import javax.servlet._
import javax.servlet.http.HttpServletResponse

class CorsFilter extends Filter {
  override def init(filterConfig: FilterConfig): Unit = ()

  override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
    val res = response.asInstanceOf[HttpServletResponse]
    res.addHeader("Access-Control-Allow-Origin", "*")
    res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT")
    res.addHeader("Access-Control-Allow-Headers", "Content-Type")
    chain.doFilter(request, response)
  }

  override def destroy(): Unit = ()
}

UndertowのDeploymentInfoを設定するところで、ServletFilterとして追加します。

    val server = new UndertowJaxrsServer
    val deployment = server.undertowDeployment(classOf[JaxrsActivator])
    deployment.setContextPath("")
    deployment.setDeploymentName("resteasy-swagger")
    deployment.addFilter(Servlets.filter("corsFilter", classOf[CorsFilter]))
    deployment.addFilterUrlMapping("corsFilter", "/*", DispatcherType.REQUEST)
    server.deploy(deployment)

これで気を取り直して確認すると、アクセスできることが確認できます。


もう少し確認

表示されているAPIのURLを展開して、もうちょっと確認してみましょう。

例えば、「GET /calc/add」を展開するとこんな表示になるので、テキストフィールドに値を入れて
「Try it out!」を押すと結果の確認(=APIの呼び出し)もできます。

また、JSONを扱うようなAPIの場合、ちゃんとアノテーションで設定を入れていると「Model Schema」に構造が表示されるので、
これをクリックすることで入力パラメーターのテンプレートとしても使うことができます。

今回は、これを編集してデータ登録をしてみました。

まとめ

SwaggerのJAX-RS用のモジュールを使ってSwagger Specを生成し、Swagger UIで見るということをRESTEasy+Undertowで
行ってみました。

正直、Undertowでてこずるところが多く素直にJava EEサーバーにデプロイしていればもっと簡単だったのかな?と思うところも
ありますが、とりあえず目標達成、と。

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