○クラスパスとモジュールパスの違い

前回は、Java 9で導入された新しいモジュールシステムについて、その概要や基本的な使用方法を解説した。そこでも触れたように、Java 9以降のJavaでは標準ライブラリやランタイムそのものが新しいモジュールシステムの仕組みの上で動作する。

それでは、java 8以前に作られたような、モジュールに対応していないプログラムやライブラリとの互換性はどうなっているのだろうか。今回はその辺りの仕組みについて解説したい。

従来(Java 8以前)のJavaアプリケーションでは、コンパイラやJVMがクラスやパッケージを見つける仕組みとしてクラスパスが利用されていた。Javaプログラムのコンパイル時や実行時は、クラスパス上にあるクラスやパッケージが走査されて読み込まれる。任意の場所にあるクラスやパッケージを利用できるようにしたい場合は、javac や java コマンドの実行時に -classpash オプションを使うことで、明示的に走査するクラスパスを指定できる。

Java 9から導入されたモジュールシステムでは、クラスパスに加えてモジュールパスという概念が追加された。モジュールパスはその名の通りモジュールを配置するパスで、コンパイラやJVMはモジュールパスをたどることで必要なモジュールを検索する。実行時に任意の場所をモジュールパスに追加したい場合には、--module-path オプションを使用する。

Java 9以降でもクラスパス自体は依然として有効だが、後述するように、クラスパス上におかれたクラスやパッケージも内部的にはモジュールシステム上で動作することになる。そのため、クラスパスは(Java 8以前の)既存のアプリケーションやライブラリとの互換性を保つためのもので、新規で開発するアプリケーションに関してはモジュールパスを使う前提で設計することが推奨される。

○名前付きモジュール/自動モジュール/無名モジュール

さて、Java 9以降のモジュールシステムでは、モジュールの種類を「名前付きモジュール」「自動モジュール」「無名モジュール」の3つに分類できる。モジュールシステムに対応していないライブラリなどを使用する場合は、この3種類のモジュールの違いについてよく知っておく必要がある。

○名前付きモジュール

Java 9で導入されたモジュールシステムに対応して定義されたモジュールは、自動的に名前付きモジュール(Named Module)になる。前回のように、module-info.java によってモジュール定義が行われたモジュールのことだ。標準ライブラリに含まれるモジュールもすべてこの名前付きモジュールになる。

○自動モジュール

module-info.java によるモジュール定義を持たない形式のJarファイルがモジュールパス上に配置された場合、自動的にモジュールとして扱われる。この形式のモジュールは自動モジュール(Automatic Module)と呼ばれる。この時、Jarファイル内で定義されたパッケージは自動的にすべてexportsされたものとして扱われる。また、モジュールグラフ(モジュール同士の依存関係を表した有向グラフ)に読み込まれたすべてのモジュールをrequiresしているものとして扱われる。

モジュール名については、もしManifestファイル(META-INF/MANIFEST.MF)にAutomatic-Module-Name属性が指定されている場合、その値がモジュール名になる。Manifestによる指定がない場合は、以下の命名規則によって決定される。

ファイル名から最後の「.jar」を除外する

「hoge-0.1.2」のようにファイル名の末尾がハイフン(-)に続けて数値とピリオド(.)の組み合わせになっている場合、ハイフン以降を除外する

数字でない文字をすべてドットに置き換える

例えば、「foo-bar.jar」のモジュール名は「foo.bar」に、「hoge-piyo-1.2.3.jar」のモジュール名は「hoge.piyo」になる。

○無名モジュール

モジュールパスではなくクラスパスに配置されたJarファイルのクラスやパッケージは、無名モジュール(Unnamed Module)と呼ばれるモジュールに所属するようになる。無名モジュールは、その名の通りモジュール名も持たない。したがって、ほかのモジュールからモジュール名を指定して明示的に依存関係を定義することができない。

代わりに、無名モジュール内のすべてのパッケージは自動的にすべてexportsされたものとして扱われる。また、モジュールグラフに読み込まれたすべてのモジュールをrequiresしているものとして扱われる。

○異なる種類のモジュール間の参照

新しいモジュールシステム上でモジュール定義がないクラスやパッケージが使えるということは、Java 8以前に作られたライブラリでもJava 9以降のシステムで使うことができることを意味している。ただし、自動モジュールや無名モジュールはrequiresやexportsといった明示的な宣言を持たないため、これらを混在させて使う場合はモジュール間の参照に制限がかかっている。下の図は、3種類のモジュール間の参照の可否をまとめたものだ。

異なる種類のモジュール間の参照可否

