日々常々

ふつうのプログラマがあたりまえにしたいこと。

API仕様をHTMLファイルで出力する(SpringBoot+Redocly CLI)

このエントリでの「API」はHTTP APIです。Javadocなどでドキュメント化できるAPIではないです。この件に関してはいつかブログとか書くかもだけど、いまは ツイートを参照してください。
  • SpringBoot 3.1.0
  • springdoc-openapi 2.1.0
  • springdoc-openapi-gradle-plugin 1.6.0

できるようになること

./gradlew generateOpenApiHtml

このコマンドを実行するだけで ./build/redoc-static.html ができます。この子は「zero-dependency HTML」なので適当な場所にこのファイルだけ持っていって大丈夫です。CIで出力したあとに持っていくファイルが少ないのは楽でいいですよね。 あ、CDNのJSとかフォントとか使ってるのでインターネットに繋がる環境にはしてね。

今回はこれができるようになるまでの仕組みをだらだら書くよ。だらだら。

springdoc-openapiでAPI仕様を見る

SpringBootでは springdoc-openapi を使うと http://localhost:8080/swagger-ui/index.html でSwagger UIを使ってブラウザで見れるようになります。

こんなの。

これで満足なことも多いのですが、実務では静的HTMLファイルが欲しいってなったりします。こういう時に:

  • Swagger UIはアプリケーションが動作している時にしか見れない
  • そのためアプリケーションにアクセスできない状態(ダウンしていたり、権限的な問題があったり)だと使用できない
  • 開発中のAPIが半端に見えると混乱する
  • 三者に渡したい

「ブラウザで見れるならそのまま持っていったら?」と思うかもしれませんが、Swagger UIはそれ自体で情報を持っているわけではありません。 springdoc-openapiを使用すると org.webjars:swagger-ui が依存に入りますが、このリソースがそのまま表示されているだけです。

詳しくは springdoc-openapiのドキュメントを見てください。依存関係の図とかわかりやすいです。

http://localhost:8080/swagger-ui/index.html をブラウザで開いて「ソースを表示」ってやるのと GitHub/swagger-api/swagger-uiのindex.html が完全に一致することからも素のSwaggerUIでしかないことが分かるかと思います。

ではアプリケーションのAPI情報はどのように取得しているかですが、 http://localhost:8080/v3/api-docs で見れるJSONがそれで、springdoc-openapiによってOpenAPI仕様で出力されるようになっています。springdoc-openapiの本業はこれで、SwaggerUIで表示できるのはおまけ的な機能です。

流れをおさらい。

実際のフレームワークの動きとは異なっています(このタイミングでControllerにアクセスしたりしない)が、どこにあるかのイメージです。SwaggerUIの時点ではアプリケーションに関係なく、分けられるってところを伝えたい感。

ブラウザの動きは開発ツールとかで見てみるとわかるかと。

index.html 開いてから api-docs 取りに行っておりますね。

だもんでSwagger UIは独立したアプリケーションであり、別にローカル以外でも適当なOpenAPIのJSONを食わせればそのまま表示できるってのがわかるかと思います。

ローカルで表示したい

「SwaggerUIをローカルに持ってきて、OpenAPIのJSONもローカルに持ってきて、そのままひらけばよくない?」とか思うかもしれませんが、ローカルリソースをそのまま開けるほど最近のブラウザは甘くありません。 「ローカルファイル間ならよくね?」って思ったりもするんですが、それやるとローカルファイルからリモートアクセスを遮断しないといけなくなり、世界が困る(クソデカ主語)。ブラウザの設定変えたりすればできますが、それも同様。

こういう時は開発者端末でならローカルにHTTPサーバーをさくっと立ててしまえば良いのですが、非開発者やローカルでHTTPサーバーを実行する機会の少ない人も多く、みんなにそんなことを求められないです。 見たい人全員の手元にOpenAPIブラウザ的なもの(SwaggerUIもその一種)があれば良いのですが、それも求められるものでもないと思います。

