LoginSignup
41
40

More than 5 years have passed since last update.

Jigsaw 勉強メモ

Posted at

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

ソースコード

sub/src/sample/sub/api/Foo.java
package sample.sub.api;

public interface Foo {
    void hello();
}
sub/src/sample/sub/internal/FooImpl.java
package sample.sub.internal;

import sample.sub.api.Foo;

public class FooImpl implements Foo {
    @Override
    public void hello() {
        System.out.println("FooImpl.hello()");
    }
}
sub/src/sample/sub/FooFactory.java
package sample.sub;

import sample.sub.api.Foo;
import sample.sub.internal.FooImpl;

public class FooFactory {
    public static Foo newInstance() {
        return new FooImpl();
    }
}
sub/src/module-info.java
module sample_module.sub {
    exports sample.sub;
    exports sample.sub.api;
}
main/src/sample/main/Main.java
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();
    }
}
main/src/module-info.java
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()

説明

依存される側のモジュール宣言

sub/src/module-info.java
module sample_module.sub {
    exports sample.sub;
    exports sample.sub.api;
}
  • モジュールの宣言は、ルートパッケージに置く module-info.java という専用のファイルに記述する
    • 名前は module-info.java 固定
  • モジュール宣言は、 module <モジュール名> {<モジュール定義>} という形式で記述する
  • モジュール名はパッケージ名と同じようにドット . 区切りで記述できる
    • 通常はパッケージ名と同じような名前になるようにつけるようだが、それだとサンプル実装上パッケージ名とモジュール名が区別しづらいので、ここではあえて sample_module.sub とパッケージとは異なる名前を付けている
  • {} の中ではこのモジュールに関する定義を記述できる
  • exports は、外部に公開するパッケージを指定する
  • ここでは sample.subsample.sub.api の2パッケージを外部に公開している
    • sample.sub.internal は内部 API のつもりなので、公開しないようにしている

参照する側のモジュール宣言

main/src/module-info.java
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 などならコロン : で連結して指定する
  • また、メインクラスの指定は -m--module)オプションで指定するように変わった(モジュールを使用している場合)
  • メインクラス名の指定も変わっていて、 <module名>/<メインクラス名> と指定する
    • ここでは、 sample_module.main モジュールの sample.main.Main クラスを指定するため、 sample_module.main/sample.main.Main としている

exports していないパッケージを参照した場合の挙動

Main.java
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.basejava.lang パッケージとかが含まれるモジュールで、指定していなくても必ずついてくる
  • contains は、おそらく非公開のパッケージ

実行する

> java -p sub\jar;main\jar -m sample_module.main/sample.main.Main
FooImpl.hello()
  • -p で、モジュール(jar)のあるフォルダを指定すれば、そこにある jar を読み込んでくれるっぽい
    • jar ファイルまでのパスを直接しても良い

モジュールの種類

モジュールには、大きく次の3つの種類がある。

  1. named module
  2. automatic module
  3. 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)をいろいろ組み合わせみる。

先に結果を表にすると、

jigsaw.jpg

こんな感じになった。

実装(共通)

sub/src/sample/sub/Sub.java
package sample.sub;

public class Sub {
    public void method() {
        System.out.println("Sub.method()");
    }
}
main/src/sample/main/Main.java
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
subモジュールのコンパイル
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java

# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
main\src\module-info.java
module sample_module.main {
    requires no.module.sub;
}
  • requires に指定するsub モジュールの名前は、 jar ファイル名から補完される名前を指定する
  • no_module-sub.jar の場合は、 no.module.sub というモジュール名になる
  • 詳しい変換ルールは後述
mainモジュールのコンパイルと起動
# 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 のモジュール名の自動決定ロジック

  1. マニフェストファイルの Automatic-Module-Name 属性
  2. マニフェストファイルで指定されていない場合はファイル名から自動決定
    • 末尾の .jar を除く
    • 正規表現 -(\d+(\.|$)) より前だけを抽出する
      • この正規表現から後ろはバージョン文字列として扱われる
      • xxx-1.1.2.jarxxx がモジュール名として抽出される
    • 英数字([^A-Za-z0-9])以外の文字はドット (.) に置き換える
      • ドット (.) が繰り返された場合は1つのドットに置き換える

named module -> unnamed module

できないっぽい。

main/src/module-info.java
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
sub/src/module-info.java
module sample_module.sub {
    exports sample.sub;
}
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 .
mainモジュールのコンパイル
# コンパイル
> 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.subrequires されておらず、ロードされていない状態となっている、たぶん(モジュールグラフ上に存在しない、と表現すればいいのかな?)
  • --add-modules を使うと、任意のモジュールをルート・モジュールというものに追加することができる
  • ルート・モジュールが何なのか厳密には分かっていないが、たぶん依存関係のグラフの起点としてモジュールを追加することなんだと思う
  • これで sample_module.sub モジュールがロードされて、そこにあるクラスが使えるようになる
  • automatic module は、他の全てのモジュールを requires しているものとして扱われるらしいので、ルート・モジュールとして追加されたモジュールは automatic module から参照できるようになるっぽい
    • つまり、 main モジュール(automatic module)から、 --add-modules でルート・モジュールに追加した sub モジュールが参照できるようになる、ということらしい

