JVMのオプションを各場面で指定する方法（特にmaven-surefire-plugin）

アプリケーションをJava 8から11にバージョンアップする際の作業で、Java仮想マシン（JVM）に `--illegal-access=warn` を設定して様子をみることがありました。JVMのオプションの設定方法が場面によって色々あったため、今後も使いそうな方法をメモしておきます。

試したバージョンは以下のとおりです。

* (OS: Ubuntu 18.04)
* Java: OpenJDK 11.0.18
* Tomcat: 8.5.89
* Maven: 3.9.1
* maven-surefire-plugin: 3.0.0

## TL;DR
今回調べてわかった主な方法です。~~一番上の方法に早く気付いていたら苦労しませんでした。~~

* 多くの場面で動作
	* 環境変数 `JDK_JAVA_OPTIONS` （Java9以降）または `JAVA_TOOL_OPTIONS` で指定する
* `java` コマンドで使う
	* コマンドライン引数で指定する ← これが基本
* Tomcatで使う
	* 環境変数 `JAVA_OPTS` または `CATALINA_OPTS` で指定する
* Mavenで使う
	* 環境変数 `MAVEN_OPTS` で指定する
	* ファイル ${maven.projectBasedir}/.mvn/jvm.config で指定する
* 単体テストで使う（maven-surefire-plugin）
	* パラメータ `argLine` で指定する
		* pom.xml 内でプラグインの `<configuration>` に書く
		* pom.xml 内で `<properties>` に書く
		* `mvn` のコマンドライン引数で `-DargLine='...'` と指定する
* JShellで使う
	* コマンドライン引数で `-R'...' -R'...'` と1つずつ指定する

## 設定方法の実験
オプションは簡単に効果を見れる `-showversion` で試します。

### `java` コマンド
Javaの実行に使うコマンドです。直接使うこともあれば、スクリプト等が間接的に使っていることもあります。

ソースコードが無いと寂しいので、簡単なものを用意します。

```java:HelloWorld.java
public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("Hello, world!");
	}
}
```

普通に実行すれば以下のようになります。

```shell-session
$ java HelloWorld.java
Hello, world!
```

---

コマンドライン引数にオプションを指定してみます。するとJavaのバージョンが表示され、オプションがきいていることがわかります。

```shell-session
$ java -showversion HelloWorld.java
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)
Hello, world!
```

