Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

--add-exportsをMaven/Gradleで使う

--add-exports なんてオプションは使わないに越したことはないのですが、依存先ライブラリの都合でどうしても必要という私のような人のためのメモ。

ポイントは javac だけでなく javadoc ないし java (テスト実行)コマンドに対するオプション提供も必要という点です。Gradle用のサンプルプロジェクトMaven用のサンプルプロジェクトもあります。

コンパイラ用の設定

注意点として、--add-exports オプションは --release オプションと同時に使えません

Maven3

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.9.0</version>
        <configuration>
          <source>11</source>
          <target>11</target>
          <encoding>${project.build.sourceEncoding}</encoding>
          <forceJavacCompilerUse>true</forceJavacCompilerUse>
          <showWarnings>true</showWarnings>
          <compilerArgs>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
          </compilerArgs>

Gradle

// build.gradle.kts
val exportsArgs = listOf(
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
)

tasks.withType<JavaCompile>() {
    sourceCompatibility = "11"
    targetCompatibility = "11"
    options.compilerArgs.addAll(exportsArgs)
}

javadoc用の設定

いろいろとオプションを指定する手法が提供されていますが、以下のように<additionalOptions>を使う方法での動作を確認しました。内部的にはOptionファイルを経由してjavadocコマンドのオプションを設定しています。

Maven

  <build>
    <plugins>
      <plugin>
      <artifactId>maven-javadoc-plugin</artifactId>
      <version>3.3.1</version>
      <configuration>
        <additionalOptions>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
        </additionalOptions>
      </configuration>
    </plugin>

Gradle

一気にややこしくなります。 options.addStringOption()では動作しないので、Maven同様にOptionファイルを経由しての指定が望ましいです。

// build.gradle.kts
val addExportsFile = file("$buildDir/tmp/javadoc/add-exports.txt")
val createJavadocOptionFile by tasks.registering {
    outputs.file(addExportsFile)
    doLast {
        addExportsFile.printWriter().use { writer ->
            exportsArgs.chunked(2).forEach {
                writer.println("${it[0]}=${it[1]}")
            }
        }
    }
}
tasks {
    withType<Javadoc> {
        dependsOn(createJavadocOptionFile)
        options.optionFiles(addExportsFile)
    }
}

テスト実行

Maven

surefireプラグインJVMをforkするなら、forkするJVM側にオプションを設定します。そうではない場合、以下のような .mvn/jvm.config ファイルでMavenを実行するJVM自体に設定を行います。

--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED

Gradle

jvmArgs プロパティを経由して設定します。このプロパティは標準でnullなので、ビルドスクリプトの他の箇所で設定していないなら .addAll() ではなく = を使って設定します。