無名モジュールは、自動モジュールからは参照できるが、名前付きモジュールからは参照できない。また、名前付きモジュールから自動モジュールへは、通常の名前付きモジュール同士の場合と同様に、requires宣言されていれば参照できる。自動モジュールや無名モジュールからの名前付きモジュールに対する参照、および無名モジュールからの自動モジュールへの参照は、コンパイル/実行時 --add-modulres オプションによってモジュールグラフへの追加を行うことで可能になる。

実際に例を見てみよう。以下の例で使用する各Jarファイルは、それぞれ次表に示すようなモジュール/パッケージ定義になっている。

モジュール定義の記述方法や具体的なコンパイル/実行の仕方などは前回を参照していただきたい。

○名前付きモジュールから自動モジュールを参照

nomodule-greeting.jarはモジュール定義がないJarファイルだが、モジュールパスに含まれるようにすれば自動モジュールとして扱われる。この場合、モジュール名は「nomodule.greeting」になる。名前付きモジュールmodule-hello.jarからはnomodule.greetingをrequires宣言することで参照することができる。

名前付きモジュール→自動モジュール

$ java --module-path nomodule-greeting.jar:module-hello.jar --module jp.mynavi.hello/jp.mynavi.imajava.hello.Hello

Hello MYNAVI!

この仕組みを使えば、Java 8以前に作られた古いライブラリでも、モジュール対応のシステムで利用することが可能となる。

○名前付きモジュールから無名モジュールを参照

モジュール定義がないnomodule-greeting.jarを(モジュールパスではなく)クラスパスに含めた場合、それは無名モジュールとなる。無名モジュールは自動モジュールとは違って名前を持たず、requires宣言することができない。したがってmodule-hello.jarからは参照することができず、次のように例外が発生する。

$ java -classpath nomodule-greeting.jar --module-path module-hello.jar --module jp.mynavi.hello/jp.mynavi.imajava.hello.Hello

Error occurred during initialization of boot layer

java.lang.module.FindException: Module nomodule.greeting not found, required by jp.mynavi.hello

モジュール定義がないライブラリをモジュール対応したシステムで利用したい場合には、クラスパスではなくモジュールパスの通る場所に配置して自動モジュール扱いにしておく必要がある。

○自動モジュールから無名モジュールを参照

nomodule-hello.jarはモジュール定義がないJarファイルだが、モジュールパスに含めることで自動モジュールとなる。自動モジュールはモジュールツリー上のすべてのモジュールをrequires宣言している扱いになるため、次のように、相手が無名モジュールだとしても問題なく参照することができる。

$ java -classpath nomodule-greeting.jar --module-path nomodule-hello.jar --module nomodule.hello/jp.mynavi.imajava.hello.Hello

Hello MYNAVI!

○自動モジュールから名前付きモジュールを参照

nomodule-hello.jarを自動モジュールとして実行した場合、名前付きモジュールであるjp.mynavi.greetingを参照することが可能だろうか。次のように単に2つのJarファイルをモジュールパスに含めただけではエラーになってしまう。

$ java --module-path module-greeting.jar:nomodule-hello.jar --module nomodule.hello/jp.mynavi.imajava.hello.Hello

Exception in thread "main" java.lang.NoClassDefFoundError: jp/mynavi/imajava/greeting/Greeting

at nomodule.hello/jp.mynavi.imajava.hello.Hello.main(Hello.java:7)

Caused by: java.lang.ClassNotFoundException: jp.mynavi.imajava/greeting/Greeting

at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:602)

at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)

at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)

... 1 more

このケースでは、自動モジュールであるnomodule.helloがルート・モジュールとなっているが、このモジュールは依存関係の定義を持たないため、コアAPIのようなデフォルトでロードされるモジュールを除いて自動では追加されない。そこで、次の例のように --add-modules というオプションを使って参照したいモジュール(今回はjp.mynavi.greeting)をロードすることで参照できるようになる。

$ java --module-path module-greeting.jar:nomodule-hello.jar --add-modules jp.mynavi.greeting --module nomodule.hello/jp.mynavi.imajava.hello.Hello

Hello MYNAVI!

Java 8以前に作ったプログラムの中身を書き換えることなく、依存しているライブラリだけをJava 9以降のバージョンに置き換えたい場合などは、この方法を使って解決できる可能性がある。

○無名モジュールから名前付きモジュールを参照

無名モジュールから名前付きモジュールを参照したい場合も、上記と同様に --add-modules オプションで対象のモジュールを追加すればよい。

$ java -classpath nemodule-hello.jar --module-path module-greeting.jar --add-modules jp.mynavi.greeting jp.mynavi.imajava.hello.Hello

