Swaggerというものがあるのはなんとなく知っていたのですが、使ったことがなかったので
試してみます。
The Best APIs are Built with Swagger Tools | Swagger
あんまりちゃんと調べたことなかったのですが、JSON/YAMLを使って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用のモジュールはこちら。
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("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を使ってビジュアルに見てみます。
Swagger UIを使う方法はいくつかあるみたいですが、今回はWildFly Swarmに組み込まれたものを使いました。
ダウンロードして、起動。ポートは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