巨人の肩から銀の弾丸

Silver Bullet from the Shoulders of Giants - 銀の弾丸が難しくても、"銅"の弾丸くらいは撃てるように。Webソフトウェア開発に加え、自然言語処理、機械学習等の分野に触れる予定です。

JAX-RS/Glassfish/Swaggerでお手軽にはじめるAPIドキュメンテーション

こんにちは!
刺すような暑さが多少は和らいできて助かりますね。。。


さて、継続的インテグレーションの続編、を書く前に。

既存のAPIにほんのちょっと手を加えるだけで、
見た目もきれいで触って試せるドキュメントを簡単に作成できました。
そういう技術がいくつか出てきているようですが、自分で試したのは初めてだったので、
ハマったポイントなども併せて、こちらで紹介していきます。


f:id:tsumiki_brick:20150816115435p:plain

f:id:tsumiki_brick:20150816115520p:plain


Swagger Core Library

今回使用したライブラリです。

github.com

The goal of Swagger™ is to define a standard, language-agnostic interface to REST APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection.

ということで、

  • 言語の種類に依存せず
  • 人間にもコンピュータにも分かりやすい形で

REST APIへのインタフェースを提供してくれることを意図したライブラリです。
人間がAPIを理解するとき、ソースコードを読んだり、あるいはWikiとかにまとめたドキュメントを読んだりすると思いますが、

  • そもそもソースコードを読めない人がAPIを使う必要に迫られる
  • Wikiのドキュメントの更新が滞り、ドキュメントが正しいかどうか分からない

という問題は往々にして起こります。
そのような問題に対して、他にも以下のようなライブラリやサービスがソリューションを提供してくれていますが、

今回はひとまず、こちらの Swagger Core Libraryを試してみました。


Swagger UI

Swagger Core Libraryで作成したAPIの情報を、ブラウザに見やすく表示してくれるライブラリ。
特徴的なのは、"Try it out!"でcurlコマンドが見れたり、実際のレスポンスが見れたりするところ。
コードを見なくても、

  • 「ちゃんと4+3=7できてる!」
  • 「というかそれ以前にAPIがきちんと動いてる!」

のを確認できるのは便利ですね。

f:id:tsumiki_brick:20150816120004p:plain


環境

今回は、

  • Java EE 7
    • JAX-RS 2.x (jersey-common-2.1.jar)
  • NetBeans8.0.2
  • GlassFish4.1
  • swagger core 1.5.0

こんな環境で試してみました。


JAX-RSアプリケーション作成

NetBeans

New Project -> Maven -> Web Application

シンプルなJAX-RSアプリケーションを作成しておきます。


pom.xmlにSwaggerの依存を追加

JAX-RS 2.xのバージョンに合わせて、swagger-jersey2-jaxrsを選択します。
サンプルアプリ作成時の最新は
http://mvnrepository.com/artifact/io.swagger/swagger-jersey2-jaxrs/1.5.0
なので、pom.xmlに以下のように

<dependency>
  <groupId>io.swagger</groupId>
  <artifactId>swagger-jersey2-jaxrs</artifactId>
  <version>1.5.0</version>
</dependency>

依存を追加します。


Swaggerの設定と初期化、resourcesを追加

ApplicationConfig.javaの概略です。

import io.swagger.jaxrs.config.BeanConfig;

@ApplicationPath("webresources")
public class ApplicationConfig extends Application {


    // "simple.maven.glassfish.jaxrs.resource"
    private static final String RESOURCE_PACKAGE = SimpleResource.class.getPackage().getName();

    public ApplicationConfig() {
        BeanConfig beanConfig = new BeanConfig();
        beanConfig.setTitle("simpleMavenGlassfishJaxrs");
        beanConfig.setDescription("A simple Maven Glassfish JAX-RS project.");
        beanConfig.setVersion("1.0.2");
        beanConfig.setSchemes(new String[]{"http"});
        beanConfig.setHost("localhost:8080"); // ex. "localhost:8002"
        beanConfig.setBasePath("/simpleMavenGlassfishJaxrs/webresources");  // ex. "/api"
        beanConfig.setPrettyPrint(true);

        beanConfig.setResourcePackage(RESOURCE_PACKAGE); // ex. "io.swagger.resources"
        beanConfig.setScan(true);
    }


    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> resources = new java.util.HashSet<>();
        addRestResourceClasses(resources);

