日々常々

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

たすけてあっぷる、iPhoneのロック画面に閉じれないモーダルダイアログが出たの!

絶望してるときのツイート

一見ロック画面に表示されるただのダイアログなんですが、どちらのボタンを押しても閉じてくれません。

経緯

ノイズキャンセリングヘッドセットはBoseのQC35IIを使ってます。

もう5年半か……

これ今はもう珍しくもないBluetoothで複数デバイスに繋がるもので、MaciPhoneのPCとMacMacに繋がるようにしています。 ただ複数に繋がってるとノイズが入ったり、Macに繋がってる時にiPhoneに通知来て音鳴ったりしたらそっちに切り替わったりするので、安定して使いたい時は接続切ってます。

で、繋がってる状態でiPhoneBluetoothボタンでプチっと切ると冒頭のダイアログが出ます。 普段はそのまま閉じるだけなんですが、今回はBluetooth切断→ダイアログ表示間いろんな操作をした結果、ロック画面に操作閉じてくれないダイアログが出る状態になりました。

試行錯誤

iPhoneは少し前からボタンを長押しするだけだと電源切ったり再起動はできなくなっています。

(ダイアログ出てるスクショとれなかったけど、この上に新規接続云々のダイアログが出てます)

長押しすると「スライドで電源オフ」が出て、それスライドすると切れるのですが、今回はダイアログが邪魔してスライドできない。 画面操作できない状態でも再起動したいはあるあるで、強制再起動的なのはないのかと調べたらありまして。

iPhoneを強制的に再起動する - Apple サポート (日本)

まー手順用意されてるよね、うんうん。へぇ面白い操作だなぁと思いながらやってみた。

……緊急電話のカウントダウンが始まるんだが???

iOSのバージョンも合ってるしなぁ、どうにかならないかなぁとMacに繋いでみたりしたんだけどどうもできなくて。 パスコード入力の画面は出るけどダイアログがあるからダメだし、FaceIDにもなってくれないし……

これはもしかしてAppleStoreに持ち込むか?いや電池切れるの待つか?でもフル充電しちゃってるぞ??とか思ってたところで、AppleStoreの場所を確認しとくかとサポートページを開いたら、問い合わせがあったのを思い出しました。電話はやだけどチャットならなんとか頑張れるかもしれない、頑張れ私。(よく考えたらiPhoneロックされてるから電話もできないわ。)

問い合わせ

ということでAppleのチャットサポートに聞いてみました。 ウィンドウひらいて2分くらいで応答してくれたので、状況とやってみたことをぶちまける。

そしたら「お困りですよね、急いでますよね」というよりそうメッセージに続けて「強制再起動してみて」とページを案内されました。

support.apple.com

「やったけどなぁ……」と思いながら開いてみたら違うページだった。 で書いてる通りにやってみたら再起動できた。

そっこー解決した。ありがとうあっぷる!

解決時のツイート

改めて確認すると

違うページだけど、見直してみると手順は同じ。

1つ目にみたページの画像で、「音量下げボタンを押しっぱなしでサイドボタンを押す」と誤認していた模様。焦るとダメだね!

案内してくれたページはアニメーションGIFになっていたので、間違えずにできたようです。

なるほどねー気づかなかったー。

……てか今気づいた。

これ書き始めた時は「1つ目の通りにやっても強制再起動できないんですけど!?」って書くつもりだったのに、ただの私のうっかりを晒すだけのものになってしまった。つらい。

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は、うん、まぁ。気にしない気にしない。)

ローカルでSonarQubeを触ってみる(MavenとJaCoCoと)

  • SonarQube 9.9.1-community
  • Maven 3.9.1

これはなに

SonarQubeよくわからん人向け。

「何できるもんなんだろ」とか「現場でSonarQube使ってるものの、設定とか影響範囲わからなくて怖い」とか、色々とあると思う。ローカルでいじくれるとイメージも湧きやすいしハードル下がるかなと思って書いた。

あとJaCoCo周りはあちこちでなんやかんやあるので。

準備

SonarQube

Dockerで。

docker run -p 9000:9000 --rm sonarqube:9.9.1-community
  1. http://localhost:9000 ひらく。
  2. admin/admin でログイン
  3. パスワード設定画面になるのでパスワードを hoge に変更

通常はTokenを作成するが、ここではSonarQubeの設定も揮発するコンテナで動かしてるのでやらない。

他の形

https://www.sonarsource.com/products/sonarqube/downloads/

Docker使わないならダウンロードして解凍してでもできる。けど「とりあえず触る」にはちょっと面倒だよね。

手元に入れるの面倒ならオープンソースならSonarCloudが無料で使えるけど、このブログのタイトルから外れるので書かない。

アプリケーション(ない場合)

https://start.spring.io とかで spring-boot-starter-webpom.xml とってきて準備。

 ▼pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>dev.irof.suburi</groupId>
    <artifactId>spring-maven-sonar</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>すぶり</name>

    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

