0
0

Java モジュールシステム

Last updated at Posted at 2024-07-26

Java 11 Gold 取得に向けた学習記録

モジュールシステム

複数のパッケージをまとめる仕組みをモジュールシステムという。

定義

モジュールの定義は module-info.java(モジュール定義ファイル)に記述する。このファイルに記述する情報は モジュールディレクティブ という。

module-info.java
module モジュール名 {
    exports 公開するパッケージ名;
    requires このモジュールが依存する別のモジュール名;
}

module-info.javaを配置したフォルダはモジュールとして扱われる。

パッケージ内ではクラス単位でアクセス制御を行うのに対して、モジュールシステムではパッケージ単位でアクセス制御を行うことができる。

モジュール名

モジュール名はパッケージと同様に一意であることが必要。そのため他のモジュール名と被らない名前にする必要がある。

通常は、モジュールに含めるパッケージ名に共通の文字列があれば、その部分をモジュール名として使用する。共通部分がない場合は、モジュールの目的を表現する名前にする事が望ましい。

ちなみにモジュール名とパッケージ名は重複しても良い。

ただし、ドット「.」で区切ったパッケージがフォルダ階層にマッピングされるのに対して、モジュール名にドット「.」を含めてもフォルダ階層にマッピングされない。そのため、モジュールを配置するフォルダ名はキャメルケースなどで命名する、もしくはmodsなどのプロジェクト毎に変わらない名前にする。

モジュールディレクティブ

モジュール宣言で使用するrequiresexportsprovideswithusesopensprovides キーワードなどは モジュールディレクティブ と呼ばれる。

requires

自身のモジュールが利用(依存)する外部のモジュール名を指定する。

requires
module my.module {
    requires another.module;
}

requires transive

transitive=推移的

依存するモジュールがさらに依存するモジュールについても暗黙的に含めることができる。モジュールグラフで表現するとこちら

moduleA
module moduleA {
    requires transive moduleB; // 暗黙的に moduleC にも依存する
}
moduleB
module moduleB {
    requires moduleC;
}
moduleC
module moduleC {

}

exports

自身のモジュール内で外部に公開するパッケージ名を指定する。

exports
module my.module {
    exports com.example.mypackage;
}

provides / with

モジュールが特定の サービス の実装を提供する サービスプロバイダ であることを宣言する。プログラム実行時に サービスローダ が動的にサービスプロバイダをロードする際に、この情報が検索時のヒントとして利用される。

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-info.java
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-info.java(別のサービスプロバイダを提供するモジュール)
module my.another.module {
    requires my.module; // サービスの定義を含むモジュール
    provides com.example.MyServiceInterface with com.another.example.impl.AnotherServiceImpl;
}

サービスローダ

module-info.javaprovideディレクティブによって指定されたサービスプロバイダ(サービスを実装したクラス。MyServiceImplAnotherServiceImpl)を、プログラムの実行時に動的に探し出し、ロードする機能を持つものを サービスローダ という。

動的に検索したいサービスは、usesディレクティブで指定する必要がある。

uses
module consumer.module {
    uses com.example.MyServiceInterface; // 動的にロードしたいサービス
}

サービスローダの機能はjava.util.ServiceLoader から提供されている。

サービスローダはサービスプロバイダ(サービスの実装クラス)をロードして、メソッドを実行するためのフレームワークを提供している。

ロードはload()メソッドによって行う。

public static <S> ServiceLoader<S> load​(Class<S> service)

ServiceLoader.load()
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キーワードはモジュールが特定のサービスを使用することをサービスローダ伝えることができる。サービスローダはこの情報を基に、サービスの実装を動的に取得する。

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> とすることで、リフレクションを許可するモジュールを限定することができる。

opens to
module moduleA {
    opens com.example.mypackage to moduleB;
}

モジュール全体をリフレクションの対象とすることもできる。

opens(モジュール全体)
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 以降では、通常はモジュール宣言が必要なため、名前つきモジュールから無名モジュールを参照することはできない。

名前つきモジュールから無名モジュールへはアクセスできない

モジュールグラフ

モジュール間の依存関係を表現した図をモジュールグラフと言う。

moduleAmoduleB に依存し、moduleBmoduleC に依存している場合、モジュールグラフは以下のようになる。

module.png

この状況で moduleBmoduleC への依存を moduleA に伝播させることができる。

モジュールの依存を伝播させるためにはtransitiveを使用する。

module-info.java(moduleB)
module moduleB {
    requires transitive moduleC;
}

これによりモジュールAはrequires moduleCを記述しなくても直接モジュールCが使用できるようになる。

この時、moduleAmoduleC には推移的な依存関係があると表現される。

module_2.png

  • モジュール名: 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ファイル化の構文
jar --create \
--file-name JARファイル名 \
--main-class エントリポイント \
-C JARファイルにするフォルダパス
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形式化する構文
jmod create \
--class-path クラスパス \
出力JMODファイル名 モジュール名
モジュールをJMOD形式化する
jmod create \
--class-path mods/abc \
abc.jmod a.b.c

情報を確認する1

モジュールが公開するパッケージや依存するモジュールを調べる方法。

モジュールの情報を確認する構文1
java \
--module-path コンパイル後のモジュールのルートディレクトリ \
--describe-module モジュール名
モジュールの情報を確認する1
java \
-module-path mods \
--describe-module a.b.c

情報を確認する2

対象のモジュールがJMOD形式の時の使用できる、モジュールが公開するパッケージや依存するモジュールを調べる方法。

モジュールの情報を確認する構文2
jmod describe モジュール名
モジュールの情報を確認する2
jmod describe a.b.c

情報を確認する3(依存関係のみ調べる)

jdepsコマンドを使うと、モジュールに限らずJARファイルやクラス同士の依存関係を調べることができる。

JARの依存関係を調べる
jdeps JARファイル名
モジュールの依存関係を調べる構文
jdeps --module-path コンパイル後のモジュールのルートディレクトリ -module モジュール名
モジュールの依存関係を調べる構文
jdeps --module-path mods -module a.b.c

参考

0
0
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
0
0