コマンドライン引数を使わず、環境変数 `JDK_JAVA_OPTIONS` でも指定できます（Java9以降）。実行時には環境変数からオプションを拾ったことが注記されます。
[ツール・リファレンス # JDK_JAVA_OPTIONSランチャ環境変数の使用方法](https://docs.oracle.com/javase/jp/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE)

```shell-session
$ export JDK_JAVA_OPTIONS='-showversion'

$ java HelloWorld.java
NOTE: Picked up JDK_JAVA_OPTIONS: -showversion
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)
Hello, world!
```

Java9より前からある環境変数 `JAVA_TOOL_OPTIONS` も同様です。ただ、 `-showversion` はこの方法だと拒否されてしまいました。

```shell-session
$ unset JDK_JAVA_OPTIONS  # 上で試した設定を消去

$ JAVA_TOOL_OPTIONS='-showversion' java HelloWorld.java
Picked up JAVA_TOOL_OPTIONS: -showversion
Unrecognized option: -showversion
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

$ JAVA_TOOL_OPTIONS='--illegal-access=warn' java HelloWorld.java
Picked up JAVA_TOOL_OPTIONS: --illegal-access=warn
Hello, world!
```

参考：[javaコマンドで使える、JDK_JAVA_OPTIONS環境変数ってなんだ？ - CLOVER🍀](https://kazuhira-r.hatenablog.com/entry/2022/07/06/002410)

### Tomcat
Tomcatでも `JDK_JAVA_OPTIONS` は有効です。しかし、手元にあるアプリケーションをデプロイする際は環境変数 `JAVA_OPTS` でメモリ量などを指定しています。

オプションの指定方法はTomcatのスクリプトのコメントに色々書いてあります。基本的には `JAVA_OPTS` よりも `CATALINA_OPTS` のほうが良いみたいです。
https://github.com/apache/tomcat/blob/8.5.x/bin/catalina.sh

> `CATALINA_OPTS`
> (Optional) Java runtime options used when the "start", "run" or "debug" command is executed.
> Include here and not in JAVA_OPTS all options, that should only be used by Tomcat itself, not by the stop process, the version command etc.
> Examples are heap size, GC logging, JMX ports etc.
> 
> `JAVA_OPTS`
> (Optional) Java runtime options used when any command is executed.
> Include here and not in CATALINA_OPTS all options, that should be used by Tomcat and also by the stop process, the version command etc.
> Most options should go into CATALINA_OPTS.

実際にTomcatを起動させると以下のようになります。（ `JDK_JAVA_OPTIONS` はTomcatによって自動で追加されます）

```shell-session
$ export JAVA_OPTS='-showversion'

$ ./apache-tomcat-8.5.89/bin/startup.sh
Using CATALINA_BASE:   /home/user/apache-tomcat-8.5.89
Using CATALINA_HOME:   /home/user/apache-tomcat-8.5.89
Using CATALINA_TMPDIR: /home/user/apache-tomcat-8.5.89/temp
Using JRE_HOME:        /usr
Using CLASSPATH:       /home/user/apache-tomcat-8.5.89/bin/bootstrap.jar:/home/user/apache-tomcat-8.5.89/bin/tomcat-juli.jar
Using CATALINA_OPTS:   
Tomcat started.

$ less ./apache-tomcat-8.5.89/logs/catalina.out
NOTE: Picked up JDK_JAVA_OPTIONS:  --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)
...
```

### Maven
Mavenもやはり `JDK_JAVA_OPTIONS` は有効です。一方でTomcatで使えた `JAVA_OPTS` は効果が無く、また `mvn` コマンドの引数に `java` と同じように指定するとMavenのオプションとして認識されてしまいました。

```shell-session
$ JAVA_OPTS='-showversion' mvn -v
Apache Maven 3.9.1 (2e178502fcdbffc201671fb2537d0cb4b4cc58f8)
...

$ mvn -showversion
[ERROR] Error executing Maven.
[ERROR] The specified user settings file does not exist: /home/user/howversion
```

[公式ドキュメント](https://maven.apache.org/configure.html)によると、JVMのオプションを指定するには主に以下の方法があるようです。

* 環境変数 `MAVEN_OPTS` で指定する
* ファイル ${maven.projectBasedir}/.mvn/jvm.config に書いておく
  （→ [実行スクリプト](https://github.com/apache/maven/blob/maven-3.9.1/apache-maven/src/bin/mvn)内で `MAVEN_OPTS` に合成される）

`MAVEN_OPTS` だと確かに効果があります。

```shell-session
$ MAVEN_OPTS='-showversion' mvn -v
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)
Apache Maven 3.9.1 (2e178502fcdbffc201671fb2537d0cb4b4cc58f8)
...
```

### 単体テスト（maven-surefire-plugin）
単体テストのログを見ていたところ、 `MAVEN_OPTS` でオプションを有効にしてもテスト時には反映されていないことに気付きました。

単体テストに関わるmaven-surefire-pluginを調べると、forkしたJVMに `MAVEN_OPTS` の内容は引き継がないと書いてあります。JVMの設定はパラメータ `argLine` （または `debugForkedProcess` による追加）で行うようです。
[Maven Surefire Plugin &#x2013; surefire:test](https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html)

> `<argLine>`
> Arbitrary JVM options to set on the command line.
> 
> `<jvm>`
> Option to specify the jvm (or path to the java executable) to use with the forking options. For the default, the jvm will be a new instance of the same VM as the one used to run Maven. JVM settings are not inherited from MAVEN_OPTS.

---

設定方法でも少しハマりました。最初は pom.xml の `<configuration>` 内に `<argLine>` を書いたのですが、するとCIを回した際にJaCoCoのレポートが生成されなくなっていました。

```xml:pom.xml
...
		<plugin>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>3.0.0</version>
			<configuration>
				<argLine>--illegal-access=warn</argLine>
				<forkCount>1</forkCount>
				<reuseForks>false</reuseForks>
			</configuration>
		</plugin>
...
```

どうやらJaCoCoでもVMにオプションを設定していて、上の書き方ではそれを潰してしまっているようです。
[JaCoCo fails to generate reports if maven-surefire-plugin has the argline --illegal-access=permit · Issue #964 · jacoco/jacoco · GitHub](https://github.com/jacoco/jacoco/issues/964)

簡単な解決策として、 `<configuration>` 内でなく `<properties>` のほうで設定することにしました。

```xml:pom.xml
...
	<properties>
		...
		<argLine>--illegal-access=warn</argLine>
	</properties>
...
```

---

プロパティは `mvn` コマンドの引数に `-D` オプションでも渡せます。またMaven 3.9.0からは環境変数 `MAVEN_ARGS` が使えるということで、ここに `-D` を入れることも一応できます。

```shell-session
$ mvn test -DargLine='--illegal-access=warn'

$ MAVEN_ARGS='-DargLine=--illegal-access=warn' mvn test
```

### JShell
リフレクションの基礎を学ぶ際に使ったので、これもメモしておきます。

JShellで入力したコードの実行はリモートのプロセスで行っているようで、そちらのJVMにオプションを渡す必要があります。これは `jshell` コマンドのオプション `-R` によって指定できます。（これまでと異なり、JVMのオプション1つごとに指定します）
https://docs.oracle.com/javase/jp/11/tools/jshell.html

リモートの出力が見れないため、 `-showversion` や `--illegal-access=warn` などは効果がわかりにくいです。そのため今回はメモリ使用量を制限してOutOfMemoryErrorを起こしてみます。

```
$ jshell -R'-Xms4M' -R'-Xmx4M'
|  Welcome to JShell -- Version 11.0.18
|  For an introduction type: /help intro

jshell> "a".repeat(1_000_000).length()
|  Exception java.lang.OutOfMemoryError: Java heap space
|        at String.repeat (String.java:3161)
|        at (#1:1)
```

なお、環境変数 `JDK_JAVA_OPTIONS` はリモートにだけ、 `JAVA_TOOL_OPTIONS` はシェルとリモートの両方に効果がありました。シェル側のプロセスは `jshell` であって `java` が関わらないからでしょうか。

## おまけ
実際にillegal reflective accessが起きる単純なサンプルコードです。以下のissueと同じ問題に突き当たったことをきっかけに、ライブラリのソースコードから[要点](https://github.com/eclipse-ee4j/jersey/blob/2.39.1/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java#L546)を抜き出しました。
[Jersey 2.34 with HTTPS and PATCH method fails with error when run with JDK 16 · Issue #4825 · eclipse-ee4j/jersey · GitHub](https://github.com/eclipse-ee4j/jersey/issues/4825)

```java:SimpleReflection.java
public class SimpleReflection {
	public static void main(String[] args) throws Exception {
		java.net.HttpURLConnection.class.getDeclaredField("method").setAccessible(true);
	}
}
```

実行例は以下のとおりです。

```shell-session
$ java --illegal-access=debug SimpleReflection.java
WARNING: Illegal reflective access by SimpleReflection to field java.net.HttpURLConnection.method
	at SimpleReflection.main(SimpleReflection.java:3)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:404)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:179)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:119)
```