このへん、神々の Togetter を眺めての想像なので、違ってるかもしれないかもしれなくもなくもないかもしれない。

automatic module -> automatic module

フォルダ構成
|-main/
| |-class/
| `-src/
|   `-sample/main/
|     `-Main.java
|
`-sub/
  |-class/
  `-src/
    `-sample/sub/
      `-Sub.java
subモジュールのコンパイル
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java

# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
mainモジュールのコンパイル
# コンパイル
> 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

subモジュールのコンパイル
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java

# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
mainモジュールのコンパイル
# コンパイル
> 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
sub/src/module-info.java
module sample_module.sub {
    exports sample.sub;
}
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 .
mainモジュールのコンパイル
# コンパイル
> 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
subモジュールのコンパイル
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java

# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
mainモジュールのコンパイル
# コンパイル
> 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

subモジュールのコンパイル
# コンパイル
> javac -d sub\class sub\src\sample\sub\Sub.java

# jar 作成
> jar -c -f sub\jar\no_module-sub.jar -C sub\class .
mainモジュールのコンパイル
# コンパイル
> 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

あるモジュール BC に依存しているとする。
別のモジュール ABrequires で読み込んだときに、 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 モジュール

C.java
package c;

public class C {}
C/src/module-info.java
module module_c {
    exports c;
}

B モジュール

B.java
package b;

import c.C;

public class B {}
B/src/module-info.java
module module_b {
    requires module_c;
    exports b;
}

A モジュール

A.java
package a;

import b.B;
import c.C;

public class A {}
A/src/module-info.java
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 を使う

B\src\module-info.java
module module_b {
    requires transitive module_c;
    exports b;
}
  • B モジュールの module-info.javarequires していた 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
Sub.java
package sample.sub;

public class Sub {}
sub\src\module-info.java
module sample_module.sub {
    exports sample.sub;
}
SubFactory.java
package sample.main;

import sample.sub.Sub;

public class SubFactory {
    public static Sub newSub() {
        return new Sub();
    }
}
Main.java
package sample.main;

public class Main {
    public static void main(String... args) {
        System.out.println("Hello World!!");
    }
}
main/src/module-info.java
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 をつけた場合

main\src\module-info.java
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
Sub.java
package sample.sub;

public class Sub {
    public void sub() {
        System.out.println("Sub.sub()");
    }
}
sub\src\module-info.java
module sample_module.sub {
    // まずは一切公開せずに作成する
}
Main.java
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() メソッドを実行している。

main/src/module-info.java
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.subopens で公開してやり直してみる。

sub\src\module-info.java
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.javasample.sub パッケージを参照するようにすると、次のようになる。

Main.java
package sample.main;

import sample.sub.Sub;

public class Main {
    public static void main(String... args) throws Exception {
        new Sub().sub();
    }
}

普通に Subnew してメソッドを実行している。

コンパイル
# 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
Foo.java
package sample.sub.foo;

public class Foo {
    public void foo() {
        System.out.println("Foo.foo()");
    }
}
sub/src/module-info.java
open module sample_module.sub {
    exports sample.sub;
}
  • モジュールの前に open を追加している
  • sample.sub のみ exports して、 sample.sub.foo パッケージについては何も設定していない
  • Sub.java は変更なし
Main.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.Fooopensexports も宣言していなかったが、リフレクションから参照できている
  • moduleopen で修飾することで、そのモジュールに含まれるパッケージが全て 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
Foo.java
package sample.provider.api;

public interface Foo {
    void foo();
}
FooImpl.java
package sample.provider.impl;

import sample.provider.api.Foo;

public class FooImpl implements Foo {
    @Override
    public void foo() {
        System.out.println("FooImpl.foo()");
    }
}
provider/src/module-info.java
module sample_module.provider {
    exports sample.provider.api;
    provides sample.provider.api.Foo with sample.provider.impl.FooImpl;
}
Main.java
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();
        }
    }
}
user/src/module-info.java
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()

説明

provider/src/module-info.java
module sample_module.provider {
    exports sample.provider.api;
    provides sample.provider.api.Foo with sample.provider.impl.FooImpl;
}
user/src/module-info.java
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
Main.java
package sample.jlink;

public class Main {
    public static void main(String... args) {
        System.out.println("Hello jlink!!");
    }
}
module-info.java
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.basesample_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!!

生成されたスクリプトファイルは、以下のような内容になっている。

jlink-hello.bat
@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 で指定する必要がある(まぁ、当然なんだろうけど、なんか依存モジュールを調べるために依存モジュールを用意するところが変な感じがする)。

参考


  1. 「サービス」を実装したクラス 

  2. インターフェースや抽象クラスなど 

41
40
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
40