        // enable Swagger
        resources.add(io.swagger.jaxrs.listing.ApiListingResource.class);
        resources.add(io.swagger.jaxrs.listing.SwaggerSerializers.class);

        return resources;
    }

    private void addRestResourceClasses(Set<Class<?>> resources) {
        ...
    }

}


こちらのリンクに沿って、Applicationをextendsしているクラスで以下の設定を行います。

  • コンストラクタで基本情報の初期設定
    • setScanはいちばん最後に持ってくる
    • setVersionは自分のAPIのバージョンを指定するメソッドのよう
  • io.swagger.jaxrs.listing.* の2クラスを追加

web.xmlを使った方法など、別の方法もあるので、上記リンクから辿ってみてください。


アノテーションを追加

SimpleResource.javaクラスの概略です。

@Path("/simple")
@Api(tags = {"simple"})
public class SimpleResource {

    @GET
    @Path("/hello/{message}")
    @ApiOperation(value = "Hello Message API",
            notes = "Append 'Hello, ' before the message.",
            response = HelloMessage.class)
    public HelloMessage helloMessage(@PathParam("message") String message) {
        HelloMessage helloMessage = new HelloMessage();
        helloMessage.setMessage("Hello, " + message);
        return helloMessage;
    }

    ....

}
  • SimpleResourceクラス全体に@Apiを付ける
  • ドキュメントを作成したい各エンドポイントに、@ApiOperationを付ける

のが最低限の構成のようです。


確認

http://localhost:8080/simpleMavenGlassfishJaxrs/webresources/swagger.json
にアクセスして、自分のAPIの情報が見れていたら成功です。

{
  "swagger" : "2.0",
  "info" : {
    "description" : "A simple Maven Glassfish JAX-RS project.",
    "version" : "1.0.2",
    "title" : "simpleMavenGlassfishJaxrs"
  },
  "host" : "localhost:8080",
  "basePath" : "/simpleMavenGlassfishJaxrs/webresources",
  "tags" : [ {
    "name" : "simple"
  } ],
  "schemes" : [ "http" ],
  "paths" : {
    "/simple/hello/{message}" : {
      "get" : {
        "tags" : [ "simple" ],
        "summary" : "Hello Message API",
        "description" : "Append 'Hello, ' before the message.",
        "operationId" : "helloMessage",
        "parameters" : [ {
          "name" : "message",
          "in" : "path",
          "required" : true,
          "type" : "string"
        } ],
        "responses" : {
          "200" : {
            "description" : "successful operation",
            "schema" : {
              "$ref" : "#/definitions/hello message"
            }
          }
        }
      }
    },
    "/simple/path_param_addition/{op1}/{op2}" : {
      "get" : {
        "tags" : [ "simple" ],
        "summary" : "Path Param Addtion API",
        "description" : "Adds two numbers.",
        "operationId" : "pathParamAddition",
        "parameters" : [ {
          "name" : "op1",
          "in" : "path",
          "required" : true,
          "type" : "integer",
          "format" : "int32"
        }, {
          "name" : "op2",
          "in" : "path",
          "required" : true,
          "type" : "integer",
          "format" : "int32"
        } ],
        "responses" : {
          "200" : {
            "description" : "successful operation",
            "schema" : {
              "$ref" : "#/definitions/path param result"
            }
          }
        }
      }
    },
    "/simple/path_param_division/{op1}/{op2}" : {
      "get" : {
        "tags" : [ "simple" ],
        "summary" : "Path Param Division API",
        "description" : "Divide the first number by the second number.",
        "operationId" : "pathParamDivision",
        "parameters" : [ {
          "name" : "op1",
          "in" : "path",
          "required" : true,
          "type" : "integer",
          "format" : "int32"
        }, {
          "name" : "op2",
          "in" : "path",
          "required" : true,
          "type" : "integer",
          "format" : "int32"
        } ],
        "responses" : {
          "200" : {
            "description" : "successful operation",
            "schema" : {
              "$ref" : "#/definitions/path param result"
            }
          }
        }
      }
    }
  },
  "definitions" : {
    "hello message" : {
      "type" : "object",
      "required" : [ "message" ],
      "properties" : {
        "message" : {
          "type" : "string",
          "description" : "message"
        }
      }
    },
    "path param result" : {
      "type" : "object",
      "required" : [ "result" ],
      "properties" : {
        "result" : {
          "type" : "string",
          "description" : "result"
        }
      }
    }
  }
}

