LoginSignup
12
11

More than 3 years have passed since last update.

Kotlin/SpringbootでSwaggerを使って、コードからAPIドキュメントを自動生成するまで

Posted at

前回この記事でKotlin/SpringbootでCRUD操作のできるREST APIを作成しました。
そこではREST APIの動作確認をするのにいちいちcurlコマンド使って確認していましたが、正直かなりめんどくさいです。
PostmanAdvanced REST clientを使うのも一つの手ですが、今回はSwaggerを導入してAPIドキュメントを作成したいと思います。

Swaggerとは

SwaggerとはREST APIのAPIドキュメントを作成するためのオープンソースのフレームワークです。
実際どうゆうものなのかはSwagger Editorを見るとイメージが付きやすいかと思います。

他にも様々な機能がありますが、今回は説明を省きます。
詳細や概要が気になる方はこちらをご覧ください。
Swaggerの概要をまとめてみた。
SwaggerでRESTful APIの管理を楽にする

今回はSpringFoxを使って導入します。

SpringFoxとは

Swaggerをベースに、Springbootで作られたAPIからAPIドキュメントを自動で作成するライブラリです。
具体的にはSwagger準拠のjsonファイルを作成し、springfox-swagger-uiを使用することでSwagger EditorのようにAPIドキュメントを確認できるようになります。

今回はバージョン2.9.2を使用します。

使い方

まずは最低限CRUD操作のできるREST APIを準備します。
今回はこちらの記事で作成したQiita APIをそのまま流用します。

最小構成でjsonファイルを返す

まずはbuild.gradleにSpringFoxを追加します。

build.gradle.kts
dependencies {
    ...
    compile ('io.springfox:springfox-swagger2:2.9.2')
}

次に適当な場所にConfigファイルを作成します。
今回はcom.example.sample_qiitaパッケージ内にConfiguration.ktを作成します。

Configuration.kt
package com.example.sample_qiita

import org.springframework.context.annotation.Configuration
import springfox.documentation.swagger2.annotations.EnableSwagger2

@Configuration
@EnableSwagger2
class SpringFoxConfig { //名前はなんでもいいです。
}

これを作成することによってSwaggerが有効化されます。

ではさっそくRunしてjsonファイルを確認してみましょう。
Runしてhttp://localhost:8080/v2/api-docsにアクセスすると、Swaggerの書式で書かれたAPIドキュメントのjsonがずらずらと並んでいると思います。

※サンプルのjsonファイルを置いておくので、とりあえずどんなものか見てみたいって方はこちらをコピペして確認してください。

