Javaのモジュールシステムとは

Javaのモジュールシステムに関する議論がスタートしたのは15年ほど前のことになる。Javaアプリケーションの多様化やJava言語仕様の巨大化によって,従来のパッケージの仕組みだけではクラスライブラリの適切な構造化や管理が難しくなったというのがその発端だ。さまざまなライブラリのJarファイルが複雑に依存し合っているこの状況は「Jar地獄」などと呼ばれ、Java 9のリリースに到るまで問題視され続けてきた。

Java 9に導入されたモジュールシステムは、「Project Jigsaw」というプロジェクト名で仕様策定と実装が進められた。Java Community Processにおける正式なJSRは「JSR 376: Java Platform Module System」で、OpenJDKプロジェクトではJEP 200を中心とした複数のJEPによって構成されている。

Jigsawによるモジュールシステムでは、複数のパッケージを関連するもの同士でグループ化し、再利用可能なモジュールとして管理することができる。具体的には、各モジュールごとに次のような設定を行うことが可能になる。

  • モジュール間の依存関係の明確化: モジュール同士の依存性を明示的に宣言するメカニズムが提供されるため、コンパイル時と実行時のそれぞれで依存性を検証できる。
  • モジュールの公開範囲の設定: モジュール内のパッケージは、明示的にエクスポートされた場合のみ、ほかのモジュールからアクセスできる。

Java 9以降のJavaでは、このモジュールシステムがJDKの標準ライブラリに対しても適用された。Javaのコア・プラットフォームはjava.baseモジュールをはじめとする複数のモジュールに分割され、それぞれの依存関係や公開範囲が明確化されている。

次の図はJava 9におけるJava SEプラットフォームのモジュールの依存関係をまとめたものだ。青い矢印は後述する「requires transitive」宣言による推移的な依存性を表しており、グレーの矢印は「requires」宣言による直接的な依存性を表している。また、java.base モジュールだけは特別な存在で、このモジュールには java.lang をはじめとする Java プログラムを実行するうえで不可欠なパッケージが含まれている。そのため、このモジュールに対してはすべてのモジュールが自動的に requires 宣言しているものとして扱われる。

  • Java SEのコア・プラットフォーム・モジュール(Java 9)

    Java SEのコア・プラットフォーム・モジュール(Java 9)

上の図の赤枠で囲まれている部分はJava EEに由来するAPIがまとめられたモジュールだが、これはJava 9で非推奨となっており、Java 11で正式に削除された。一方で、java.net.httpとjava.transaction.xaという2つの新しいモジュールが追加されている。その結果、Java 11のモジュールの依存関係は次の図に示すような形になっている。

  • Java SEのコア・プラットフォーム・モジュール(Java 11)

    Java SEのコア・プラットフォーム・モジュール(Java 11)

※より詳細は公式のAPIドキュメントを参照のこと。

このモジュール化の影響で、Java 8以前とJava 9以降では標準ライブラリの構成が大きく異なっている。したがって、Java 8以前に作られたシステムを移行する際には慎重に互換性を確認する必要がある。

モジュールを使ったプログラムの例

それでは、早速Javaのモジュールシステムを使ってみよう。今回は、「jp.mynavi.greeting」というモジュールと、それを利用する「jp.mynavi.hello」というモジュールの2つを作ってみる。モジュール名はグローバルに一意でなくてはならないため、パッケージ名と同様に組織のドメイン名を逆順にして使うのが慣例となっている。

ソースツリーはモジュール毎に分けて構成するので、今回のケースでは作業ディレクトリの下に次のように2つのディレクトリを用意した。

.
├── jp.mynavi.greeting
└── jp.mynavi.hello

jp.mynavi.greetingモジュールの作成

jp.mynavi.greetingディレクトリ以下のファイル構成は次のようになる。ソースコードはsrcディレクトリ以下にパッケージ構成にしたがって配置する。この例では jp.mynavi.imajava.mylib とjp.mynavi.imajava.hidden というパッケージ名にしているが、ここは各自のパッケージ名に応じてディレクトリを構成して欲しい。

jp.mynavi.greeting/
└── src
    ├── jp
    │   └── mynavi
    │       └── imajava
    │           ├── hidden
    │           │   └── HiddenGreeting.java
    │           └── mylib
    │               └── Greeting.java
    └── module-info.java