よさそうな雰囲気ですね。


Swagger UI

続いて、さっきのswagger.jsonをもとにドキュメンテーションのUIを作ります。
特別な設定は必要なく、

などが分かりやすいかと思います。
dist内のindex.htmlをブラウザで開いても大丈夫そう。


スクリーンショット

http://localhost:8080/swagger-ui-dist
にアクセスして、
最上部にあるAPIのURL欄に自分のswagger.jsonまでのURLを入れると、記事冒頭のスクリーンショットが見られます!


はまった点

と、ここまでスムーズにいけば良いのですが、
自分の場合は、swagger.jsonがスカスカになるという問題がありました。

{"swagger":"2.0","info":{"version":"1.0.2"},"host":"localhost:8080/simpleMavenGlassfishJaxrs/webresources","basePath":"/simple","schemes":["http"]}


これは

Swaggerの設定と初期化、resourcesを追加


のところで、サンプルをコピペしていたのが原因だったので、

beanConfig.setResourcePackage("io.swagger.resources");

上の手順にあるように、きちんと自分のリソースクラスを指定したのですが、

private static final String RESOURCE_PACKAGE = SimpleResource.class.getPackage().getName();
....
beanConfig.setResourcePackage(RESOURCE_PACKAGE);


今度はアプリケーション自体がGlassfishにデプロイできなくなるはめに。

java.lang.NoSuchMethodError: com.fasterxml.jackson.databind.AnnotationIntrospector.findPropertyIndex(Lcom/fasterxml/jackson/databind/introspect/Annotated;)Ljava/lang/Integer;
    at io.swagger.jackson.ModelResolver.resolve(ModelResolver.java:372)
    at io.swagger.jackson.ModelResolver.resolve(ModelResolver.java:151)
    at io.swagger.converter.ModelConverterContextImpl.resolve(ModelConverterContextImpl.java:85)
    at io.swagger.jackson.ModelResolver.resolveProperty(ModelResolver.java:131)
    at io.swagger.jackson.ModelResolver.resolveProperty(ModelResolver.java:93)
    at io.swagger.converter.ModelConverterContextImpl.resolveProperty(ModelConverterContextImpl.java:65)
    at io.swagger.converter.ModelConverters.readAsProperty(ModelConverters.java:58)
    at io.swagger.jaxrs.Reader.parseMethod(Reader.java:768)
    at io.swagger.jaxrs.Reader.read(Reader.java:286)
    at io.swagger.jaxrs.Reader.read(Reader.java:169)
    at io.swagger.jaxrs.Reader.read(Reader.java:146)
    at io.swagger.jaxrs.config.BeanConfig.setScan(BeanConfig.java:170)
    ....


NetBeansでアプリケーションのDependencyを確認してもjackson-databind-2.4.2.jarが入っているし、このバージョンではfindPropertyIndexメソッドもきちんと実装されているのに・・・?

結果的には、実はアプリケーションの問題ではなかったので、
Glassfishで使われているjacksonのバージョンが古いのでそれを新しくすることで問題が解決しました。


おわりに

新規のアプリからでも、既存のアプリからでも、
手間をかけずにお手軽にAPIドキュメントを作成して、楽しく開発しましょう!