Javaはカレントディレクトリを使わない

Java 11 から user.dir システムプロパティを実行時に変更することができなくなりました。この変更によって 相対パスを解決する開始点を実行時に変更している一部のアプリケーションが影響を受けます。

user.dir システムプロパティとカレントディレクトリの関係について説明します。

相対パスはカレントディレクトリを開始点として解決される?

Windows であれ Linux であれ 実行プロセスはそれぞれがカレントディレクトリ 作業ディレクトリ を持っています。そして 多くのプログラミング言語は 相対パスを解決するための開始点としてプロセスのカレントディレクトリ 作業ディレクトリ を使用します。OS 自体がカレントディレクトリを開始点として相対パスを解決するシステムコールを提供しているのでこれは合理的です。

たとえば Linux には open 関数というシステムコールがあり このシステムコールがカレントディレクトリを開始点とした相対パス解決の機能を持っています。そして C 言語の標準ライブラリに含まれる fopen 関数は与えられたパスを素直に open システムコールに渡すので 結果として C 言語の fopen 関数もカレントディレクトリを開始点として相対パスを解決していることになります。他の多くのプログラミング言語も同様です。最終的に呼び出される open システムコールに相対パスが素直に渡されれば 相対パスはカレントディレクトリを開始点として解決されます。

このような経緯から 相対パスはカレントディレクトリを基準に解決される と一般的に認識されています。

Java は独自のロジックで相対パスを解決している

しかし Java の場合は違います。Java OS のシステムコールに相対パスを渡しません。それよりも前の段階 Java のファイル API 独自のロジックによって相対パスから絶対パスへの解決がおこなわれ OS のシステムコールには解決された絶対パスが渡されるようになっています。

Java OS のシステムコールに頼らず 独自のロジックで相対パスから絶対パスへの解決をしています。この独自の相対パス解決ロジックで開始点として使われているのが user.dir システムプロパティです。

Java では相対パスの解決にカレントディレクトリは使用されません。

user.dir の既定値
JavaVM の起動時に user.dir システムプロパティを指定しなかった場合 user.dir システムプロパティの値は暗黙的にカレントディレクトリで初期化されます。多くの場合 user.dir システムプロパティの値はカレントディレクトリと一致しているため Java においても相対パスがカレントディレクトリを開始点として解決されているように見えます。

Java にはカレントディレクトリを変更する API が存在しない

Java にはカレントディレクトリを変更する API が存在しません。カレントディレクトリを変更する必要がないからです。それにもかかわらず カレントディレクトリを変更したいという質問は繰り返されてきました。

Java でカレントディレクトリを変更するにはどうすればいいですか?

回答も同じように繰り返されます。

  • なんのために カレントディレクトリを変更したいのですか?

  • JNI/JNA でカレントディレクトリを変更することはできるけど
    Java でそれをする意味はほとんどないぞ

  • もし 相対パス解決の開始点を変更したいのなら
    user.dir システムプロパティを変えればいいよ

だいたいはこれで質問者の抱えていた問題は解決します。

このように Java では相対パス解決の開始点を変更するために user.dir システムプロパティの値を実行時に書き換える という対処方法が知られていました。

Java 11 で破壊的な変更がおこなわれてしまった

Java 11 user.dir システムプロパティを書き換えることができなくなり それによって 実行時に相対パス解決の開始点を変更することもできなくなってしまいました。

厳密に言うと Java 11 でも user.dir システムプロパティの書き換え自体はできます。しかし JavaVM の起動時にファイル API 群が user.dir システムプロパティの値のコピーを保持するように変更されたため 実行時に user.dir システムプロパティを書き換えても 相対パス解決の開始点として反映されなくなりました。

Java 8 のときのソースコードは以下のようになっていました。Windows の場合

WinNTFileSystem.java
private String getUserPath() { /* For both compatibility and security, we must look this up every time */ return normalize(System.getProperty("user.dir")); }

getUserPath メソッドはパスを解決するために resolve メソッドから呼ばれます。これまでの実装ではメソッドは呼び出しの都度 user.dir システムプロパティを参照し直すため user.dir システムプロパティの書き換えは相対パス解決の開始点として随時反映されていました。

Java 11 のソースコードは以下のように変更されました。Windows の場合

WinNTFileSystem.java
private String getUserPath() { /* For both compatibility and security, we must look this up every time */ SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPropertyAccess("user.dir"); } return normalize(userDir); }