まず、 Greeting.java は次のようにした。static な hello() メソッドがひとつ定義してある。

Greeting.java
package jp.mynavi.imajava.mylib;

public class Greeting {
    public static String hello(String name) {
        return "Hello " + name + "!";
    }
}

HiddenGreeting.java は、パッケージが異なるだけで Greeting.java とほぼ同じである。

HiddenGreeting.java
package jp.mynavi.imajava.hidden;

public class HiddenGreeting {
    public static String hello(String name) {
        return "(Hidden) Hello " + name + "!";
    }
}

モジュールに関する設定は、srcディレクトリ直下に module-info.java という名前のファイルを作って記述する。ファイル名からも分かるように、これもJavaプログラムの一種である。jp.mynavi.greeting モジュールの設定は次のようになっている。

jp.mynavi.greetingのmodule-info.java
module jp.mynavi.greeting {
    exports jp.mynavi.imajava.mylib;
}

「module」はモジュールを宣言するためのキーワードである。moduleキーワードに続けてモジュール名を記述し、波括弧内にモジュールの設定を記述するのが、モジュール宣言の基本形になる。

モジュール宣言の基本形
module モジュール名 {
}

「exports」は、モジュールに含まれるパッケージに対して、ほかのモジュールのコードからアクセスできることを指定する宣言になる。関連する宣言に「exports…to」があり、これはto以降で指定したモジュールに対してのみアクセスを許可するという意味になる。今回の例の場合、 jp.mynavi.imajava.mylib パッケージのみ外部からのアクセスを許可しており、 jp.mynavi.imajava.hidden パッケージは公開していない。

コンパイルは通常通り javac コマンドで行うが、 module-info.java を含めるのを忘れないようにしよう。次の例のようにコンパイルすると、mods ディレクトリ以下にclassファイルが生成される。長いコマンドなので見やすいように「\」で区切っているが、一行で記述することもできる。

$ javac -d mods \
> src/module-info.java \
> src/jp/mynavi/imajava/mylib/Greeting.java \
> src/jp/mynavi/imajava/hidden/HiddenGreeting.java

あとで利用しやすいようにJarファイルも作成しておこう。次の例のようにすれば、jars ディレクトリ以下に greeting.jar というファイルが生成される。

$ mkdir jars
$ jar -c -f jars/greeting.jar -C mods .

コンパイルおよびJarファイルの生成が完了した後の jp.mynavi.greeting ディレクトリ以下の構成は次のようになる。

jp.mynavi.greeting/
├── jars
│   └── greeting.jar
├── mods
│   ├── jp
│   │   └── mynavi
│   │       └── imajava
│   │           ├── hidden
│   │           │   └── HiddenGreeting.class
│   │           └── mylib
│   │               └── Greeting.class
│   └── module-info.class
└── src
    ├── jp
    │   └── mynavi
    │       └── imajava
    │           ├── hidden
    │           │   └── HiddenGreeting.java
    │           └── mylib
    │               └── Greeting.java
    └── module-info.java

jp.mynavi.helloモジュールの作成

続いて、jp.mynavi.hello モジュールの方を作っていこう。jp.mynavi.hello ディレクトリの構成は次のようになる。

jp.mynavi.hello/
└── src
    ├── jp
    │   └── mynavi
    │       └── imajava
    │           └── hello
    │               └── Hello.java
    └── module-info.java

Hello.java は、Greeting クラスの hello() メソッドを呼び出す。

Hello.java
package jp.mynavi.imajava.hello;

import jp.mynavi.imajava.mylib.Greeting;

public class Hello {
    public static void main(String[] args) {
        String str = Greeting.hello("MYNAVI");
        System.out.println(str);
    }
}

したがって、このモジュールは Greeting クラスが定義されている jp.mynavi.greeting モジュールの jp.mynavi.imajava.mylib パッケージに依存するということなので、そのための設定を module-info.java に記述しておかなければいけない。 module-info.java は次のようになる。

jp.mynavi.helloのmodule-info.java
module jp.mynavi.hello {
    requires jp.mynavi.greeting;
}

requires は、このモジュールが指定されたモジュールに依存することを宣言している。あるモジュールAが別のモジュールBを必要とする場合、モジュールAでは requires 宣言によってモジュールBへの依存関係を明示しなくてはいけない。

