プロジェクト

全般

プロフィール

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の仕組みによって、次のルールで決まるモジュール名となります。

  1. MANIFEST.MFのAutomatic-Module-Name属性が定義されていれば、その名前がモジュール名となる
  2. 属性がなければ、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.foocom.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は、モジュール定義ファイルをコンパイルするときのみモジュール用の予約語を認識します。

  1. module M.N { ... }
  2. 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情報局」(櫻庭祐一氏)

注)Java SE 9がリリースされたのが2017年9月ですが、記事はその1年前時点の仕様案に基づく記述ですので、正式仕様と違う箇所が含まれています。

セミナー資料

英語

仕様

OpenJDK資料

JavaOne 2017セッション

セッションの動画がYoutubeで公開されています。

書籍

  • Parlog, Nicolai. 2019. The Java Module System. Shelter Island:Manning Publications.
  • Mak, Sander; and Bakker, Paul. 2017. Java 9 Modularity. Sebastopol:O'Reilly


約2ヶ月前に更新