ということで(強引)、ローカルで見るにはHTMLファイルにする必要があるのです。

OpenAPIをHTMLにするツールはいくつかありまですが、今回は Redocly CLI を使います。redoc-cliの後継です。 使い方はドキュメントを見てもらえればいいんですが、 npx が入っているなら @redocly/cli build-docs にOpenAPI仕様の場所を教えてあげるだけです。 SpringBootアプリケーションが動作している状態であればこんな感じで redoc-static.html が実行ディレクトリにできます。

% npx @redocly/cli build-docs http://localhost:8080/v3/api-docs
Found undefined and using theme.openapi optionsPrerendering docs
🎉 bundled successfully in: redoc-static.html (50 KiB) [⏱ 7ms].

これで満足してもいいと思いますが、もう少し行きたい。

OpenAPI仕様を自動出力したい

「HTML出そ、えーとSpringBootアプリケーション起動して……」ってやるのはかったるいと感じる私の同類さんは、勝手にファイルが出力されると嬉しいでしょう。

springdoc-openapi はそういう同類さんのためにプラグインを用意してくれています。私はGradle派なので(仕事の6割はMavenなんだけど)、ここでは springdoc-openapi-gradle-plugin を紹介します。MavenPluginもあるね、使ったことないけど。

使い方はプラグイン追加したらタスクが生えるので、それ実行するだけです。

plugins {
      id "org.springframework.boot" version "3.1.0"
      id "org.springdoc.openapi-gradle-plugin" version "1.6.0"
}

こうして ./gradlew generateOpenApiDocs を実行するだけ。ワオ簡単。 ドキュメントはSpringBoot2.7になってるけど、3.1でも問題なく動いてるから気にしてない。SpringBoot自体にどうこうするもんでもないしね。

ポート変えてたりProfileとかパラメタ与えたくなったら springdoc-openapi-gradle-pluginの設定方法 を見てください。それなりに複雑なアプリケーションでやったら引っかかったけど、設定で十分対応できたので多分大丈夫。ダメだったらコントリビュートチャンスですよ。

ちなみにドキュメントでは毎回 clean 叩いてるけど、これはコード変えようが出力済みだったら UP-TO-DATE になっちゃうから。私は clean すると他の消えていやんってなるので UP-TO-DATE 避けは以下を入れてます。お好みで。

tasks.withType(org.springdoc.openapi.gradle.plugin.OpenApiGeneratorTask).configureEach {
    outputs.upToDateWhen { false }
}

upToDateWhen が何かしらない?大丈夫、普通は知らないと思うよ。Gradleのドキュメントに載ってるよ!(親切)

もちろんメソッドのアノテーション書き換えとかならともかく、クラス名の変更とか削除になってくるとゴミは残るので、安定したドキュメントが欲しいなら clean した方がいい。使い分け使い分け。面倒ならいつも clean すればいいんじゃないかな。その場合は「毎回 clean 叩くこと」とかするのはダサいから dependsOn した方がいいだろけど。

問題はSpringBootアプリケーションを実際に動かすので、起動失敗したり時間がかかるような構成だと使えないこと。 簡単に起動できないなら、ローカルで起動できないような構成を見直したほうがいいよ?悪いこと言わないから。って言っても難しいところもあるのは知ってるけど……。 これを機に正常化するとか考えてもいいんじゃないかな。

なお springdoc-openapi-gradle-plugin などを使用しないOpenAPI仕様の自動出力の別解として、SpringBootTestを使用した出力もできます。 こんな感じのテストを書くだけ。え、pluginよりこっちのが楽?私もそう思うよ……

@Test
void generateOpenApiJSON() throws Exception {
    mockMvc.perform(get("/v3/api-docs"))
            .andDo(result ->
                    Files.write(Paths.get("openapi.json"), result.getResponse().getContentAsByteArray())
            );
}