SpringBootである必要は何一つない。

見ての通り(本エントリはMavenある程度わかる人を想定しています)、SonarQubeに関する記述は pom.xml にはありません。 なのでもし手元に手頃なMavenプロジェクトあるならそのまま使えばいいです。

以降は ./mvnw 使ってるけど、パス通ってるMaven使うなら mvn でもいいし、 mvn wrapper:wrapper 叩いてからでもいい。

実行

./mvnw clean package sonar:sonar -Dsonar.login=admin -Dsonar.password=hoge

さっきパスワードを hoge に変えたから sonar.password はそれにしてる。他のにしてたら変えてね。 トークン使ってみたり別ユーザー作ってみたりしていじってみましょう。

説明

sonar-maven-plugin は特別扱いされてるので、 pom.xml に何も書かなくても sonar:sonar で実行できちゃいます。

とはいえ設定書きたいとかバージョン制御したいとかあると思うんで、本番では書いた方がいいかもですね。

sonar.login / sonar.password とかは前述の通り。SonarQubeのURLはデフォルト http://localhost:9000 なので何も書いてないだけです。 この辺りは設定したくなるはずなので、ドキュメント読んで settings.xml に書くとか、実行シェルに書くとかしていきませう。

ちなみにプラグインのソースは https://github.com/SonarSource/sonar-scanner-maven にあります。

なお clean package とかしなくても sonar:sonar だけでスキャンはしてくれます。 でも普通はテストの通ったものをスキャンすると思うし、テスト結果もSonarQubeにのっけときたいと思うし。

確認

前述の pom.xml を使ったなら http://localhost:9000/projects に「すぶり」ってプロジェクトができてるはず。

この名前は /project/name になる。

あとは好きにどうぞ。

もう一歩: カバレッジ

たとえばカバレッジをSonarQubeに取り込みたいなら、 jacoco-maven-plugin 入れてやってください。

...
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.8</version>
                <executions>
                    <execution>
                        <id>default-prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <!-- verify -->
                        <id>default-report</id>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

prepare-agent がテストとか実行する前にJaCoCoのagentを設定するもの。 phase は指定しなくてもいい感じの場所( initialize )に入ります。

report がSonarQubeも取り込める形のJaCoCoのレポート(XML)を出力するものです。これ実行してあげないとSonarQubeのカバレッジは永久に0%と表示されます。

<execution><phase> を書かない場合、 jacoco:reportverify で実行されます。 先に挙げたコマンドだと package までしか実行してないので、JaCoCoのレポートは出力されません。

てことでこう。

./mvnw clean verify sonar:sonar -Dsonar.login=admin -Dsonar.password=hoge

<execution> 書かずに jacoco:report を手で実行してあげてもいいですけどね。

./mvnw clean package jacoco:report sonar:sonar -Dsonar.login=admin -Dsonar.password=hoge

分けるならこうでも。

./mvnw clean package
./mvnw jacoco:report
./mvnw sonar:sonar -Dsonar.login=admin -Dsonar.password=hoge

test でJaCoCoのファイル target/jacoco.exec が出来て、それを元に target/site/jacoco/* を作るのが jacoco:report です。 うまく取り込めてると sonar:sonar の時に以下のように Importing 1 report(s) が出ます。

[INFO] Sensor JaCoCo XML Report Importer [jacoco]
[INFO] 'sonar.coverage.jacoco.xmlReportPaths' is not defined. Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml
[INFO] Importing 1 report(s). Turn your logs in debug mode in order to see the exhaustive list.
[INFO] Sensor JaCoCo XML Report Importer [jacoco] (done) | time=7ms

ダメだったらこんな風に出るはず。

[INFO] Sensor JaCoCo XML Report Importer [jacoco]
[INFO] 'sonar.coverage.jacoco.xmlReportPaths' is not defined. Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml
[INFO] No report imported, no coverage information will be imported by JaCoCo XML Report Importer
[INFO] Sensor JaCoCo XML Report Importer [jacoco] (done) | time=1ms

少し前のSonarQubeは jacoco.exec を読んだのだけど、XML読むようにちょっと前に変わった。まぁ気持ちはわかる。

注意

  • SonarQubeのフッタに警告表示されてるけど、組み込みデータベース使ってるんでコンテナ落としたらデータ消えます。あくまで実験用。
  • SonarQubeのプロジェクトページ開いたら警告表示されてるけど、 sonar.password はそのうち使えなくなるらしいです。なのでここでは 9.9.1-community とバージョン固定してます。「これから使っていくためにとりあえず触る」目的なら lts とか latest とか使う方がいいと思うよ。

併せて読む必要がない

irof.hateblo.jp

JaCoCoで検索してもこの記事しかなかった。あんま書いてないのね私。SonarQubeは長年の付き合いなのに一記事も書いてなかった……。