{"swagger":"2.0","info":{"description":"Api Documentation","version":"1.0","title":"Api Documentation","termsOfService":"urn:tos","contact":{},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0"}},"host":"localhost:8080","basePath":"/","tags":[{"name":"basic-error-controller","description":"Basic Error Controller"},{"name":"qiita-controller","description":"Qiitaデータの取得・編集コントローラ"}],"paths":{"/api/qiitas":{"get":{"tags":["qiita-controller"],"summary":"全Qiitaデータ取得","description":"全てのQiitaデータを取得します。","operationId":"getAllQiitasUsingGET","produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/Qiita"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}},"deprecated":false},"post":{"tags":["qiita-controller"],"summary":"createNewQiita","operationId":"createNewQiitaUsingPOST","consumes":["application/json"],"produces":["*/*"],"parameters":[{"in":"body","name":"qiita","description":"qiita","required":true,"schema":{"$ref":"#/definitions/Qiita"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/Qiita"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}},"deprecated":false}},"/api/qiitas/{id}":{"get":{"tags":["qiita-controller"],"summary":"getQiitaBiId","operationId":"getQiitaBiIdUsingGET","produces":["*/*"],"parameters":[{"name":"id","in":"path","description":"id","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/Qiita"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}},"deprecated":false},"put":{"tags":["qiita-controller"],"summary":"updateQiitaById","operationId":"updateQiitaByIdUsingPUT","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"id","in":"path","description":"id","required":true,"type":"integer","format":"int64"},{"in":"body","name":"newQiita","description":"newQiita","required":true,"schema":{"$ref":"#/definitions/Qiita"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/Qiita"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}},"deprecated":false},"delete":{"tags":["qiita-controller"],"summary":"deleteQiitaById","operationId":"deleteQiitaByIdUsingDELETE","produces":["*/*"],"parameters":[{"name":"id","in":"path","description":"id","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"OK"},"204":{"description":"No Content"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"deprecated":false}},"/error":{"get":{"tags":["basic-error-controller"],"summary":"error","operationId":"errorUsingGET","produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":{"type":"object"}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}},"deprecated":false},"head":{"tags":["basic-error-controller"],"summary":"error","operationId":"errorUsingHEAD","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":{"type":"object"}}},"204":{"description":"No Content"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"deprecated":false},"post":{"tags":["basic-error-controller"],"summary":"error","operationId":"errorUsingPOST","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":{"type":"object"}}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}},"deprecated":false},"put":{"tags":["basic-error-controller"],"summary":"error","operationId":"errorUsingPUT","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":{"type":"object"}}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}},"deprecated":false},"delete":{"tags":["basic-error-controller"],"summary":"error","operationId":"errorUsingDELETE","produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":{"type":"object"}}},"204":{"description":"No Content"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"deprecated":false},"options":{"tags":["basic-error-controller"],"summary":"error","operationId":"errorUsingOPTIONS","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":{"type":"object"}}},"204":{"description":"No Content"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"deprecated":false},"patch":{"tags":["basic-error-controller"],"summary":"error","operationId":"errorUsingPATCH","consumes":["application/json"],"produces":["*/*"],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":{"type":"object"}}},"204":{"description":"No Content"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}},"deprecated":false}}},"definitions":{"ModelAndView":{"type":"object","properties":{"empty":{"type":"boolean"},"model":{"type":"object"},"modelMap":{"type":"object","additionalProperties":{"type":"object"}},"reference":{"type":"boolean"},"status":{"type":"string","enum":["100 CONTINUE","101 SWITCHING_PROTOCOLS","102 PROCESSING","103 CHECKPOINT","200 OK","201 CREATED","202 ACCEPTED","203 NON_AUTHORITATIVE_INFORMATION","204 NO_CONTENT","205 RESET_CONTENT","206 PARTIAL_CONTENT","207 MULTI_STATUS","208 ALREADY_REPORTED","226 IM_USED","300 MULTIPLE_CHOICES","301 MOVED_PERMANENTLY","302 FOUND","302 MOVED_TEMPORARILY","303 SEE_OTHER","304 NOT_MODIFIED","305 USE_PROXY","307 TEMPORARY_REDIRECT","308 PERMANENT_REDIRECT","400 BAD_REQUEST","401 UNAUTHORIZED","402 PAYMENT_REQUIRED","403 FORBIDDEN","404 NOT_FOUND","405 METHOD_NOT_ALLOWED","406 NOT_ACCEPTABLE","407 PROXY_AUTHENTICATION_REQUIRED","408 REQUEST_TIMEOUT","409 CONFLICT","410 GONE","411 LENGTH_REQUIRED","412 PRECONDITION_FAILED","413 PAYLOAD_TOO_LARGE","413 REQUEST_ENTITY_TOO_LARGE","414 URI_TOO_LONG","414 REQUEST_URI_TOO_LONG","415 UNSUPPORTED_MEDIA_TYPE","416 REQUESTED_RANGE_NOT_SATISFIABLE","417 EXPECTATION_FAILED","418 I_AM_A_TEAPOT","419 INSUFFICIENT_SPACE_ON_RESOURCE","420 METHOD_FAILURE","421 DESTINATION_LOCKED","422 UNPROCESSABLE_ENTITY","423 LOCKED","424 FAILED_DEPENDENCY","426 UPGRADE_REQUIRED","428 PRECONDITION_REQUIRED","429 TOO_MANY_REQUESTS","431 REQUEST_HEADER_FIELDS_TOO_LARGE","451 UNAVAILABLE_FOR_LEGAL_REASONS","500 INTERNAL_SERVER_ERROR","501 NOT_IMPLEMENTED","502 BAD_GATEWAY","503 SERVICE_UNAVAILABLE","504 GATEWAY_TIMEOUT","505 HTTP_VERSION_NOT_SUPPORTED","506 VARIANT_ALSO_NEGOTIATES","507 INSUFFICIENT_STORAGE","508 LOOP_DETECTED","509 BANDWIDTH_LIMIT_EXCEEDED","510 NOT_EXTENDED","511 NETWORK_AUTHENTICATION_REQUIRED"]},"view":{"$ref":"#/definitions/View"},"viewName":{"type":"string"}},"title":"ModelAndView"},"Qiita":{"type":"object","required":["id","title"],"properties":{"content":{"type":"string","description":"Qiitaコンテンツ"},"id":{"type":"integer","format":"int64","description":"QiitaID"},"title":{"type":"string","description":"Qiitaタイトル"}},"title":"Qiita"},"View":{"type":"object","properties":{"contentType":{"type":"string"}},"title":"View"}}}

jsonファイルだけ見せられても全くわからないので、Swagger Editorの左の部分を全部消してjsonをコピペするといい感じにAPIドキュメントが表示されます。

ローカルでSwagger UIを見れるようにする。