さて、モジュールAがモジュールBに依存し、モジュールBがモジュールCに依存しているケースを考えてみる。この場合、モジュールAは直接モジュールCを requires しているわけではないので、モジュールCにアクセスすることはできない。ただし、モジュールBの module-info.java の中で、モジュールCを「requires transitive」を使って依存性を宣言していた場合に限って、モジュールAからモジュールCへのアクセスが可能になる。

さて、jp.mynavi.hello モジュールをコンパイルしよう。次のようにコンパイルすることで、mods ディレクトリ以下に各classファイルが生成される。

$ javac -d mods \
> --module-path ../jp.mynavi.greeting/jars/greeting.jar \
> src/module-info.java \
> src/jp/mynavi/imajava/hello/Hello.java

このモジュールは jp.mynavi.greeting モジュールを必要とするため、コンパイル時には --module-path オプションを使ってその場所を指定している。 javac コマンドや java コマンドがモジュールの走査を行うパスのことをモジュールパスと呼び、--module-path オプションは指定されたパスをモジュールパスに追加するためのものだ。

もし --module-path の指定をしなかった場合、jp.mynavi.greeting モジュールが発見できないため次の例のようにコンパイルエラーになる。

$ cd jp.mynavi.hello
$ javac -d mods \
> src/module-info.java \
> src/jp/mynavi/imajava/hello/Hello.java
src/module-info.java:2: エラー: モジュールが見つかりません: jp.mynavi.greeting
    requires jp.mynavi.greeting;
                      ^
エラー1個

続いて、Jarファイルを作成しよう。

$ mkdir jars
$ jar -c -f jars/hello.jar -C mods .

これで、jars ディレクトリの下に hello.jar が生成されたはずだ。ここまで完了した状態のファイル構成は次のようになっている。

jp.mynavi.hello/
├── jars
│   └── hello.jar
├── mods
│   ├── jp
│   │   └── mynavi
│   │       └── imajava
│   │           └── hello
│   │               └── Hello.class
│   └── module-info.class
└── src
    ├── jp
    │   └── mynavi
    │       └── imajava
    │           └── hello
    │               └── Hello.java
    └── module-info.java

ちなみに、jp.mynavi.hello モジュールが jp.mynavi.greeting モジュールに依存することは明示されているが、jp.mynavi.greeting が exports しているのは jp.mynavi.imajava.mylib パッケージだけなので、依然として jp.mynavi.imajava.hidde パッケージにアクセスすることはできない。

たとえば Hello.java で次の例のように HiidenGreeting クラスを利用しようとした場合にはコンパイルエラーになる。

jp.mynavi.imajava.hiddenパッケージにアクセスしようとした場合
package jp.mynavi.imajava.hello;

import jp.mynavi.imajava.hidden.HiddenGreeting;

public class Hello {
    public static void main(String[] args) {
        String str = HiidenGreeting.Hello("MYNAVI");
        System.out.println(str);
    }
}

jp.mynavi.hello モジュールの実行

それでは、完成したプログラムを実行してみよう。java コマンドを次のようなオプションで実行すれば、Hello クラスの main() メソッドが実行されて「Hello MYNAVI!」という出力を得られる。

$ java --module-path ../jp.mynavi.greeting/jars/greeting.jar:jars/hello.jar \
> --module jp.mynavi.hello/jp.mynavi.imajava.hello.Hello
Hello MYNAVI!

まず、jp.mynavi.greeting と jp.mynavi.hello の2つのモジュールを使うので、greeting.jar と hello.jar の2つのJarファイルを --module-path オプションでモジュールパスに加える。

次に、このプログラムの起点となるモジュールを指定する必要がある。これはモジュールはルート・モジュールと呼ばれるもので、java コマンドのアプリケーションランチャーはこのルート・モジュールからモジュールの依存関係を走査していく。ルート・モジュールの指定は --module オプションで行う。

本稿の例の場合、ルート・モジュールは jp.mynavi.hello である。さらに、モジュール名に続けて「/クラス名」(main()メソッドを持ったクラスの、パッケージ名付きの名称)と記述することで、最初に実行されるクラスを指定することができる。

今回は基本的なJavaのモジュールシステムの使い方について解説した。次回は、この新しいモジュールシステムと従来のパッケージシステムとの関係や、両者の共存のさせ方などについて紹介する。