// build.gradle.kts
tasks {
    test {
        useJUnit()
        jvmArgs = exportsArgs
    }

JJUG CCC 2021 FallでLT参加してきました

JJUG CCC 2021 FallにてJSR305に代わる静的解析用標準アノテーションの策定を目指す活動について紹介しました。スライドは以下からご覧いただけます:

speakerdeck.com

2021年11月24日更新:動画はJJUG公式から公開いただいています:

www.youtube.com

以下、単に感想です。

静的解析用アノテーションが多数存在する件

この17年間で生まれた静的解析アノテーション、数はかなりあるんですが歴史等まとまっているものではないので、Maven Centralにデプロイされた日をベースに整理していました。スライド草案に記載した歴史は以下のように非常に長いものになりました:

f:id:eller:20211121203045p:plain
関連アノテーションの歴史

地味に困ったのがJSR305に関する意思決定がJCPのサイトで公開されていないことです。

JCPからリンクされているGoogle Groupを見ても「なぜ活動が止まったか」はわかりません。ここでの活動自体が非活発になったので。近年のJEPがチケットやメーリングリストで管理され透明性が高いのは、こうした反省を踏まえたものなのかもとか思いました。

わかりやすい動画を作成したい

私の動画はmacOSのスクリーンキャプチャ機能で録画しただけのものです。解像度を変えるためにQuickResを購入しましたが、ほかはマイクのようなハードウェア含め工夫をしていません。

が、今回みなさんの動画を見ていると、画面隅に顔を映している方がいらして、しかもその方が気持ちわかりやすかったりするんですよね。デジタル表現の工夫や技法は重要になっていますし、きちんと学びたいなと思いました。まぁ動画編集の時間が作れるかは別の問題ですけど……(今回も体調不良で急遽幼稚園を休んだ子供の面倒を見ながらの録画だった)

JJUG CCCでの過去発表へのリンク

GitPodでJavaプロジェクトを開発する

GitHub Codespacesがなかなか個人向けに来ないので、changelog.comで宣伝していたGitPodを試しています。 どうも公式のJava向けの説明が古いようで、既にDeprecatedになっているtheiaを前提としているため、調べたことをメモしておきます。

最新のJavaを使う

普通に gitpod/workspace-full イメージ内でJavaを起動すると、Zuluの11が使われていることがわかります:

$ java --version
openjdk 11.0.12 2021-07-20 LTS
OpenJDK Runtime Environment Zulu11.50+19-CA (build 11.0.12+7-LTS)
OpenJDK 64-Bit Server VM Zulu11.50+19-CA (build 11.0.12+7-LTS, mixed mode)

SDKMANやhomebrewが入っているので好きなバージョンを入れてもいいですが、ワークスペースを立ち上げる度に実行するのは面倒なので、 azul/zulu-openjdk のようなイメージを使ってしまうのが楽でいいと思います。

# .gitpod.yml
image: azul/zulu-openjdk:16

Mavenの依存をダウンロードしておく

Mavenの場合、dependency:go-offlineプラグインや依存をすべてダウンロードできます。 これをイメージ作成時に実行しておくのが良さそうです。

# .gitpod.yml
image: azul/zulu-openjdk:16

tasks:
  - init: ./mvnw -B dependency:go-offline

Gradleの依存をダウンロードしておく

Gradleには標準的な手法がないので、単にビルドを回しておきます。

# .gitpod.yml
image: azul/zulu-openjdk:16

tasks:
  - init: ./gradlew build

Extensionを導入する

Java向けExtensionを3つ入れて様子を見ています。

# .gitpod.yml
vscode:
  extensions:
    - redhat.java
    - vscjava.vscode-java-dependency
    - vscjava.vscode-java-debug

ポートを開けておく

Spring Framework標準の8080ポートを開けておく場合は ports の設定 で足ります。 が、URLの取得にgpコマンドが必要なので azul/zulu-openjdk ではなく gitpod/workspace-full をベースとしたイメージを用意する必要があります。URLの決定ロジックがいまのところ非常に単純なのでなくてもなんとかなりそうではありますが、一応。

# .gitpod.yml
image:
  file: .gitpod.Dockerfile
ports:
  - port: 8080
# .gitpod.Dockerfile
FROM gitpod/workspace-full
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && sdk install java 16.0.2-zulu"

バッジを付ける

バッジはDiscourseに落ちています。 README.md とかに貼っておくと、Contributorの敷居が下がって良いんじゃないでしょうか。

community.gitpod.io

MRJAR (Multi-Release JAR)を使っているOSS一覧

参考

SLF4JとLogbackは2021年現在では積極採用しない方が良い(2023年12月 追記)

SLF4JとLogbackの中の人はここ数年活発ではないのでLog4j2などを代わりに使いましょう。

SLF4Jの活動は最近活発ではない

SLF4JはVCSとしてGitHubを利用しています。最後の変更が2020年2月最後のリリースが2019年12月となっていることからも、あまり活発ではないことが伺えます。

またBTSとしてJIRAを使っていますが、こちらもメンテナンスされていません。昨夏SLF4J-209が既にクローズ可能な状態であることSLF4J-186が修正可能であることなどをコメントしましたが、1年近く経った今もすべて返信がない状態です。

2020年12月にイシューを閉じていたりするので全く動きがないわけではないのですが、年間で22つ作成されたのに対して2つしか閉じられていないので、充分にメンテされているとは言い難い状況です。

2021年5月31日時点での過去360日のイシュー消化状況

2021年6月2日追記:Java9リリース以降期待されていたJigsaw対応が入っている2.0(元々1.8だったもの)の安定版がずっと来ていないことも課題です。初回リリースである 1.8.0-alpha0 が2017年04月だったので、4年は経過しています。最新の 2.0.0-alpha1 は2019年10月のリリースで、未だα版ですが、モジュールを利用する場合は1.7は使えない(複数のモジュールが同一パッケージを提供できない)ため使わざるを得ない状況にあります。

メンテナーを増やすつもりはない?

2020年8月14日に ceki@qos.ch にメールを送り、メンテナーを増やすつもりはあるか、あるなら自分はこういった貢献ができますという意思を伝えたのですが、こちらもまだ返信がありません。

少数のメンバーがリリース権限を握っており、かつプロジェクトリーダーが連絡を取れない状態……と考えると、かつてのFindBugsプロジェクトの最後に非常に近い状況にある気がしています:

blog.kengo-toda.jp

Log4j 2が活発に活動しているのが幸い

ではSLF4JやLogbackに頼れないとして、どういった移行先があるのか。ロギングファサードないしロギングライブラリとしては、以下のような選択肢があります:

個人的にありがたいのはLog4j 2が活発なことです。Logbackを採用する理由として実行性能があったと記憶していますが、Log4j 2はLogbackと比べても高い性能を示しているとされています。lazy loggingflow tracingなども備えています。あと地味にLoggerインスタンス作るのが楽です:

// SLF4J
// http://www.slf4j.org/faq.html#declaration_pattern
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

// Log4j 2
// https://logging.apache.org/log4j/2.x/manual/api.html#Logger_Names
private static final Logger logger = LogManager.getLogger();

ログライブラリはプロジェクトによって要件も異なるでしょうし、必ずしもLog4j2が最適な選択肢とは限りませんが、一度試してみても良いのではないかと思います。

2021年6月2日追記:既にあるプロジェクトのSLF4JやLogback依存を今すぐに切らなければならない状況ではないと考えています。これから始めるプロジェクトでは積極採用するべきではないと思いますが、そうではない場合、特にモジュールを利用する予定がない場合(Java 8利用中など)はそのまま使い続ければいいかと思います。新規プロジェクトでモジュールを利用する場合は、更新頻度の低いα版に依存するリスクを受容できるのかどうか検討する必要があるでしょう。

2021年12月11日追記:CVE-2021-44228の1件でLogbackが注目されているようなので、ここ半年の状況を整理します。

まずSLF4Jが更新されていなかったという点。6月から8月にかけていくつか更新が入り、ResolvedがCreatedを上回った時期がありました。

2020年12月〜2021年12月におけるチケットの遷移(SLF4J)

Logbackも同様で、6月から8月にかけて更新が入っています。

2020年12月〜2021年12月におけるチケットの遷移(Logback)

ちょうど6月にEclipse FoundationからSLF4Jがabandoned projectなんじゃないかと言われているのでこの集中的な更新との関係性を疑いましたが、関係がないそうです。個人がリードするプロジェクトなので更新にムラがあるのは理解できます。

ちょっと信じがたい脆弱性を作り込んだもののセキュリティポリシーが明確で有事の対応が期待できるlog4j2と、非活発だがログに理解の深い開発者が見ているLogback。どちらを選ぶのかは組織のポリシーによって変わってきそうです。

2023年12月15日追記: 2023年末時点での情報に関する記事を書きました。

blog.kengo-toda.jp

JJUG CCC 2021 Springでバイトコードの話をしました

ということでJava8〜16におけるバイトコード生成の変化について、先日開催されたJJUG CCCで喋ってきました。 動画をYoutubeにて配信していただいているので、よろしければご覧ください:

youtu.be

資料はSpeakerdeckにあります。ハイパーリンクを埋めているところは、PDFを落としてもらえれば追えるはずです:

speakerdeck.com

マイクロベンチマークGitHubに置いてあります。みんなも手元でレッツJMHだ:

github.com

なお最後の方に触れたJEP396については、掘り下げたセッションがあったようです:

youtu.be

運営の皆様、いつも素敵なイベントを開催いただきありがとうございます!

JavaのRecordでは配列を使わないほうが良いという話

配列使うとmutableになるから使うべきではない、というのに加えて。生成される hashCode()equals(Object), toString() が配列を考慮しない実装になっているため、JavaのRecordでは配列を使わないほうが良いようです。

検証コード

生成される hashCode()equals(Object) の挙動を検証するコードです。

gist.github.com

なぜこうなるのか

ObjectMethods クラスによって生成されるコードを見ると、 Arrays.hashCode(int[]) ではなくint[].hashCode()が、Arrays.equals(int[], int[]) ではなく int[].equals(Object) が使われているようです。これは考慮漏れというわけではなく、ObjectMethodsクラスのjavadocに明記されている仕様です。core-libs-devメーリスに質問を投げて確認しました

実際の実装はObjectMethodsクラスのコードを読むか、以下の記事を参照ください。

alidg.me

望ましい対策

Recordでは配列の代わりに不変コレクションを利用するのが望ましいと思われます。Java 9からimmutable listを作るfactory methodが導入されています。また不変であることを型として示したいならば、GuavaのImmutableListを使う手もあります。

Recordクラスがmutableになるリスクを受容した上で配列を使う場合は、hashCode()equals(Object), toString() を自動生成に頼るのではなくハードコードすることで、この問題を回避可能です。大抵のIDEにはこれらのコードを自動的に生成する機能が備わっているので、利用すると良いでしょう。