背景 (自分語りなのでとばしてもいいです)
筆者は普段はPython、C++、Rust、Goを主に書いていて機械学習、数理最適化関連のアプリケーションやWebAPIの開発をしています。機械学習はPython、数理最適化はC++ or Rust、WebAPIはPython or Goというような使い分けをしています。今の会社のプロダクトの内、あまり機械学習と関係ないのにPythonで書かれているものがあり、しかもMyPyが導入されていなく、型hintも適当であまりにも可読性が低いものがあって改修しようと考えています。その際、せっかくなので今後外注もしやすいようにJavaで書き直すことも検討し、気持ちよく開発できる環境をいろいろと試行錯誤しました。
なぜJavaなのか? (自分語りPart 2)
本当はRustで書きたいし、社内の自分の部門はこの用途でRustメインで書こうと決めましたが、外部に今後の保守を外注することも考えるとJavaになりました。Kotlinも考えましたが、外注先がJavaしか書けない場合もありますし、何よりVSCodeにはMicrosoft社、あるいはJetBrains社によるKotlinの公式の拡張が無く、個人の方が開発している1という状況です。このKotlinの拡張の出来自体はすばらしいのですが、開発者の方は初めはKotlinを使用しようと思ってこの拡張を作ったが、結局Kotlinは使用しなくなったのでメンテするのが大変で誰かに手伝ってほしいという状態で今後の不安が残ります。Kotlinを作っているJetbrains社自体がIntellijやFleetなどのIDEを開発していて競合のVSCode向けに作る気は全く無いと思うのであまり期待はできません。したがって、Microsoftが公式で拡張を開発しているJavaの方がVSCodeで書く上ではよさそうです。Javaの方も近年ではScalaやKotlinの様々ないいところを取り入れて、唯一Javaの言語仕様でサポートされていないNonNull/Nullableの対処のみ何とかすればKotlinじゃなくてもいいと考えました。
考慮した結果の環境
- Builder: Gradle or Maven
- NonNull:
org.springframework.lang.NonNullApi
- Linter: Checkstyle (Google Style) +
SonarLint - Formatter: google-java-format (+ spotless)
- Test Framework: JUnit5 + JaCoCo
- 利用するVSCodeの拡張
vscjava.vscode-java-pack
vscjava.vscode-gradle
nicolasvuillamy.vscode-groovy-lint
shengchen.vscode-checkstyle
sonarsource.sonarlint-vscode
ilkka.google-java-format
ryanluker.vscode-coverage-gutters
以下説明をしていきます。
NonNull/Nullableの対応
JavaにはAnnotationを利用して引数や戻り値がNonNullなのかNullableなのかをチェックする仕組みがあります。このNull関連のAnnotationがまたたくさんあり、どれがいいのかを議論した記事などもたくさんありますがここでは触れません。私が欲しいのはKotlinのようにデフォルトでNonNullを強制し、必要な場合だけNullableを許可するような機能です。この条件で探したところSpring Frameworkの中にあるNonNullApi
というannotationが見つかりました。詳細は以下の記事を参照してください。
使う際にはpackageルートにpackage-info.java
というファイルを作成し、中に
@org.springframework.lang.NonNullApi
package sample;
の様に書くだけです。これでこのパッケージ全体でNonNullがデフォルトの挙動になり、必要な場合に@NullAble
を付けるかOptional
でラッピングするなどの対応をすればよくなります。
Intellijではこのannotationもデフォルトでチェックしてくれますが、VSCodeではそれができなく、SonarLintを試したらやってくれたのでsonarsource.sonarlint-vscode
が必須の拡張になります。
追記[2022/11/03]
VSCodeのJava拡張(redhat.java
)のv1.11.0からAnnotationを利用したNullチェックが利用可能になりました。これによりSonarLintが不要になりました。ただしorg.springframework.lang.NonNullApi
だとちゃんと動いてくれなく、同様の機能を有するorg.eclipse.jdt.annotation.NonNullByDefault
にすると意図した挙動になります。また、この機能を正しく利用するには.vscode/settings.json
のjava.settings.url
で設定ファイルを例えば.vscode/java_settings.prefs
のように指定し、その中に以下のように記載します。
org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.Nonnull
org.eclipse.jdt.core.compiler.annotation.nonnull.secondary=
org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
org.eclipse.jdt.core.compiler.annotation.nullable=jorg.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
この部分についてはドキュメントには書かれておらず、以下のスレッドから推測した挙動です。
https://github.com/redhat-developer/vscode-java/issues/1693
Formatter
Formatterが結構曲者でした。google-java-formatを利用したいのですが、VSCodeの公式サイトはじめ、9割くらいの記事はsettings.jsonに
"java.format.settings.url": "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml"
を追加するという書き方になっていますが、このファイルはもう何年も更新されていなく、後述のCheckStyleのGoogle Styleでlintすると大量に警告が出てきます。実はgoogle-java-formatの挙動はエディタの標準機能では設定できない項目も多く、単純に設定をするのではなくてGoogleが公式にIntellijやEclipse向けに拡張を開発しています。そして残念ながらVSCode向けにはありません。これはgoogle-java-formatがJavaで開発されており、IntellijやEclipseにはそのまま組み込めるけどElectronを利用してTypescriptで書かれているVSCodeにはそのまま組み込めないためだと思います。
幸いなことに3rd partyの拡張が公開されていて、これは単純に外部コマンドでgoogle-java-formatをたたいているだけです。
どうもMacはhomebrewで google-java-format
というコマンドが導入できるそうですが、WindowsやLinuxの場合はjavaコマンドでjarファイルを実行するスクリプトを書く必要があるようです。どうせwrapperスクリプトを書くならもういっそのこと実行ファイル化しようということでGraalVM Native Imageで事前にgoogle-java-formatを実行ファイルにし、それをセットして利用しています。こうすることでJVMの実行時の1秒弱の時間も節約できてきびきび動きます。
"google-java-format.executable-path": "/usr/local/bin/google-java-format-1.15.0-linux-x86_64",
GraalVMで変換するときにまたいろいろとオプションを追加しないといけなかったのですが、今回はその話は省略します。以下のレポジトリにWindowsとLinux向けのビルドスクリプトを上げてあります。Releaseページには実際に作った1.15.0の実行ファイルもおいてあります。
.gradle
ファイルのフォーマットはnicolasvuillamy.vscode-groovy-lint
を利用しています。
Linter
Checkstyleを利用します。VSCodeからはshengchen.vscode-checkstyle
という拡張で利用できます。私はgoogle_checks-10.3.1.xml
というファイルをGoogleStyleの公式レポジトリからダウンロードしておきJavadocが無い時に出る警告だけコメントアウトしています。
また、前述した通り@NonNullApi
をチェックするためにsonarsource.sonarlint-vscode
もインストールします。
Test Framework
JUnitとTestNGが二大巨頭のようですが、詳しくないのでとりあえずJUnit5を選択しました。TestNGの方がいいという場合はコメントなどで教えてください。そしてカバレッジを取得するためにJaCoCoを利用します。Gradleの設定は基本的に公式ページに書いてある通りです。
test {
finalizedBy jacocoTestReport // report is always generated after tests run
}
jacoco {
toolVersion = "0.8.7"
}
jacocoTestReport {
dependsOn test // tests are required to run before generating the report
reports {
html.required = true
csv.required = false
xml.required = true
}
}
そしてこのカバレッジをVSCodeで表示しているソースコード上に重ねるのにryanluker.vscode-coverage-gutters
を使用します。Gradle上でJaCoCoを実行した際にxmlはデフォルトでjacocoTestReport.xml
という名前で出力されますが、これはCoverage Guttersがデフォルトで監視しているファイルには含まれていないのでsettings.json内に追加してあげます。
"coverage-gutters.coverageFileNames": [
"cov.xml",
"coverage.xml",
"jacoco.xml",
"coverage.cobertura.xml",
"jacocoTestReport.xml"
]
まとめ
上記の設定はまとめてGitHubにTemplate Projectとして公開しています。
https://github.com/lucidfrontier45/JavaTemplate
他にもおすすめの設定等ありましたら是非教えてください。