試しに書いた簡単なコードならいいですが、開発で作成したjsonを毎回Swagger Editorにアクセスして不用意にネット上に上げるわけにはいきません。
なのでSwagger UIを使ってローカルで確認できるようにしましょう。

まずはgradle.buildに以下の文を追加します。

build.gradle.kts
dependencies {
    ...
    compile ('io.springfox:springfox-swagger2:2.9.2')
    compile ("io.springfox:springfox-swagger-ui:2.9.2") //追加
}

そしてRunしたらhttp://localhost:8080/swagger-ui.htmlにアクセスしてみましょう。

スクリーンショット 2019-06-07 15.50.04.png

もしうまく表示されない場合はキャッシュが邪魔している可能性があるので、スーパーリロードなどを試してみましょう。

ソースコードからAPIドキュメントをカスタマイズする

APIドキュメントは見れるようになりましたが、Spring Bootに初期から入っているerrorコントローラが表示されていたり、ドキュメントやコントローラの説明が一切なく非常にわかりづらいです。
なのでConfigクラスとアノテーションを使って、より見やすくしましょう。

Configクラスを使う方法

先ほど作成したConfiguration.ktに以下の内容を追記します。

Configuration.kt
package com.example.sample_qiita

import ...

@Configuration
@EnableSwagger2 //Springfox Swagger 2を有効化
class SpringFoxConfig {

   @Bean //@Beanを使用することでDocketインスタンスを設定することができます。
   fun api(): Docket = Docket(DocumentationType.SWAGGER_2)
           .groupName("Qiita") //グループネームの設定。
           .select() 
            //ApiSelectorBuilderを返し、swaggerを通してエンドポイントを制御する。
           .apis(RequestHandlerSelectors.any()) 
            //指定したAPIのみを出力したい場合使用する。今回はデフォルトのanyを使用。
           .paths(PathSelectors.regex("/api.*")) 
            //指定したパス以下のAPIのみを対象としたい場合使用する。これをしないとSpring Bootのデフォルトのエラーまで出力されてしまう。。
           .build()
            //select()とapis(), paths()を設定した後に必ず書く必要がある。

           .apiInfo(apiinfo())
            //APIの冒頭の説明部分をapiinfo()を参照して変更します。
           .protocols(newHashSet("http", "https"))
            //複数のプロトコル/スキーマを設定することができます。

   fun apiinfo(): ApiInfo = ApiInfoBuilder()
           .title("Qiita API")
           .description("Qiita情報を扱うためのAPIです")
           .version("1.0")
           .contact(Contact("sample_qiita_example", "http://example.com", "example@mail.com"))
           .build()
}

早速Runしてみましょう。

スクリーンショット 2019-06-09 22.08.20.png

ちゃんとAPIドキュメントの概要欄が変更されていますね。

アノテーションを使用する方法

今度はコントローラやモデルの部分を変更していきましょう。
まずは以前作成したQiitaController.ktQiita.ktに変更を加えましょう。

QiitaController.kt

import ...

@RestController
@RequestMapping("/api")
@Api(description = "Qiitaデータの取得・編集コントローラ") 
//APIの概要やタグを変更することができる。
class QiitaController(private val qiitaRepository: QiitaRepository) {

    @ApiOperation(value = "全Qiitaデータ取得", notes = "全てのQiitaデータを取得します。", response = Qiita::class)
    //APIの動作やパスなどを記載することができる。
    @GetMapping("/qiitas")
    fun getAllQiitas(): List<Qiita> =
            qiitaRepository.findAll()

...
}
Qiita.kt
package com.example.sample_qiita.model

import ...

@Entity
data class Qiita (
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
        @ApiModelProperty(value = "QiitaID", required = true)
        //APIのモデルに関する情報を設定できる。
        val id: Long = 0,

        @get: NotBlank
        @ApiModelProperty(value = "Qiitaタイトル", required = true)
        val title: String = "",

        @get: NotBlank
        @ApiModelProperty(value = "Qiitaコンテンツ")
        val content: String = ""
)

Runした結果がこちらになります。

スクリーンショット 2019-06-09 22.46.58.png

上手く反映されていますね。

その他の変更方法

他にも様々なアノテーションやDocketを使用することでAPIドキュメントをより細かく自由に編集することができるので、是非ともこちらのSpringfoxの公式ドキュメントをご確認ください。

参考文献

Spring Boot/KotlinでCRUD操作のできるAPIを作成する方法
Springfox Reference Documentation
Swaggerの概要をまとめてみた。
SwaggerでRESTful APIの管理を楽にする
SpringFoxを使ってSwaggerのjsonを吐き出す
Spring BootでSpringFox(Swagger)を試す
Adding Swagger to your Spring Rest API written in Kotlin

12
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
11