Javaモジュールシステム¶
Javaモジュールシステム(Java Platform Module System: JPMS)は、Java SE 9から導入されたモジュール構成です。JSR 376で仕様化され、開発名 Project Jigsaw として知られています。
モジュールの目的¶
"A module is a set of packages designed for reuse."(モジュールは再利用のために設計されたパッケージの集合)
JavaOne SF 2017 'Modular Development with JDK 9'より
パッケージの上位にモジュールという新たな構成上の概念を導入しました。
モジュールの中に含まれるパッケージを、モジュール外部に公開するか否かを制御できるようになっています。また、モジュール間の依存関係を定義できるようになっています。
パッケージより上位の可視性制御¶
これまでのJava
では、パッケージの中のクラスをpublic
にすると、そのクラスは他のパッケージのいずれにおいても可視となってしまい、どこからでも利用可能になっていました。
しかし、多少複雑なライブラリを開発するような場合、設計として複数のパッケージに分割することがよくあります。分割したパッケージは、APIのようにライブラリを利用するアプリケーションからアクセスするためのpublic
な型もありますが、ライブラリの中だけで利用する内部限定のパッケージではライブラリ内の他のパッケージからのみアクセスするpublic
な型となります。
内部限定のパッケージでアプリケーションには公開したくないが、ライブラリの他のパッケージからアクセスするため設計上public
にせざるを得ない型は、言語仕様ではアクセスを制御することができません。せいぜい、ドキュメントでライブラリ内部で使う型につき、ライブラリの外部からは使わないよう注意喚起する程度しかありませんでした。
Java SE 9でモジュールが導入されたことによって、モジュールの定義としてパッケージ単位に公開するか否か制御できるようになりました。そこで、モジュールの中に含まれるパッケージについて、外部から利用できず、内部からのみ利用できるパッケージ(とそのpublicクラス)を設けることができるようになりました。
Java SE 8までの可視性制御 | Java SE 9での可視性制御 |
---|---|
public | 全てにpublic |
- | 特定のモジュールにのみpublic |
モジュールの中だけpublic | |
protected | |
package | |
private |
依存関係の定義と追跡¶
アプリケーションが使用するモジュールと、そのモジュールから依存関係により推移的に依存するモジュールが明らかになります。依存関係を辿ることにより、アプリケーションを実行するために必要なモジュールだけを切り出すことが可能になりました。従来は、アプリケーションを実行するには、JRE(Java Runtime Environment:Java実行環境)全体をマシンに用意するか、アプリケーションと一緒にJREを入れる必要がありました。JREはかなりのサイズとなります。
Java SE 9では、Java実行環境自体がモジュール化されたので、アプリケーションを実行するのに必要となるJava実行環境のモジュールだけを抽出してアプリケーションに含めることで最小限の配布サイズとすることができます。
Java SE 9(初期リリース)では、95のモジュールがあるようです。
JARの構成¶
従来のJARファイルは、パッケージと型を単にまとめたアーカイブに過ぎません。JARを見ただけでは、そのJARが外部に提供するAPI(広義のインタフェース)と、そのJARが必要とするAPIが分かりません。JARの中にあるプログラムをくまなく調べる必要があります。
新たなモジュール(モジュール定義を含んだJARファイル)は、そのモジュールが外部に提供するAPIと、そのモジュールが依存している他のモジュールの情報が提供されています。
モジュールの概要¶
プログラムの構造¶
従来のJavaプログラムの構造は、クラスファイルの集合体でした。クラスファイルはディレクトリ構造の中に格納し、パッケージはクラスファイルが置かれるディレクトリとして表現されます。クラスファイルはファイルシステム上に直置きするか、JARファイルの中に含むかの形態をとっています。
JPMS(Java Platform Module System)が導入されたJava SE 9からは、Javaプログラムの構造はモジュールの集合体となります。クラスファイルはモジュールの中に含まれます。モジュールは、モジュール記述子を含むJARファイルか、JMOD形式のファイルの形態をとります。
JMOD形式のファイルはコンパイルおよびリンク用に提供されるものです。JARファイルと同様ZIPアーカイブ形式で中にクラスファイル、モジュール記述子、リソースファイルの他、ネイティブコードを含むことができます。JMOD形式ファイルを実行時に使用することはできません。
モジュールファイル¶
モジュールファイル(JAR形式)は、JARファイルの中にモジュール記述子(module descripter)が含まれたものです。
モジュール記述子は、module-info.javaに書かれたモジュール定義(module declaration)をコンパイルしたものです。
モジュール定義は、module
キーワードから始まる定義記述です。
module <モジュール名> { }
モジュール名は、公開するパッケージのうちもっとも主となるものにちなんだ命名が推奨されています(JLS 6.1)
このモジュール定義に、
- そのモジュール内のクラスから使用しているAPIが含まれるモジュールへの依存関係
- そのモジュール内のクラスが他のモジュールから使用される場合の公開
等を記述します。
なお、デフォルトでは、すべてのモジュールについてjava.base
モジュールへの依存関係が定義済みとして扱われるので、java.base
に含まれるAPIを使用する際に module-info.java への依存関係定義(requires java.base
)の記述は不要です。
java.util.loggingパッケージを使用する場合の記述例¶
java.util.logging
パッケージは、java.logging
モジュールで提供されています。
そのため、java.util.logging
パッケージを使用するモジュールでは、モジュール定義にjava.util.logging
パッケージを提供しているjava.logging
モジュールへの依存関係を記述しなくてはなりません。記述しない場合は、コンパイルエラーとなります。(なお、後方互換性を保つため、モジュールとしてコンパイルしない場合はエラーとはなりません)
ここで、java.util.logging
パッケージを利用するアプリケーションプログラムとしてcom.torutk.hello.greeting
モジュールを定義し、このアプリケーションプログラムをコンパイルする際に、java.logging
モジュールへの依存関係を記述しなかった場合に生じるコンパイルエラーを次に示します。
import java.util.logging.Logger; (パッケージjava.util.loggingはモジュールjava.loggingで宣言されていますが、モジュールcom.torutk.hello.greetingに読み込まれていません) エラー1個
以下は、アプリケーションプログラムのモジュールcom.torutk.hello.greeting
のモジュール定義です。モジュール定義に依存関係を記述する場合は、requires
を用いて次のようにjava.logging
モジュールへの依存関係を記述します。
module com.torutk.hello.greeting {
requires java.logging;
}
別のモジュールにクラスを公開する場合の記述例¶
com.torutk.hello.greeting
モジュールに含めるcom.torutk.hello.greeting
パッケージを別なモジュールから使用する場合は、モジュール定義にexports
を用いて次のようにcom.torutk.hello.greeting
パッケージを公開する記述をします。
module com.torutk.hello.greeting {
exports com.torutk.hello.greeting;
requires java.logging;
}
- 注)モジュール名を、モジュールに含まれるパッケージの代表名に一致する命名を採用する場合、モジュール名とパッケージ名が重なって混乱します…。
module
キーワードおよびrequiresキーワードに続く識別子はモジュール名exports
キーワードに続く識別子はパッケージ名
module <モジュール名> { exports <パッケージ名>; requires <モジュール名>; }
非モジュールなJARファイルをモジュールとして扱う¶
従来のJARファイル(モジュール定義が含まれないJARファイル)を、モジュールとして扱うAutomatic modulesという仕組みが用意されています。
これによって、すべてのJARファイルがモジュール対応するまでモジュールが使えない、ということを避けることができます。
従来のJARファイルをモジュールパスで指定されるディレクトリに配置します。すると、このJARファイルは遷移的(transitive)にすべてのモジュールに依存するものとして扱われます。また、jlinkには対応できません。
従来のJARは、Automatic moduleの仕組みによって、次のルールで決まるモジュール名となります。
- MANIFEST.MFの
Automatic-Module-Name
属性が定義されていれば、その名前がモジュール名となる - 属性がなければ、JARファイル名から所定のルールでモジュール名が生成される
ファイル名末尾(拡張子)の.jar を除去し、残りのファイル名において、ハイフンと数字からなる以降の文字をバージョン番号として扱い、それ以前の文字をモジュール名(アルファベットと数字以外の文字をピリオドとする)とする
使い方¶
モジュール定義ファイルとソースファイルの配置¶
ビルドツールやjavacを直接使う場合などで幾つかのディレクトリ構成の違いがあります。
javac で複数モジュールを一括コンパイルする際のディレクトリ構成¶
OpenJDKのProject Jigsaw説明 にあるディレクトリ構成です。
一つの基点の下に、ソースファイルを置くsrcディレクトリと、モジュールファイル(ビルド結果)を置くmodsディレクトリを設けます。
それぞれsrcとmodsとの下に、モジュール名に相当するサブディレクトリを作成し、その下にファイル群を置きます。
<開発作業基点> +-- src/ | +-- com.torutk.foo/ | | +-- module-info.java | | +-- com/ | | +-- torutk/ | | +-- foo/ | | +-- Foo.java | +-- com.torutk.bar/ | : +-- module-info.java | +-- com/ | +-- torutk/ | +-- bar/ | +-- Bar.java +-- mods/ +-- com.torutk.foo/ | +-- module-info.class | +-- com/ | +-- torutk/ | +-- foo/ | +-- Foo.class +-- com.torutk.bar/ | +-- module-info.class | +-- com/ | +-- torutk/ | +-- bar/ | +-- Bar.class :
srcディレクトリの下とmodsディレクトリの下にあるピリオドを含むディレクトリ名(例、com.torutk.foo)は、JPMSのモジュール名で、Javaのパッケージ名と必ずしも同じにする必要はありません(この例では同じになっていますが)。
このディレクトリ構成を取ると、javac コマンドで --module-source-path
オプションを指定し一括して複数のモジュールをコンパイルすることが可能です。
モジュール毎にコンパイル¶
モジュール(一つのJARファイルを生成する単位)毎に独立したディレクトリとする構成です。モジュールごとに独立した開発基点を持つので、チーム開発では実践的な構成です。
<開発作業基点> +-- modules/ <-- 成果物(モジュール)のJARファイルを収容するディレクトリ | +-- foo/ <-- モジュール個々の基点ディレクトリ(名前は任意) | +-- bin/ | +-- src/ | +-- module-info.java | +-- com/ <-- パッケージに対応するディレクトリ | +-- torutk/ | +-- foo/ | +-- Foo.java +-- bar/ :
modulesディレクトリ下はJARファイルを配置しています。
NetBeans IDE 9.0 のJava Modular Project¶
モジュール対応のアプリケーションを作成するときに使用します。1つのプロジェクトに複数のモジュールを収容できます。
次は、NetBeans IDEで、Java Module Projectを選択して新規にHelloアプリケーションプロジェクトを作成、中に2つのモジュールcom.torutk.foo
とcom.torutk.bar
を作成した場合のディレクトリ構造です。
Hello +-- build | +-- modules | +-- com.torutk.foo | | +-- com | | | +-- torutk | | | +-- foo | | | +-- Foo.class | | +-- module-info.class | +-- com.torutk.bar | : +-- dist | +-- jlink | | +-- Hello | | +-- bin | | +-- conf | | +-- include | | +-- legal | | +-- lib | +-- com.torutk.foo.jar | +-- com.torutk.bar.jar +-- nbproject +-- src +-- com.torutk.foo | +-- classes | | +-- com | | | +-- torutk | | | +-- foo | | | +-- Foo.java | | +-- module-info.java | +-- tests +-- com.torutk.bar :
- distの下には、ビルドしたJARファイル(モジュール対応)と、実行に必要なJRE(抜粋)が置かれています。
NetBeans IDE 9.0 の既存プロジェクトにモジュール定義追加¶
既存のプロジェクト(例えば、JavaFX プロジェクト)をJava SE 9のモジュール対応とするには、ソースパッケージ直下に、[ファイル]メニュー > [新規ファイル] で、左側ペインで[Java]を選択、右側ペインで[Java Module Info]を選択し、[次へ]ボタンを押します。すると、ソースパッケージ下のデフォルトパッケージ内に、module-info.java が生成されます。
この時点で、java.base
以外のパッケージを使用している個所(import
文)がエラーになります。使用しているパッケージが含まれるモジュールをrequires
で指定します。
module HelloFx {
requires javafx.controls;
requires javafx.graphics;
requires javafx.fxml;
}
モジュールを収容するディレクトリ¶
モジュールを収容するディレクトリには、クラスファイルを配置するかJARファイルを配置することができます。
クラスファイルを置く場合、次のようにディレクトリ構成をする必要があります。
T.B.D.
操作方法¶
参考としてJigsaw試行錯誤 (Java SE 9リリース前の情報)
モジュールの作成(他のモジュールに依存がないもの)¶
モジュールとして作成する場合、ソースツリーのトップディレクトリにはモジュール名のディレクトリを設けます。
foo/ +-- mods/ | +-- src/ +-- com.torutk.foo <-- モジュール名のディレクトリを設ける +-- module-info.java +-- com/ +-- torutk/ +-- foo/ +-- Foo.java
まず、module-info.javaを含めて必要なソースファイルをコンパイルし、クラスファイルを生成します。
foo$ javac -d mods --module-path lib --module-source-path src -m com.tourtk.foo
すると、modsディレクトリ下にモジュール名と同じディレクトリが作られ、その中は次のようにクラスファイルが生成されます。
foo/ +-- mods/ | +-- com.torutk.foo/ | +-- module-info.class | +-- com/ | +-- torutk/ | +-- foo/ | +--Foo.class +-- src/
次に、jarコマンドでモジュールJARファイルを作成します。
foo$ jar --create --file=../modules/com.torutk.foo.jar -C mods/com.torutk.foo .
work/ +-- modules/ | +-- com.torutk.foo.jar +-- foo/ +-- bin/ : +-- src/
モジュールの作成(他のモジュールに依存があるもの)¶
まず、module-info.javaを含めて必要なソースファイルをコンパイルし、クラスファイルを生成します。その際、別のモジュールファイルを参照する必要があるので、モジュールファイルの場所を指定します。
bar$ javac -d bin --module-path ../modules --module-source-path src -m com.torutk.bar
次に、jarコマンドでモジュールJARファイルを作成します。
bar$ jar --create --file=../modules/com.torutk.bar.jar -C bin/com.torutk.bar .
実行可能なモジュールを作成する場合は、main-classの指定を追加します。
bar$ jar --create --file=../modules/com.torutk.bar.jar --main-class=com.torutk.bar.Bar -C bin/com.torutk.bar .
プログラムを実行するコマンドは次です。
work$ java --module-path modules -m com.torutk.bar/com.torutk.bar.Bar
main-classを指定して作成したモジュールJARファイルの場合は次のとおりモジュール名を指定して実行可能です。
work$ java --module-path modules -m com.torutk.bar
モジュールのメインクラス指定は、マニフェストのMain-Class
属性指定とは異なります。jar
コマンドで--main-class
オプションを指定した場合、MANIFEST.MFのMain-Class
属性にメインクラスが設定されますが、それだけでなく、module-info.classにModuleMainClass
属性を書き込みます。javaコマンドの-m
オプションでメインクラスを省略した場合、そのモジュールJARファイルに含まれるmodule-info.classのModuleMainClass
属性からメインクラス情報を取り出して使用します。もしmodule-info.classにModuleMainClass
属性が定義されていない場合(つまり、jar
コマンドで--main-class
オプションを指定しなかった場合)は、例えMANIFEST.MFにMain-Class
属性を記述していても実行時にエラーとなります。
既存のクラスファイル・JARファイルが必要とするモジュールを調べる¶
jdeps
コマンドで、クラスファイルやJARファイルに必要なモジュールを確認することができます。
$ jdeps -s SimpleDemo.class SimpleDemo.class -> java.base SimpleDemo.class -> javafx.controls SimpleDemo.class -> javafx.graphics
実行に必要なモジュールの抜粋¶
作成したプログラムに必要なJREモジュール、ライブラリ、コマンドを抜粋します。従来はJavaプログラムを実行するにはJRE全体が必要でした。JREは、JRE 8u102 Windows 64bit版の場合で176MBを占めます。また、JDK 8でネイティブバンドルで作成したJRE込みプログラムでも166MBのサイズとなります。
JPMSの導入により、プログラムが必要とするJREのモジュールだけを抜粋することで、実行に必要なファイル群のサイズが削減できます。
C:\work> jlink --module-path module;"C:\Program Files\Java\jdk-9\jmods" --add-modules com.torutk.bar --output bundle :
jlink
コマンドで、JPMSモジュール格納パスを指定します。格納パスは、アプリケーションのモジュールを格納している ./modulesディレクトリと、JDK/JRE 9のJPMSモジュールを格納しているパスを指定します。--output
で指定した場所に、実行に必要なファイルと抜粋したモジュールファイルが生成されます。
work/ +-- bundle/ +-- bin/ | +-- java.dll | +-- java.exe : : +-- conf/ | +-- net.properties | +-- security/ | | +-- java.policy | | +-- java.security | +-- sound.properties +-- lib +-- amd64/ | +-- jvm.cfg +-- fontconfig.bfc +-- fontconfig.properties.src +-- fonts/ | +-- LucidaBrightDemiBold.ttf : : +-- modules :
bundleの下に、javaコマンドやmodulesファイルがあります。modulesファイルにはJPMSモジュールファイルが格納されています。
今回の例では、bundleディレクトリ以下の容量は79MBとなっていました。
実行は次のコマンドです。
C:\work> bundle\bin\java -m com.torutk.bar/com.torutk.bar.Bar
抜粋したbundleディレクトリ以下を別なマシンへコピーして、上述コマンドで実行したところ、プログラムが立ち上がりました。
仕様調査¶
モジュール定義ファイル(module-info.java)の仕様¶
javacは、モジュール定義ファイルをコンパイルするときのみモジュール用の予約語を認識します。
module M.N { ... }
open module M.N { ... }
1. は通常のモジュールで、コンパイル時および実行時にアクセス許可を行う
2. はオープンモジュールで、コンパイル時にアクセス許可を行うが実行時はすべてのパッケージが公開されたものとして扱う
モジュール定義内の記述は次の通りです。
requires <モジュール>
当該モジュールが依存するモジュールを記述する。requires transitive <モジュール>
当該モジュールに依存する(使用する)モジュールが暗黙的に依存するモジュールを記述する。
使用するモジュールには、当該モジュールへの依存を記述すればよく、このモジュールへの依存は記載しなくてよい。requires static <モジュール>
requires transitive static <モジュール>
exports <モジュール>
他のモジュールに公開(export
)するパッケージを記述する。
他のモジュールからは、コンパイル時および実行時に、public
およびprotected
な型、ならびにそれらのpublic
およびprotected
なメンバーに対してアクセスを許す。また、リフレクションを使ったそれらの型およびメンバーに対するアクセスも許す。exports <モジュール> to <モジュール'>, <モジュール''>
opens <モジュール>
他のモジュールに開放(open
)するパッケージを記述する。
他のモジュールからは、実行時に(コンパイル時は除く)、public
およびprotected
な型、ならびにそれらのpublic
およびprotected
なメンバーに対してアクセスを許す。また、リフレクションを使ったパッケージ内のすべての型とそのすべてのメンバーに対するアクセスを許す。opens <モジュール> to <モジュール'>, <モジュール''>
uses <モジュール>
provides <モジュール> with <モジュール'>, <モジュール''>
requires java.base
が暗黙で定義される(記述不要)opens
で指定したパッケージは実行時においてのみ他のモジュールからアクセス可能。すべての型およびメンバーはリフレクションを通じてアクセス可能。
その他¶
Java SEの標準モジュール¶
javaコマンドのオプション--list-modules
で、Java SE の標準モジュールが一覧できます。
$ java --list-modules java.activation@9 java.base@9 java.compiler@9 java.corba@9 :
問題など¶
JARファイル構成¶
MANIFEST.MFの情報が読めない?¶
JARファイルのMANIFEST.MFに、Implementation-Versionを記載し、プログラム内で、Main.class.getPackage().getImplementationVersion()
を呼びます。
package com.torutk.hello;
public class HelloJar {
private static void showImplimentationVersionFromPackage() {
String version = HelloJar.class.getPackage().getImplementationVersion();
System.out.println("package getImplementationVersion = " + version);
}
public static void main(String[] args) {
showImplimentationVersionFromPackage();
}
}
% cat META-INF/MANIFEST.MF Manifest-Version: 1.0 Implementation-Version: 1.0.1
- JARファイルを、モジュール方式で起動した場合
java -p build/libs -m com.torutk.hello
getImplementationVersion()はnullを返しました。
- 同じJARファイルを、クラスパス方式で起動した場合
java -cp build/libs/hello-1.0.1.jar com.torutk.hello.HelloJar
getImplementationVersion()は、MANIFEST.MFに記載のテキストを返しました。
対処¶
クラスローダーからMETA-INF/MANIFEST.MFのInputStreamを取得し、Manifestクラスに渡して属性を取得します。
import java.io.InputStream;
import java.util.jar.Manifest;
:
InputStream is = HelloJar.class.getClassLoader().getResourceAsStream("META-INF/MANIFEST.MF");
Manifest mf = new Manifest(is);
String version = mf.getMainAttributes().getValue("Implementation-Version");
- クラスから直接getClassLoaderで良いか、Module経由でclassLoaderを取得する必要があるか要調査
参考資料¶
日本語¶
Java Magazine 日本語訳¶
http://www.oracle.com/technetwork/jp/articles/java/overview/index.html
ITPro連載「最新Java情報局」(櫻庭祐一氏)¶
- 2016-04-07 JDK統合で試しやすくなった、Java SE 9の注目機能「Project Jigsaw」
- 2016-04-21 Java SE 9を先取り、Project Jigsawでモジュールを作成する
- 2016-05-12 Java SE 9、Project Jigsawの標準モジュールと依存性の記述
- 2016-05-26 Java SE 9、Project Jigsawにおけるpublic
- 2016-06-09 Java SE 9、Project Jigsawにおける後方互換性
- 2016-06-24 Project JigsawによるJREのカスタマイズ
注)Java SE 9がリリースされたのが2017年9月ですが、記事はその1年前時点の仕様案に基づく記述ですので、正式仕様と違う箇所が含まれています。
セミナー資料¶
英語¶
仕様¶
OpenJDK資料¶
JavaOne 2017セッション¶
セッションの動画がYoutubeで公開されています。
- Modules in One Lesson(動画)
- Migrating to Modules(動画)
- Modular Development with JDK 9
- Modules and Services(動画)
書籍¶
- Parlog, Nicolai. 2019. The Java Module System. Shelter Island:Manning Publications.
- Mak, Sander; and Bakker, Paul. 2017. Java 9 Modularity. Sebastopol:O'Reilly