Java 9 の Project Jigsaw を勉強したときのメモ。
環境
OS
Windows 10
Java
>java --version
java 9
Java(TM) SE Runtime Environment (build 9+181)
Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)
Hello World
フォルダ構成
|-main/
| |-class/
| `-src/
| |-module-info.java
| `-sample/main/
| `-Main.java
|
`-sub/
|-class/
`-src/
|-module-info.java
`-sample/sub/
|-api/
| `-Foo.java
|-internal/
| `-FooImpl.java
|
`-FooFactory.java
ソースコード
package sample.sub.api;
public interface Foo {
void hello();
}
package sample.sub.internal;
import sample.sub.api.Foo;
public class FooImpl implements Foo {
@Override
public void hello() {
System.out.println("FooImpl.hello()");
}
}
package sample.sub;
import sample.sub.api.Foo;
import sample.sub.internal.FooImpl;
public class FooFactory {
public static Foo newInstance() {
return new FooImpl();
}
}
module sample_module.sub {
exports sample.sub;
exports sample.sub.api;
}
package sample.main;
import sample.sub.FooFactory;
import sample.sub.api.Foo;
public class Main {
public static void main(String... args) {
Foo foo = FooFactory.newInstance();
foo.hello();
}
}
module sample_module.main {
requires sample_module.sub;
}
コンパイル
# sub モジュールをコンパイルする
> javac -d sub\class sub\src\module-info.java sub\src\sample\sub\FooFactory.java sub\src\sample\sub\api\Foo.java sub\src\sample\sub\internal\FooImpl.java
# main モジュールをコンパイルする
> javac -p sub\class -d main\class main\src\module-info.java main\src\sample\main\Main.java
実行する
> java -p main\class;sub\class -m sample_module.main/sample.main.Main
FooImpl.hello()
説明
依存される側のモジュール宣言
module sample_module.sub {
exports sample.sub;
exports sample.sub.api;
}
- モジュールの宣言は、ルートパッケージに置く
module-info.java
という専用のファイルに記述する- 名前は
module-info.java
固定
- 名前は
- モジュール宣言は、
module <モジュール名> {<モジュール定義>}
という形式で記述する - モジュール名はパッケージ名と同じようにドット
.
区切りで記述できる- 通常はパッケージ名と同じような名前になるようにつけるようだが、それだとサンプル実装上パッケージ名とモジュール名が区別しづらいので、ここではあえて
sample_module.sub
とパッケージとは異なる名前を付けている
- 通常はパッケージ名と同じような名前になるようにつけるようだが、それだとサンプル実装上パッケージ名とモジュール名が区別しづらいので、ここではあえて
-
{}
の中ではこのモジュールに関する定義を記述できる -
exports
は、外部に公開するパッケージを指定する - ここでは
sample.sub
とsample.sub.api
の2パッケージを外部に公開している-
sample.sub.internal
は内部 API のつもりなので、公開しないようにしている
-
参照する側のモジュール宣言
module sample_module.main {
requires sample_module.sub;
}
- 他のモジュールを参照する側も、参照される側と同様に
module-info.java
を使ってモジュール宣言を記述する -
requries
で、このモジュールが依存するモジュールを定義する - ここでは、さきほどの
sample_module.sub
モジュールを参照することを定義している - メインクラスのあるパッケージ(
sample.main
)をexports
しないと起動できない、という説明を見たが、実際やってみるとexports
していなくても動いた- 途中で仕様が変わった?
コンパイル時のモジュール指定
# sub モジュールのコンパイル
> javac -d sub\class sub\src\module-info.java sub\src\sample\sub\FooFactory.java sub\src\sample\sub\api\Foo.java sub\src\sample\sub\internal\FooImpl.java
# main モジュールのコンパイル
> javac -p sub\class -d main\class main\src\module-info.java main\src\sample\main\Main.java
- コンパイルはこれまで通り
javac
コマンドを使用する - 依存するモジュールが存在しない場合は、従来通りの指定でコンパイルできる(sub モジュールのコンパイル)
-
module-info.java
をコンパイル対象のソースに含めることを忘れないように!
-
- 依存するモジュールが存在する場合は、
-p
(もしくは--module-path
)オプションで依存するモジュールが存在するフォルダのパスを指定する- 古い記事だと、このオプションが
-mp
で紹介されているものがあるが、どうやら変わったもよう
- 古い記事だと、このオプションが
起動時のモジュール指定とメインクラスの指定
> java -p main\class;sub\class -m sample_module.main/sample.main.Main
- 起動時も、
-p
(--module-path
)オプションで使用するモジュールの場所を指定する- パスが複数ある場合は、 Windows ならセミコロン
;
、 Linux などならコロン:
で連結して指定する
- パスが複数ある場合は、 Windows ならセミコロン
- また、メインクラスの指定は
-m
(--module
)オプションで指定するように変わった(モジュールを使用している場合) - メインクラス名の指定も変わっていて、
<module名>/<メインクラス名>
と指定する- ここでは、
sample_module.main
モジュールのsample.main.Main
クラスを指定するため、sample_module.main/sample.main.Main
としている
- ここでは、
exports していないパッケージを参照した場合の挙動
package sample.main;
import sample.sub.FooFactory;
import sample.sub.api.Foo;
import sample.sub.internal.FooImpl;
public class Main {
public static void main(String... args) {
FooImpl foo = (FooImpl)FooFactory.newInstance();
foo.hello();
}
}
> javac -p sub\class -d main\class main\src\module-info.java main\src\sample\main\Main.java
main\src\sample\main\Main.java:5: エラー: パッケージsample.sub.internalは表示不可です
import sample.sub.internal.FooImpl;
^
(パッケージsample.sub.internalはモジュールsample_module.subで宣言されていますが、エクスポートされていません)
エラー1個
こんな感じになって、 exports
していないパッケージは参照できない。
jar にパッケージして利用する
Hello World のプログラムを、 main, sub ともに jar にパッケージして使ってみる。
javac
でコンパイルするところまでは同じ。
jar にパッケージする
# sub モジュールの jar を作成
> jar -c -f sub\jar\sub-module.jar -C sub\class .
# main モジュールの jar を作成
> jar -c -f main\jar\main-module.jar -C main\class .
- モジュールの jar ファイルを作成するには、これまで通り
jar
コマンドを使用する -
-c
は jar を作成するという意味のコマンド(--create
の短縮形) -
-f <jar の出力先パス>
で、出力先を指定する -
-C <パッケージするディレクトリ> .
で、指定したディレクトリを jar にパッケージする
jar のモジュール情報を参照する
# sub モジュールの情報を出力
> jar -d -f sub\jar\sub-module.jar
sample_module.sub jar:file:///.../sub/jar/sub-module.jar/!module-info.class
exports sample.sub
exports sample.sub.api
requires java.base mandated
contains sample.sub.internal
# main モジュールの情報を出力
> jar -d -f main\jar\main-module.jar
sample_module.main jar:file:///.../main/jar/main-module.jar/!module-info.class
requires java.base mandated
requires sample_module.sub
contains sample.main
-
-d
(--describe-module
) で、 jar のモジュール情報を出力できる -
exports
は公開されているパッケージ -
requres
は依存しているモジュール-
java.base
はjava.lang
パッケージとかが含まれるモジュールで、指定していなくても必ずついてくる
-
-
contains
は、おそらく非公開のパッケージ
実行する
> java -p sub\jar;main\jar -m sample_module.main/sample.main.Main
FooImpl.hello()
-
-p
で、モジュール(jar
)のあるフォルダを指定すれば、そこにある jar を読み込んでくれるっぽい- jar ファイルまでのパスを直接しても良い
モジュールの種類
モジュールには、大きく次の3つの種類がある。
- named module
- automatic module
- unnamed module
named module
モジュールパス(--module-path
)で読み込まれた定義情報(module-info.class
)を持つモジュール。
単にモジュールと言ったら、これのことを指すことが多い。
automatic module
モジュールパスで読み込まれた定義情報の無い jar。
全てのパッケージが exports
されている扱いになる。
また、モジュール名は jar ファイル名から自動的に補完される。
Java 9 より前に作成された jar ファイルを --module-path
で指定した場合などがこれになる。
明確な定義は見つけられていないが、ファイル名からモジュール名が補完されたりするので、たぶん jar ファイルでないといけないっぽい。
Javadoc にも以下のような記述があるので、最低限 jar である必要がある気がする。
A JAR file that does not have a module-info.class in its top-level directory defines an automatic module
(訳)
トップレベルにmodule-info.class
を持たない Jar ファイルは automatic module を定義します
unnamed module
クラスパス(--class-path
)で読み込まれたモジュール。
全てのパッケージが exports
されている扱いになる。
各モジュールを組み合わせる
各モジュール(named module, automatic module, unnamed module)をいろいろ組み合わせみる。
先に結果を表にすると、
こんな感じになった。
実装(共通)
package sample.sub;
public class Sub {
public void method() {
System.out.println("Sub.method()");
}
}
package sample.main;
import sample.sub.Sub;
public class Main {
public static void main(String... args) {
Sub sub = new Sub();
sub.method();
}
}
named module -> named module
Hello World がこれなので割愛。
named module -> automatic module
|-main/
| |-class/
| `-src/
| |-module-info.java
| `-sample/main/
| `-Main.java
|
`-sub/
|-class/
`-src/
`-sample/sub/
`-Sub.java
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java
# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
module sample_module.main {
requires no.module.sub;
}
-
requires
に指定するsub モジュールの名前は、 jar ファイル名から補完される名前を指定する -
no_module-sub.jar
の場合は、no.module.sub
というモジュール名になる - 詳しい変換ルールは後述
# main モジュールのコンパイル
> javac -p sub\jar\no_module-sub.jar -d main\class main\src\module-info.java main\src\sample\main\Main.java
# 起動
> java -p sub\jar\no_module-sub.jar;main\class -m sample_module.main/sample.main.Main
Sub.method()
- sub モジュールを
-p
(--module-path
) に設定して起動する
automatic module のモジュール名の自動決定ロジック
- マニフェストファイルの
Automatic-Module-Name
属性 - マニフェストファイルで指定されていない場合はファイル名から自動決定
- 末尾の
.jar
を除く - 正規表現
-(\d+(\.|$))
より前だけを抽出する- この正規表現から後ろはバージョン文字列として扱われる
-
xxx-1.1.2.jar
はxxx
がモジュール名として抽出される
- 英数字(
[^A-Za-z0-9]
)以外の文字はドット (.
) に置き換える- ドット (
.
) が繰り返された場合は1つのドットに置き換える
- ドット (
- 末尾の
named module -> unnamed module
できないっぽい。
module sample_module.main {
// requires no.module.sub;
}
> javac -cp sub\jar\no_module-sub.jar -d main\class main\src\module-info.java main\src\sample\main\Main.java
main\src\sample\main\Main.java:3: エラー: パッケージsample.subは存在しません
import sample.sub.Sub;
^
main\src\sample\main\Main.java:7: エラー: シンボルを見つけられません
Sub sub = new Sub();
^
シンボル: クラス Sub
場所: クラス Main
main\src\sample\main\Main.java:7: エラー: シンボルを見つけられません
Sub sub = new Sub();
^
シンボル: クラス Sub
場所: クラス Main
エラー3個
クラスパス(-cp
)に jar を設定しても、クラスが読み込めない。
automatic module -> named module
|-main/
| |-class/
| `-src/
| `-sample/main/
| `-Main.java
|
`-sub/
|-class/
`-src/
|-module-info.java
`-sample/sub/
`-Sub.java
module sample_module.sub {
exports sample.sub;
}
# コンパイル
> javac -d sub\class sub\src\module-info.java sub\src\sample\sub\Sub.java
# jar 作成
> jar -c -f sub\jar\module-sub.jar -C sub\class .
# コンパイル
> javac -d main\class --add-modules sample_module.sub -p sub\jar\module-sub.jar main\src\sample\main\Main.java
# jar 作成
> jar -c -f main\jar\no_module-main.jar -C main\class .
> java -p sub\jar\module-sub.jar;main\jar\no_module-main.jar --add-modules sample_module.sub -m no.module.main/sample.main.Main
Sub.method()
- main モジュールは
module-info.java
がない状態で-p
(--module-path
) に指定しているため、 automatic module として動く - しかし、
module-info.java
が無いので、sample_module.sub
はrequires
されておらず、ロードされていない状態となっている、たぶん(モジュールグラフ上に存在しない、と表現すればいいのかな?) -
--add-modules
を使うと、任意のモジュールをルート・モジュールというものに追加することができる - ルート・モジュールが何なのか厳密には分かっていないが、たぶん依存関係のグラフの起点としてモジュールを追加することなんだと思う
- これで
sample_module.sub
モジュールがロードされて、そこにあるクラスが使えるようになる - automatic module は、他の全てのモジュールを
requires
しているものとして扱われるらしいので、ルート・モジュールとして追加されたモジュールは automatic module から参照できるようになるっぽい- つまり、 main モジュール(automatic module)から、
--add-modules
でルート・モジュールに追加した sub モジュールが参照できるようになる、ということらしい
- つまり、 main モジュール(automatic module)から、
このへん、神々の Togetter を眺めての想像なので、違ってるかもしれないかもしれなくもなくもないかもしれない。
automatic module -> automatic module
|-main/
| |-class/
| `-src/
| `-sample/main/
| `-Main.java
|
`-sub/
|-class/
`-src/
`-sample/sub/
`-Sub.java
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java
# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
# コンパイル
> javac -d main\class -p sub\jar\no_module-sub.jar --add-modules no.module.sub main\src\sample\main\Main.java
# jar
> jar -c -f main\jar\no_module-main.jar -C main\class .
> java -p sub\jar\no_module-sub.jar;main\jar\no_module-main.jar --add-modules no.module.sub -m no.module.main/sample.main.Main
Sub.method()
- こちらも、
--add-modules
を指定することで実行できる - main モジュールの方はメインクラスとして読み込んでいるから、
--add-modules
への指定は不要ということ?
automatic module -> unnamed module
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java
# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
# コンパイル
> javac -d main\class -cp sub\jar\no_module-sub.jar main\src\sample\main\Main.java
# jar
> jar -c -f main\jar\no_module-main.jar -C main\class .
> java -p main\jar\no_module-main.jar -cp sub\jar\no_module-sub.jar -m no.module.main/sample.main.Main
Sub.method()
- 非モジュール jar を
-cp
でクラスパスに設定することで unnamed module として扱える
unnamed module -> named module
|-main/
| |-class/
| `-src/
| `-sample/main/
| `-Main.java
|
`-sub/
|-class/
`-src/
|-module-info.java
`-sample/sub/
`-Sub.java
module sample_module.sub {
exports sample.sub;
}
# コンパイル
> javac -d sub\class sub\src\module-info.java sub\src\sample\sub\Sub.java
# jar
> jar -c -f sub\jar\module-sub.jar -C sub\class .
# コンパイル
> javac -p sub\jar\module-sub.jar --add-modules sample_module.sub -d main\class main\src\sample\main\Main.java
# jar 作成
> jar -c -f main\jar\no_module-main.jar -C main\class .
> java -p sub\jar\module-sub.jar -cp main\jar\no_module-main.jar --add-modules sample_module.sub sample.main.Main
Sub.method()
- sub モジュールの名前を
--add-modules
で指定すれば動く - main 側は unnamed module なので、メインクラスの指定が
-m
を使わない旧来の方法になっている
unnamed module -> automatic module
|-main/
| |-class/
| `-src/
| `-sample/main/
| `-Main.java
|
`-sub/
|-class/
`-src/
`-sample/sub/
`-Sub.java
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java
# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
# コンパイル
> javac -p sub\jar\no_module-sub.jar -d main\class --add-modules no.module.sub main\src\sample\main\Main.java
# jar 作成
> jar -c -f main\jar\no_module-main.jar -C main\class .
> java -p sub\jar\no_module-sub.jar -cp main\jar\no_module-main.jar --add-modules no.module.sub sample.main.Main
Sub.method()
- ファイル名から補完されるモジュール名を
--add-modules
で指定すれば動く
unnamed module -> unnamed module
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java
# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
# コンパイル
> javac -d main\class -cp sub\jar\no_module-sub.jar main\src\sample\main\Main.java
# jar 作成
> jar -c -f main\jar\no_module-main.jar -C main\class .
> java -cp sub\jar\no_module-sub.jar;main\jar\no_module-main.jar sample.main.Main
Sub.method()
- これは、要はモジュールを使わない方法、つまり Java 8 以下で普通にコンパイルして実行しているのと同じ状態になっている
requires の修飾子
requires
には次の2つの修飾子のいずれかを設定することができる。
transitive
static
transitive
A -> B -> C
あるモジュール B
が C
に依存しているとする。
別のモジュール A
が B
を requires
で読み込んだときに、 C
も自動的に requires
したことにできるようにするのが、この transitive
修飾子になる。
まずは transitive 無しの場合
|-A/
| `-src/
| |-module-info.java
| `-a/
| `-A.java
|-B/
| `-src/
| |-module-info.java
| `-b/
| `-B.java
`-C/
`-src/
|-module-info.java
`-c/
`-C.java
C モジュール
package c;
public class C {}
module module_c {
exports c;
}
B モジュール
package b;
import c.C;
public class B {}
module module_b {
requires module_c;
exports b;
}
A モジュール
package a;
import b.B;
import c.C;
public class A {}
module module_a {
requires module_b;
// requires module_c; C モジュールは requires しない状態で試す
exports a;
}
コンパイル
# C モジュールのコンパイル
> javac -d c\class C\src\module-info.java C\src\c\C.java
# B モジュールのコンパイル
> javac -p c\class -d b\class B\src\module-info.java B\src\b\B.java
# A モジュールのコンパイル
> javac -p c\class;b\class -d a\class A\src\module-info.java A\src\a\A.java
A\src\a\A.java:4: エラー: パッケージcは表示不可です
import c.C;
^
(パッケージcはモジュールmodule_cで宣言されていますが、モジュールmodule_aに読み込まれていません)
エラー1個
A モジュールでは C モジュールを明示的に requires
していないので、パッケージ c
は参照できない。
transitive を使う
module module_b {
requires transitive module_c;
exports b;
}
- B モジュールの
module-info.java
でrequires
していた C モジュールにtransitive
を追加
# B モジュールのコンパイル
> javac -p c\class -d b\class B\src\module-info.java B\src\b\B.java
# A モジュールのコンパイル
> javac -p c\class;b\class -d a\class A\src\module-info.java A\src\a\A.java
- 今度はエラー無くコンパイルできた
static
static
は、「コンパイル時には必要だが実行時は任意」なモジュールに対して使用する。
static が無い場合
|-main/
| `-src/
| |-module-info.java
| `-sample/main/
| |-SubFactory.java
| `-Main.java
`-sub/
`-src/
|-module-info.java
`-sample/sub/
`-Sub.java
package sample.sub;
public class Sub {}
module sample_module.sub {
exports sample.sub;
}
package sample.main;
import sample.sub.Sub;
public class SubFactory {
public static Sub newSub() {
return new Sub();
}
}
package sample.main;
public class Main {
public static void main(String... args) {
System.out.println("Hello World!!");
}
}
module sample_module.main {
requires sample_module.sub;
}
- main モジュールには sub モジュールのクラスを参照している
SubFactory
というクラスがいる - このため、コンパイル時には sub モジュールが必要になる
- しかし、
Main
クラスはSubFactory
を使用していないため、起動時には sub モジュールは不要ということになる
コンパイルしてみる。
# sub モジュールのコンパイル
> javac -d sub\class sub\src\module-info.java sub\src\sample\sub\Sub.java
# main モジュールのコンパイル
> javac -p sub\class -d main\class main\src\module-info.java main\src\sample\main\Main.java main\src\sample\main\SubFactory.java
コンパイルは通る。
続いて実行してみる。
> java -p main\class -m sample_module.main/sample.main.Main
Error occurred during initialization of boot layer
java.lang.module.FindException: Module sample_module.sub not found, required by sample_module.main
sub モジュールが見つからなくてエラーになった。
static をつけた場合
module sample_module.main {
requires static sample_module.sub;
}
static
をつけて同じようにコンパイル、実行してみる。
# main モジュールのコンパイル
> javac -p sub\class -d main\class main\src\module-info.java main\src\sample\main\Main.java main\src\sample\main\SubFactory.java
# 実行
> java -p main\class -m sample_module.main/sample.main.Main
Hello World!!
実行できた。
open
コンパイル時は参照できないが、実行時は参照できるように制御することができる。
これは、リフレクションを使ってコードを参照しているような場合に利用することになる。
パッケージ単位で設定する opens
と、モジュール全体を設定する open module
の2つが存在する。
opens
|-main/
| `-src/
| |-module-info.java
| `-sample/main/
| `-Main.java
`-sub/
`-src/
|-module-info.java
`-sample/sub/
`-Sub.java
package sample.sub;
public class Sub {
public void sub() {
System.out.println("Sub.sub()");
}
}
module sample_module.sub {
// まずは一切公開せずに作成する
}
package sample.main;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class Main {
public static void main(String... args) throws Exception {
Class<?> subClass = Class.forName("sample.sub.Sub");
Constructor<?> constructor = subClass.getConstructor();
Object sub = constructor.newInstance();
Method method = subClass.getMethod("sub");
method.invoke(sub);
}
}
sample.sub
パッケージは直接参照せず、リフレクションを使って Sub.sub()
メソッドを実行している。
module sample_module.main {
requires sample_module.sub;
}
コンパイルしてみる。
# sub モジュールのコンパイル
> javac -d sub\class sub\src\module-info.java sub\src\sample\sub\Sub.java
# main モジュールのコンパイル
> javac -p sub\class -d main\class main\src\module-info.java main\src\sample\main\Main.java
sample.sub
は、静的には参照されていないため、コンパイルは通る。
しかし、これを実行すると。
> java -p sub\class;main\class -m sample_module.main/sample.main.Main
Exception in thread "main" java.lang.IllegalAccessException: class sample.main.Main (in module sample_module.main) cannot access class sample.sub.Sub (in module sample_module.sub) because module sample_module.sub does not export sample.sub to module sample_module.main
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:589)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:479)
at sample_module.main/sample.main.Main.main(Main.java:10)
IllegalAccessException
がスローされる。
sample.sub
を opens
で公開してやり直してみる。
module sample_module.sub {
opens sample.sub;
}
# sub モジュールのコンパイル
> javac -d sub\class sub\src\module-info.java sub\src\sample\sub\Sub.java
# main モジュールのコンパイル
> javac -p sub\class -d main\class main\src\module-info.java main\src\sample\main\Main.java
# 実行
> java -p sub\class;main\class -m sample_module.main/sample.main.Main
Sub.sub()
今度はうまく動いた。
ところで、この状態(opens sample.sub;
)で Main.java
で sample.sub
パッケージを参照するようにすると、次のようになる。
package sample.main;
import sample.sub.Sub;
public class Main {
public static void main(String... args) throws Exception {
new Sub().sub();
}
}
普通に Sub
を new
してメソッドを実行している。
# main モジュールのコンパイル
> javac -p sub\class -d main\class main\src\module-info.java main\src\sample\main\Main.java
main\src\sample\main\Main.java:3: エラー: パッケージsample.subは表示不可です
import sample.sub.Sub;
^
(パッケージsample.subはモジュールsample_module.subで宣言されていますが、エクスポートされていません)
エラー1個
sample.sub
パッケージはあくまで opens
であり、 exports
はされていない。
そのため、コンパイル時に静的に参照しようとするとエラーになる。
それぞれのタイミングでの参照の可否をまとめると、
参照タイミング | opens |
exports |
---|---|---|
コンパイル時 | × | ○ |
実行時 | ○ | ○ |
こんな感じ。
open module
opens
は特定のパッケージに対する設定だが、 open module
を指定するとモジュール全体に opens
を適用した状態になる。
|-main/
| `-src/
| |-module-info.java
| `-sample/main/
| `-Main.java
`-sub/
`-src/
|-module-info.java
`-sample/sub/
|-foo/
| `-Foo.java
`-Sub.java
package sample.sub.foo;
public class Foo {
public void foo() {
System.out.println("Foo.foo()");
}
}
open module sample_module.sub {
exports sample.sub;
}
- モジュールの前に
open
を追加している -
sample.sub
のみexports
して、sample.sub.foo
パッケージについては何も設定していない -
Sub.java
は変更なし
package sample.main;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import sample.sub.Sub;
public class Main {
public static void main(String... args) throws Exception {
new Sub().sub();
Class<?> clazz = Class.forName("sample.sub.foo.Foo");
Constructor<?> constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
Method method = clazz.getMethod("foo");
method.invoke(obj);
}
}
-
Sub
クラスは静的に参照してメソッドを実行している -
Foo
クラスはリフレクションを使ってメソッドを実行している
# sub モジュールのコンパイル
> javac -d sub\class sub\src\module-info.java sub\src\sample\sub\Sub.java sub\src\sample\sub\foo\Foo.java
# main モジュールのコンパイル
> javac -p sub\class -d main\class main\src\module-info.java main\src\sample\main\Main.java
# 実行
> java -p sub\class;main\class -m sample_module.main/sample.main.Main
Sub.sub()
Foo.foo()
-
sample.sub.foo.Foo
はopens
もexports
も宣言していなかったが、リフレクションから参照できている -
module
をopen
で修飾することで、そのモジュールに含まれるパッケージが全てopens
で修飾された状態になる
use, provides
ServiceLoader の定義が Java 9 からは module-info.java
でできるようになった。
実装
|-user/
| `-src/
| |-module-info.java
| `-sample/user/
| `-Main.java
|
`-provider/
`-src/
|-module-info.java
`-sample/provider/
|-api/
| `-Foo.java
`-impl/
`-FooImpl.java
package sample.provider.api;
public interface Foo {
void foo();
}
package sample.provider.impl;
import sample.provider.api.Foo;
public class FooImpl implements Foo {
@Override
public void foo() {
System.out.println("FooImpl.foo()");
}
}
module sample_module.provider {
exports sample.provider.api;
provides sample.provider.api.Foo with sample.provider.impl.FooImpl;
}
package sample.user;
import java.util.ServiceLoader;
import sample.provider.api.Foo;
public class Main {
public static void main(String... args) {
for (Foo foo : ServiceLoader.load(Foo.class)) {
foo.foo();
}
}
}
module sample_module.user {
requires sample_module.provider;
uses sample.provider.api.Foo;
}
動作確認
# provider モジュールのコンパイル
> javac -d provider\class provider\src\module-info.java provider\src\sample\provider\api\Foo.java provider\src\sample\provider\impl\FooImpl.java
# user モジュールのコンパイル
> javac -d user\class -p provider\class user\src\module-info.java user\src\sample\user\Main.java
> java -p provider\class;user\class -m sample_module.user/sample.user.Main
FooImpl.foo()
説明
module sample_module.provider {
exports sample.provider.api;
provides sample.provider.api.Foo with sample.provider.impl.FooImpl;
}
module sample_module.user {
requires sample_module.provider;
uses sample.provider.api.Foo;
}
- 「サービスプロバイダ1」を提供する側は、
provides
を使って「サービス2」に対する「サービスプロバイダ」を指定するprovides <サービス> with <サービスプロバイダ>;
- 「サービスプロバイダ」が複数ある場合は、
with
の後ろの「サービスプロバイダ」をカンマ区切りで列挙できる -
ServiceLoader
を使う側は、uses
を使って、利用する「サービス」を指定するuses <サービス>;
- あとは、通常の
ServiceLoader
と同じ方法で利用する
OSS のフレームワークのコードを読むときとかに、宣言方法が増えていることに注意しないといけなさそう。
jlink
jlink コマンドを使用すると、必要最低限のモジュールだけを集めた Java ランタイム(サブセット)を作成することができる。
配布先に JRE がインストールされていなくても、 jlink で作ったサブセットを入れればアプリケーションを起動できるようになる。
実装
`-src/
|-module-info.java
`-sample/jlink/
`-Main.java
package sample.jlink;
public class Main {
public static void main(String... args) {
System.out.println("Hello jlink!!");
}
}
module sample_module.jlink {
}
サブセットを作成する
# コンパイル
> javac -d class src\module-info.java src\sample\jlink\Main.java
# jar 作成
> jar -c -f jar\module-jlink.jar -C class .
# jlink でランタイムを作成
> jlink -p .\jar;%JAVA_HOME%\jmods --add-modules sample_module.jlink --output .\output
-
jlink
コマンドでは、最低限以下を指定する-
-p
(--module-path
)- 組み込むモジュールが存在するパスを指定する
- 標準ライブラリのモジュールも必要になるので、 JDK9 のインストールフォルダ(
%JAVA_HOME%
)の下のjmods
フォルダもモジュールパスに追加する
-
--add-modules
- ルート・モジュールを指定するらしい
- ルート・モジュールが何なのかよくわかっていないが、たぶんモジュールの依存関係のグラフを構築するときの起点となるモジュールな気がする
- ここを指定すれば、あとは芋づる式に必要なモジュールが検索されて、作成するランタイムに組み込まれることになるのだと思う
-
--output
- 出力先のフォルダのパスを指定する
- 既にフォルダが存在するとエラーになるので、存在しないパスを指定する
-
以下のように、 output
フォルダの下にランタイム入りのアプリケーションが出力される。
`-output/
|-bin/
|-conf/
|-include/
|-legal/
|-lib/
`-release
bin
の下には java.exe
ファイルが配置されており、これがサブセット用の java
コマンドとなっている。
# サブセットの bin に移動し
> cd output\bin
# 含まれているモジュールを確認
> java --list-modules
java.base@9
sample_module.jlink
サブセットには、必要最小限のモジュール(java.base
と sample_module.jlink
)しか含まれていない。
プログラムを起動してみる。
> java -m sample_module.jlink/sample.jlink.Main
Hello jlink!!
sample_module.jlink
はランタイムの中に組み込まれているので、 -p
で指定する必要はない。
ちなみに、作成されたランタイムのサイズは Windows のエクスプローラで確認したところ 35.9 MB だった。
圧縮する
オプションである程度圧縮が可能。
> jlink --compress=2 -p .\jar;%JAVA_HOME%\jmods --add-modules sample_module.jlink --output .\output
-
--compress=N
で圧縮指定が可能。 -
N
には0
,1
,2
のいずれかを指定できる - ヘルプでは次のように説明されている(Level 1 はよくわからん)
-c, --compress=<0|1|2> Enable compression of resources:
Level 0: No compression
Level 1: Constant string sharing
Level 2: ZIP
- ちなみに、サイズは 24.2 MB になった
起動ファイルを生成する
起動の際に -m
でメインクラスを指定する必要があるのは、 Java に疎い人に配布するときのことを考えるとあまりよろしくない。
その場合は、起動用のスクリプトファイルを生成しておくと便利。
> jlink --launcher jlink-hello=sample_module.jlink/sample.jlink.Main -p .\jar;%JAVA_HOME%\jmods --add-modules sample_module.jlink --output .\output
-
--launcher <コマンド名>=<メインクラス>
と指定する -
output\bin
の下に<コマンド名>
で指定した起動スクリプトが出力されるので、それを実行すればプログラムが起動する
# bin の下に移動して
> cd output\bin
# 起動スクリプトを実行
> jlink-hello.bat
Hello jlink!!
生成されたスクリプトファイルは、以下のような内容になっている。
@echo off
set JLINK_VM_OPTIONS=
set DIR=%~dp0
"%DIR%\java" %JLINK_VM_OPTIONS% -m sample_module.jlink/sample.jlink.Main %*
一応 Linux 用のシェルスクリプトも出力されていたけど、起動できるのだろうか?
依存モジュールを調べる
jdeps
コマンドを使うと、 jar ファイルがどのモジュールに依存しているかを調べることができる。
既存の非モジュール jar を追加するときに、標準ライブラリのどのモジュールを追加すればいいかが分かるようになる。
試しに、 Apache Commons lang-3 を調べてみる。
> jdeps -s commons-lang3-3.6.jar
commons-lang3-3.6.jar -> java.base
commons-lang3-3.6.jar -> java.desktop
-s
オプションをつけて jar を食わせると、依存するモジュールを表示してくれる。
java.desktop は、 awt や swing が含まれるモジュールだけど、 beans とかがあるのでそれかなぁ。
jar が named module の場合は、依存するモジュールを --module-path
で指定する必要がある(まぁ、当然なんだろうけど、なんか依存モジュールを調べるために依存モジュールを用意するところが変な感じがする)。
参考
- 最新Java情報局 - Java SE 9を先取り、Project Jigsawでモジュールを作成する:ITpro
- 最新Java情報局 - Java SE 9、Project Jigsawの標準モジュールと依存性の記述:ITpro
- 最新Java情報局 - Project JigsawによるJREのカスタマイズ:ITpro
- The Java® Language Specification
- Getting Started with Java 9 Modules - ConSol Labs
- ModuleFinder (Java SE 9 & JDK 9 [build 176])
- Project JigawにおけるUnnamed Moduleと通常のModuleの共存 - Togetterまとめ
- ykubota/jigsaw-sample-accessibility: Example program to run Modular JAR with Java 8's JAR.
- ServiceLoaderでプラグインの仕組みをさくっと作る。 - うなの日記
- JDK9のモジュールとjlinkでアプリ配布向けのJVMを作る - Qiita