Java 11 Gold 取得に向けた学習記録
モジュールシステム
複数のパッケージをまとめる仕組みをモジュールシステムという。
定義
モジュールの定義は module-info.java
(モジュール定義ファイル)に記述する。このファイルに記述する情報は モジュールディレクティブ という。
module モジュール名 {
exports 公開するパッケージ名;
requires このモジュールが依存する別のモジュール名;
}
module-info.java
を配置したフォルダはモジュールとして扱われる。
パッケージ内ではクラス単位でアクセス制御を行うのに対して、モジュールシステムではパッケージ単位でアクセス制御を行うことができる。
モジュール名
モジュール名はパッケージと同様に一意であることが必要。そのため他のモジュール名と被らない名前にする必要がある。
通常は、モジュールに含めるパッケージ名に共通の文字列があれば、その部分をモジュール名として使用する。共通部分がない場合は、モジュールの目的を表現する名前にする事が望ましい。
ちなみにモジュール名とパッケージ名は重複しても良い。
ただし、ドット「.
」で区切ったパッケージがフォルダ階層にマッピングされるのに対して、モジュール名にドット「.
」を含めてもフォルダ階層にマッピングされない。そのため、モジュールを配置するフォルダ名はキャメルケースなどで命名する、もしくはmods
などのプロジェクト毎に変わらない名前にする。
モジュールディレクティブ
モジュール宣言で使用するrequires
、exports
、provides
、with
、uses
、opens
、provides
キーワードなどは モジュールディレクティブ と呼ばれる。
requires
自身のモジュールが利用(依存)する外部のモジュール名を指定する。
module my.module {
requires another.module;
}
requires transive
transitive=推移的
依存するモジュールがさらに依存するモジュールについても暗黙的に含めることができる。モジュールグラフで表現するとこちら。
module moduleA {
requires transive moduleB; // 暗黙的に moduleC にも依存する
}
module moduleB {
requires moduleC;
}
module moduleC {
}
exports
自身のモジュール内で外部に公開するパッケージ名を指定する。
module my.module {
exports com.example.mypackage;
}
provides
/ with
モジュールが特定の サービス の実装を提供する サービスプロバイダ であることを宣言する。プログラム実行時に サービスローダ が動的にサービスプロバイダをロードする際に、この情報が検索時のヒントとして利用される。
module my.module {
provides com.example.MyServiceInterface with com.example.MyServiceImpl;
}
provides
では サービス を指定し、with
では サービスプロバイダ を指定する。
サービスプロバイダ
特定の機能を定義したインターフェースまたは抽象クラスのことをサービスと呼び、サービスの実装クラスもしくは具象クラスのことをサービスプロバイダという。
package com.example;
public interface MyServiceInterface {
void provideService();
}
package com.example.imple;
import com.example.MyServiceInterface;
public class MyServiceImpl implements MyServiceInterface {
@Override
public void provideService() {
System.out.println("サービスを提供します");
}
}
provides
ディレクティブでは サービス を指定し、with
ディレクティブでは サービスプロバイダ を指定する。これにより、自身のモジュールがサービスプロバイダを提供できることが外部に宣言される。
module my.module {
provides com.example.MyServiceInterface with com.example.impl.MyServiceImpl;
}
サービスプロバイダを含むパッケージはexports
しない(推奨)。サービスプロバイダはコンパイル時ではなくプログラムの実行時にサービスローダによってリフレクションを利用してロードされるので、あえて実装(サービスプロバイダ)を公開する必要がない。
module my.module {
exports com.example.impl; // サービスプロバイダを含むパッケージは公開しなくて良い
provides com.example.MyServiceInterface with com.example.impl.MyServiceImpl;
}
通常、特定のサービスに対しては複数のサービスプロバイダが存在する。
package com.another.example.impl;
import com.example.MyServiceInterface;
public class AnotherServiceImpl implements MyServiceInterface {
@Override
public void provideService() {
System.out.println("別のサービスを提供します");
}
}
別のサービスプロバイダが定義されたモジュール。
module my.another.module {
requires my.module; // サービスの定義を含むモジュール
provides com.example.MyServiceInterface with com.another.example.impl.AnotherServiceImpl;
}
サービスローダ
module-info.java
でprovide
ディレクティブによって指定されたサービスプロバイダ(サービスを実装したクラス。MyServiceImpl
、AnotherServiceImpl
)を、プログラムの実行時に動的に探し出し、ロードする機能を持つものを サービスローダ という。
動的に検索したいサービスは、uses
ディレクティブで指定する必要がある。
module consumer.module {
uses com.example.MyServiceInterface; // 動的にロードしたいサービス
}
サービスローダの機能はjava.util.ServiceLoader
から提供されている。
サービスローダはサービスプロバイダ(サービスの実装クラス)をロードして、メソッドを実行するためのフレームワークを提供している。
ロードはload()
メソッドによって行う。
public static <S> ServiceLoader<S> load(Class<S> service)
import java.util.ServiceLoader;
ServiceLoader<MyServiceInterface> serviceLoader = ServiceLoader.load(MyServiceInterface.class);
引数のClass<S>
オブジェクト取得方法はいくつかバリエーションがある。
ServiceLoader<S>
オブジェクトはIterable
インターフェースを実装しているため、反復処理(for
文)を使って各要素にアクセスすることができる。
import java.util.ServiceLoader;
ServiceLoader<MyServiceInterface> serviceLoader = ServiceLoader.load(MyServiceInterface.class);
for (MyServiceInterface service : serviceLoader) {
// サービスプロバイダ(サービスの各実装クラス)のメソッドが実行される
// この時、アプリケーション(クライアント)はどの実装クラスが取得されるかを知らない
service.provideService();
// >> サービスを提供します
// >> 別のサービスを提供します
}
サービスローダによってアプリケーションとサービスプロバイダが分離されるため、アプリケーションコードを一切変更することなく、特定のモジュールをモジュールパスに追加するだけでプラグインや拡張機能として動的に追加することが可能になる。
ただし、uses
ディレクティブでどのサービスを使用するのかを指定しておく必要がある。
サービスローダはuses
ディレクティブと併せて使う
uses
uses
キーワードはモジュールが特定のサービスを使用することをサービスローダ伝えることができる。サービスローダはこの情報を基に、サービスの実装を動的に取得する。
module consumer.module {
requires my.module; // サービスとサービスプロバイダが含まれるモジュール
requires my.another.module; // my.moduleとは異なるサービスプロバイダが含まれるモジュール
uses com.example.MyServiceInterface; // サービス
}
opens
リフレクションによるアクセスを許可するパッケージを指定する。
module my.module {
opens com.example.mypackage
}
モジュール宣言において、exports
を使用して外部へ公開したパッケージは通常、public
、 protected
宣言したものだけがリフレクションからのアクセスできる。
opens
ディレクティブで指定したパッケージは、パッケージとしては公開されない一方で、public
、 protected
以外もリフレクションからのアクセスの対象にすることができる。
また opens <package> to <module>
とすることで、リフレクションを許可するモジュールを限定することができる。
module moduleA {
opens com.example.mypackage to moduleB;
}
モジュール全体をリフレクションの対象とすることもできる。
open module my.module {
}
コンパイル
module-info.java
をコンパイルすると、モジュール記述子が作成される。モジュール記述子は、モジュールのルートフォルダ内の module-info.class
に格納される。
プラットフォームモジュール
標準クラスライブラリをモジュールとして提供するjava.base
モジュールについては、requires
を記述しなくても使用することができる。このようなモジュールをプラットフォームモジュールと呼ぶ。
名前つきモジュール
モジュール定義ファイル(module-info.java
)を持つモジュール。
自動モジュール
module-info.java
を持たないモジュール。
モジュールシステム自体が Java 9 で導入されたため、それ以前のアプリケーションもしくはライブラリとの互換性を保つためのもの。
module-info.java
を持たないため自動的にモジュールとして扱われ、全てのパッケージが公開され、パッケージ単位でアクセス制御を行うことができない。
また、 project_root/META_INF/
に配置したマニフェストファイル(MANUFEST.MF
)内の Automatic-Module-Name
属性を定義することで、モジュール名を定義することはできる。マニフェストファイルすら存在しない場合、jarファイル名がモジュール名として扱われる。
無名モジュール
module-info.java
を持たないことに加え、モジュールパスではなくクラスパス上に配置されたjarファイル。
自動モジュールと同様に、Java 9 以前のアプリケーションとの互換性のためのもの。Java 9 以降では、通常はモジュール宣言が必要なため、名前つきモジュールから無名モジュールを参照することはできない。
名前つきモジュールから無名モジュールへはアクセスできない
モジュールグラフ
モジュール間の依存関係を表現した図をモジュールグラフと言う。
moduleA
が moduleB
に依存し、moduleB
が moduleC
に依存している場合、モジュールグラフは以下のようになる。
この状況で moduleB
が moduleC
への依存を moduleA
に伝播させることができる。
モジュールの依存を伝播させるためにはtransitive
を使用する。
module moduleB {
requires transitive moduleC;
}
これによりモジュールAはrequires moduleC
を記述しなくても直接モジュールCが使用できるようになる。
この時、moduleA
と moduleC
には推移的な依存関係があると表現される。
例
- モジュール名:
a.b.c
- モジュールに含めるパッケージ
-
x.y.z
パッケージ -
x.y.zz
パッケージ
-
(通常はパッケージの共通部分であるx.y
を使用してモジュール名はx.y.myModule
などとした方が良い。)
project
|
|--- src
| |
| |--- abc ◀️ モジュールのルートディレクトリ(階層にしない。キャメルケースが適切?)
| |
| |--- x ◀️ パッケージのルートディレクトリ
| | |
| | |--- y
| | |
| | |--- z
| | | |
| | | |--- Main.java
| | |
| | |--- zz
| | |
| | |--- MyClass.java
| |
| |--- module-info.java
|
|--- mods ◀️ コンパイルの出力先( ◀️ ◀️ ◀️ コンパイル後のモジュールのルートディレクトリ)
|
|--- abc ◀️ コンパイルしたモジュール用のフォルダ
コンパイル
コンパイル時には-d
オプションで出力先を指定することができる。
javac \
-d コンパイルしたクラスファイルの格納先 コンパイル対象のソースファイル
javac \
-d mods/abc \
src/abc/module-info.java \
src/abc/x/y/z/Main.java \
src/abc/x/y/zz/MyClass.java
実行
コンパイルしたクラスファイルを指定して実行する。
モジュールを実行したい場合、--module-path
(-p
)オプションでモジュールのルートディレクトリを指定し、-module
(-m
)オプションで実行したいモジュールのクラスを指定する。
モジュール名とパッケージはスラッシュ「/
」で区切る。
java \
--module-path コンパイル後のモジュールのルートディレクトリ \
-module モジュール名/FQCN(エントリーポイント)
java \
--module-path mods \
-module a.b.c/x.y.z.Main
JARファイル化
モジュールをJARファイルにまとめたいときには以下のようにする。
-
--create
: 新しくJARファイルを作成する -
--file-name
(-f
): JARファイルの名前 -
--main-class
: エントリポイントのFQCN -
-C
: JARファイル化するフォルダ(フォルダ名をドット「.
」にすることでフォルダ内の全てのファイルを対象にする)
jar --create \
--file-name JARファイル名 \
--main-class エントリポイント \
-C JARファイルにするフォルダパス
jar --create \
--file-name jars/a.b.c.jar \
--main-class x.y.z.Main \
-C mods/abc .
JMODファイル化
JMODファイルは、モジュールをバンドル化して配布するための形式の1つ。
バンドル化とは、ソフトウェアの複数の要素(例えば、クラスファイル、リソースファイル、ライブラリなど)を一つのアーカイブファイルにまとめることを指す。
JavaのモジュールシステムにおいてJMOD形式のファイルはこのバンドル化の一種であり、モジュールに関連するすべてのファイルを一つの単位として扱いやすくする。
jmod create \
--class-path クラスパス \
出力JMODファイル名 モジュール名
jmod create \
--class-path mods/abc \
abc.jmod a.b.c
情報を確認する1
モジュールが公開するパッケージや依存するモジュールを調べる方法。
java \
--module-path コンパイル後のモジュールのルートディレクトリ \
--describe-module モジュール名
java \
-module-path mods \
--describe-module a.b.c
情報を確認する2
対象のモジュールがJMOD形式の時の使用できる、モジュールが公開するパッケージや依存するモジュールを調べる方法。
jmod describe モジュール名
jmod describe a.b.c
情報を確認する3(依存関係のみ調べる)
jdeps
コマンドを使うと、モジュールに限らずJARファイルやクラス同士の依存関係を調べることができる。
jdeps JARファイル名
jdeps --module-path コンパイル後のモジュールのルートディレクトリ -module モジュール名
jdeps --module-path mods -module a.b.c