ある程度までであればこれで十分かもしれませんが、いくつか気になる点とか注意とかあるので書いておきますね:

  • これは一体なんのテストなんだ?ってなる。折り合いつけられれば大丈夫。
  • @SpringBootTest でならいけるけど @WebMvcTest とかだと一捻り必要。
  • SpringBootTestはあくまでテストなんで動くアプリケーションとの違いがないこともない。springdoc-openapi-gradle-pluginはちゃんとアプリケーション動かしてる。

ご利用は計画的に😉

さておき、さあこれで build/openapi.json に出力されるようになった。 でもHTMLにするのめんどいよね。

HTML化

あとはRedocly CLIを叩ければいいのでなんでもいいんだ。 Redocly CLIは普通にローカルファイルを食べてくれるので、こう。

% npx @redocly/cli build-docs build/openapi.json
Found undefined and using theme.openapi optionsPrerendering docs
🎉 bundled successfully in: redoc-static.html (50 KiB) [⏱ 8ms].

SpringBootアプリケーションが動作してる必要もないし、どうとでもなる気はするよね。で、前のステップとまとめて雑に考えるとこれくらいが選択肢になるか。

  1. シェルスクリプトでGradleとRedocly CLIを順番に呼び出す
  2. npm からGradleとRedocly CLIを使う
  3. GradleからRedocly CLIを使う

1がポータビリティ高くていい気はする。2だとRedocly CLIのバージョン管理ができてよさげ。3はGradleから出たくない人向け。

ここでは雑に3、さらに gradle-node-plugin を使ったりせず、手元で npx が使えるのを期待する。

tasks.register("generateOpenApiHtml", Exec) {
    dependsOn 'generateOpenApiDocs'
    group = "openapi"
    workingDir "${DEFAULT_BUILD_DIR_NAME}"
    commandLine 'npx', '@redocly/cli', 'build-docs', 'openapi.json'
}

これで冒頭のように ./gradlew generateOpenApiHtml でHTML出るようになります。

上から2つ目の group はなくても実行に問題ないけど、なんとなく ./gradlew tasks に並んで表示させたかったから入れてる。

これでHTMLでAPIドキュメントが手に入るようになった。

よーし、これをCIで実行する……となると npx が入ってなきゃだし、キャッシュしなきゃ毎回ダウンロードだるいし、うん、いろいろあるね。その辺はどうにかなるでしょ。どうにかしましょう。今回は書きません。

ここまできたらSwaggerUIはなくてもいいかもしれません。APIクライアントとしては便利かもしれませんが、RedocのHTMLを正とするならSwaggerUIは外して普段から自分たちもRedocを見る方が改善が進んだりします。どっぐふーでぃんぐどっぐふーでぃんぐ。 springdoc-openapiもswagger部分は切り離せるように作られてますしね。

おれたちのたたかいははじまったばかりだ(完)

まとめ

実際動かすための対応はすごく少ないんだけど、まとめません。自分で拾って。 それぞれのツールが何をするものか、切れ目はどこにあるかをわかっていないとどハマりする系の話だと思いますので。

これに近いことでやっているところもありますし、springdoc-openapi-gradle-pluginとかは使用せず動作しているアプリケーションに向けてRedocly CLIをCIで叩いてるものもあります。 ドキュメントのライフサイクルとか、管理単位とか、アプリケーションの数とか、組織体制とか、その辺諸々勘案して選択できるためには小さい道具をそれぞれ理解してうまく組み合わせるのが、ハマりどころも少ないし対応幅も広くなると思います。 たとえばAPIドキュメントとしての充足を求める局面ではSwaggerUIよりRedocでカスタマイズかけていく方が実用的でしょうし、ドキュメントを見ながら改善を繰り返している時はCIを通してようやく見れる状態はフィードバックが遅すぎるはず。手元で確認できる道具を備えているのに越したことはありません。

こういうのをUNIXという考え方では「ソフトウェアの梃子」って表現してる。

UNIXとか触ったことない、触る機会は今後もないだろうって人でも読んでおくといいと思うんだ。小さめで144ページと薄い本だし。 (ちなみに本書はシェルスクリプト推しなので選択肢3は、うん、まぁ。気にしない気にしない。)