userDir フィールド変数を参照するように変更されています。この userDir フィールド変数は final となっており 初回に一度だけ user.dir システムプロパティを参照して その後は固定されたままです。

WinNTFileSystem.java
class WinNTFileSystem extends FileSystem { private final String userDir; public WinNTFileSystem() { Properties props = GetPropertyAction.privilegedGetProperties(); userDir = normalize(props.getProperty("user.dir")); }

こんな破壊的な変更がバグじゃないだって?

この変更に対して バグレポートがあがっていました。

報告者は Java 11 user.dir システムプロパティを変更しても相対パス解決に反映されなくなった ことを説明しています。

このバグレポートは冷たい回答とともにクローズされてしまいました。

System クラスの Javadoc にある注釈を読んでください。

標準のシステムプロパティを変更すると予測できない結果になるかもしれません。プロパティの値は初期化や初回使用時にキャッシュされます。その後にシステムプロパティを変更しても望んだ効果を得られないかもしれません。

これはバグではありません。VM 実行中に user.dir プロパティを決して変更すべきではありません。

Javadoc で注意喚起されていたのなら仕方がないか…。
そう思いつつ 念のために Javadoc に目を通してみました。

あれ? Java 10 までの Javadoc にはそんな記述はありません。Java 11 で動作が変更され それに合わせて Javadoc にも注意が追記されたようです。

このような下位互換性を損なう破壊的な変更をずいぶんと急にやったのですね。予期せぬ結果になる可能性があることを先に Javadoc に記載しておき その数バージョン先で実際の動作を変更するくらいでも良かったんじゃないかと思いました。

Java のリリースサイクルが短くなり 新機能の追加など Java の成長が早くなったことは喜ばしいことです。とても歓迎しています。ですが このような破壊的な変更はもうちょっと慎重にやってもらいたいです。

これは Java 11 LTS で直さなければいけないほど重要な問題だったのでしょうか。Java 11 LTS では Javadoc での予告にとどめ Java 12 以降での変更でもよかったのではないかと。

個人的には LTS バージョンには大きな変更 特に破壊的な変更 を入れて欲しくありません。Java 11 LTS Java 9, Java 10 の集大成であって欲しかった。Java 10 Java 11 の間で非互換が多くなると Java 11 へ移行できない かといって長期サポートのない Java 9 Java 10 にとどまることもできない それじゃあ Java 8 に戻るか ということになりかねません。

Java 12 Java 13 など LTS バージョンでなければ いろいろと新機能の投入や実験をしてもらってもいいと思います。Java 12 で入れてみた機能を Java 13 で撤回したっていい。そして Java 12 ~ Java 16 までの成果が Java 17 LTS としてまとまってくれたら嬉しいです。

ちなみに この user.dir の動作変更で Gradle が影響を受けたようです。ビルドツールは Makefile の時代から サブプロジェクトをビルドするために下位ディレクトリにもぐったりしますからね。Gradle も似たようなことをしていたのかもしれません。この問題については Gradle user.dir に依存しないように修正された模様です。

私も カレントディレクトリ変更の代替として user.dir を書き換えるのは良い方法ではないと思います。そもそもカレントディレクトリという概念が今の時代のプログラミングに合っていないのかもしれません。カレントディレクトリも user.dir もプロセス全体で 1 つしか持てないわけですから マルチスレッドアプリケーションとの相性は最悪です。各々のスレッドがカレントディレクトリや user.dir を都度変更するようなマルチスレッド アプリケーションは正しく動作しません。

これからは 相対パスをアプリケーション層でをきちんと解決することが求められることになるかもしれませんね。そうでなければ マルチスレッドに対応できないですから。

外部プロセスの起動は?

Java には ファイル API 群だけでなく 他にもカレントディレクトリが関連する箇所があります。たとえば 外部プロセス起動を担っている ProcessBuilder クラスなどですね。

ProcessBuilder で明示的に作業ディレクトリを指定しなかった場合 親プロセスのカレントディレクトリが子プロセスのカレントディレクトリになります。なぜか ProcessBuilder では user.dir ではなく本当にプロセスのカレントディレクトリが使われます。この辺り ファイル API 群との一貫性がないような気もしますね。

元々 ProcessBuilder user.dir を使っていないので Java 11 user.dir 変更禁止の影響を受けずに済みました。

この記事を共有しませんか?