Hello MYNAVI!

○無名モジュールから自動モジュールを参照

無名モジュールから自動モジュールを参照したい場合も同様だが、追加するモジュール名が自動で付けられたものだという点だけ異なる。

$ java -classpath nomodule-hello.jar --module-path nomodule-greeting.jar --add-modules nomodule.greeting jp.mynavi.imajava.hello.Hello

Hello MYNAVI!

○--add-exports による、exportsされていないパッケージへの参照

名前付きモジュールでは、モジュール定義でexports宣言されていないパッケージは外部には公開されない。しかし、module-info.javaの宣言にかかわらず、コンパイル時や実行時にexports宣言を追加する方法がある。--add-exports オプションがそれだ。このオプションを使うことで、指定されたモジュールから、対象のモジュールの特定のパッケージに対する参照が許可される。

--add-exportsオプションの文法は次のようになっている。イコールの左側に参照を許可したいパッケージ名をモジュール名付きで、イコールの右側に参照する側のモジュール名を記述する。

--add-exports (参照される側の)モジュール名/パッケージ名=(参照する側の)モジュール名

注意していただきたいのは、このオプションはあくまでもJava 8以前のシステムとの互換性のために用意されているということだ。例えば、Java 8以前で開発されたシステムにおいて、依存するライブラリがJava 9以降のモジュール対応となり、一部のパッケージが非公開になったというようなシチュエーションが考えられる。

実行時にexports宣言を上書きすることができれば、本体のプログラムを書き換えることなくこの参照問題を解決することが可能となる。ただし、この方法は古いシステムからの移行のため以外での使用は推奨されていない。

それでは、実際の使用例を見ていこう。以下のサンプルでは、名前付きモジュールjp.mynavi.greetingにあるパッケージjp.mynavi.imajava.hiddenを、モジュール定義を持たないnomodule-hello-2.jarから参照しようとしている。

○自動モジュールから名前付きモジュールを参照

まずは、nomodule-hello-2.jarを自動モジュールとして実行してみる。先ほどと同様に、nomodule-hello-2.jarをモジュールパスに追加し、--add-modulesオプションでjp.mynavi.greetingをモジュールツリーに追加する。普通に実行すると、次のようにjp.mynavi.imajava.hidden.HiddenGreetingクラスへのアクセスを拒否されてしまう。

$ java --module-path nomodule-hello-2.jar:module-greeting.jar --add-modules jp.mynavi.greeting --module nomodule.hello/jp.mynavi.imajava.hello.Hello

Exception in thread "main" java.lang.IllegalAccessError: class jp.mynavi.imajava.hello.Hello (in module nomodule.hello) cannot access class jp.mynavi.imajava.hidden.HiddenGreeting (in module jp.mynavi.greeting) because module jp.mynavi.greeting does not export jp.mynavi.imajava.hidden to module nomodule.hello

at nomodule.hello@2/jp.mynavi.imajava.hello.Hello.main(Hello.java:7)

この場合、次のように--add-exportsオプションを使って実行すればよい。この例では、nomodule.helloモジュールからjp.mynavi.imajava.hiddenパッケージへの参照を許可している。

$ java --module-path nomodule-hello-2.jar:module-greeting.jar --add-modules jp.mynavi.greeting --add-exports jp.mynavi.greeting/jp.mynavi.imajava.hidden=nomodule.hello --module nomodule.hello/jp.mynavi.imajava.hello.Hello

(Hidden) Hello MYNAVI!

○無名モジュールから名前付きモジュールを参照

次に、nomodule-hello-2.jarを無名モジュールとして実行する場合を考える。今回は無名モジュールなので--add-exportsでモジュール名を指定することができない。このようなケースでは、次のように参照を許可するモジュールの部分に「ALL-UNNAMED」と指定すれば、すべての無名モジュールに対してexports宣言が適用されることになる。

$ java -classpath nomodule-hello-2.jar --module-path module-greeting.jar --add-modules jp.mynavi.greeting --add-exports jp.mynavi.greeting/jp.mynavi.imajava.hidden=ALL-UNNAMED jp.mynavi.imajava.hello.Hello

(Hidden) Hello MYNAVI!

○まとめ

Javaの新しいモジュールシステムは、普通に使う分にはそれほど難しいものではないが、Java 8以前の古いライブラリとの互換性を考慮すると、途端に複雑さが増してしまう。とはいえ、Java 8以前のシステムを最新の環境に移行したい場合には、モジュールの互換性問題は避けて通ることはできない。新しいモジュールシステムの特性を理解した上で、適切に使いこなしていただきたい。