【Modern Java】Java Platform Module System (JPMS)

blog1.mammb.com


JSR 376: JavaTM Platform Module System

Project Jigsaw の成果として Java 9 で Java Platform Module System (JPMS) が導入されました。

モジュールシステムによる目標は以下となります。

  • 信頼性の高い設定

    • 脆弱でエラーが発生しやすいクラスパスメカニズムを、プログラムコンポーネントが互いに明示的に依存関係を宣言する手段に置き換える
  • 強力なカプセル化

    • コンポーネントが、どのAPIが他のコンポーネントからアクセス可能で、どのAPIがそうでないかを宣言できるようにする
  • 拡張性の高いJava SEプラットフォーム

    • コンポーネントをアプリケーションで実際に必要とされる機能のみを含むカスタム構成に組み立てることができる
  • プラットフォームの整合性向上

    • プラットフォーム内には外部公開を想定していないクラスが多数あり、これらは内部APIとして隠蔽される
  • パフォーマンスの向上

    • 完全なプラットフォーム構成に基づく全体の最適化技術を適用することでパフォーマンスを向上する

モジュールシステムでは、パッケージの上位にモジュールという区分けを作り、モジュールに名前を付与します。 モジュールはモジュール間の依存関係を規定や明示的なパッケージの公開などを規定することで、モジュールの相互運用性を厳密に定義します。

Java 9 以前ではクラスレベルで行っていたインポート宣言やアクセス修飾子による公開性をパッケージレベルで行うことが可能になる というイメージになります。


モジュール宣言

モジュールの宣言は、パッケージのルートに module-info.java というファイルを作成して定義します。

Gradle プロジェクトの場合は src/main/java/module-info.java として作成することになるでしょう。

複数のモジュールを一つのツリー上に作成することはできません。複数のモジュールを作成する場合はディレクトリを分ける必要があります。

一例として java.naming モジュールの module-info.java は以下のような定義になっています。

module java.naming {
    requires java.security.sasl;

    exports javax.naming;
    exports javax.naming.directory;
    exports javax.naming.event;
    exports javax.naming.ldap;
    exports javax.naming.spi;
    exports javax.naming.ldap.spi;

    exports com.sun.jndi.toolkit.ctx to
        jdk.naming.dns;
    exports com.sun.jndi.toolkit.url to
        jdk.naming.dns,
        jdk.naming.rmi;

    uses javax.naming.ldap.StartTlsResponse;
    uses javax.naming.spi.InitialContextFactory;
    uses javax.naming.ldap.spi.LdapDnsProvider;

    provides java.security.Provider with
        sun.security.provider.certpath.ldap.JdkLDAP;
}

module <moduleName> {} の形でモジュール名を定義します。 モジュール名には任意の名前を付けることができますが、慣例として代表パッケージ名に合わせます。

モジュール宣言の module-info.java はコンパイルにより モジュール記述子 module-info.class となり Jar などに含まれることになります。


モジュールの定義

module-info.java には、requiresexports などのモジュール・ディレクティブにより依存モジュールと公開パッケージを指定します。

主なディレクティブは以下となります。

ディレクティブ 説明
requires <moduleName> このモジュールが依存する他のモジュール名を指定
requires transitiv <moduleName> 依存するモジュールを指定するとともに、そのモジュールが依存する先のモジュールも暗黙裡に依存を定義
requires static <moduleName> コンパイル時には必要だが、実行時には不要な依存を指定
exports <packageName> このモジュールが外部に公開するパッケージを指定
exports <packageName> to <moduleName> 公開するパッケージを公開先モジュールを限定して指定
opens <packageName> 外部からリフレクションによる参照を許可するパッケージを指定
opens <packageName> to <moduleName> リフレクションによる参照を許可するパッケージを公開先モジュールを限定して指定

大抵の場合は requiresexports だけ覚えておけば事足ります。


opens については、以下のようにすることで、モジュール全体をリフレクションにより参照可能として定義することもできます。

opens module <moduleName> {
    exports ...;
}


ServiceLoader の依存関係

requiresexports などのモジュール・ディレクティブの他に、providesuses というモジュール・ディレクティブもあります。

providesuses では ServiceLoader の依存関係を定義します。

provides でサービス提供のインターフェース(jdk.internal.logger.DefaultLoggerFinder) とその実装クラス(sun.util.logging.internal.LoggingProviderImpl)を定義します。

module java.logging {
    exports java.util.logging;

    provides jdk.internal.logger.DefaultLoggerFinder with
        sun.util.logging.internal.LoggingProviderImpl;
}

利用側ではサービスのインターフェースを指定することで、サービス提供側で定めた実装が利用できます。

module java.base {
    // ...
    uses jdk.internal.logger.DefaultLoggerFinder;
    // ...
}


システムモジュール

--list-modules でシステムモジュールを一覧できます。

$ java --list-modules
java.base@14.0.2
java.compiler@14.0.2
java.datatransfer@14.0.2
java.desktop@14.0.2
java.instrument@14.0.2
java.logging@14.0.2
java.management@14.0.2
java.management.rmi@14.0.2
java.naming@14.0.2
java.net.http@14.0.2
java.prefs@14.0.2
java.rmi@14.0.2
java.scripting@14.0.2
java.se@14.0.2
java.security.jgss@14.0.2
java.security.sasl@14.0.2
java.smartcardio@14.0.2
java.sql@14.0.2
java.sql.rowset@14.0.2
java.transaction.xa@14.0.2
java.xml@14.0.2
java.xml.crypto@14.0.2
jdk.accessibility@14.0.2
jdk.aot@14.0.2
... 中略
jdk.xml.dom@14.0.2
jdk.zipfs@14.0.2

この中で、java.base モジュールは標準APIの基盤となるため、明示的に requires しなくとも、暗黙的に requires したものとして扱われます。


Automatic Module(自動モジュール) と Unnamed Module(無名モジュール)

旧来の Java では classpath からクラスのロードが行われました。 Java 9 からは module-path からモジュールのロードが行われます。module-path は javac コマンドおよび java コマンドのオプション --module-path (-p) で指定します。

Java 9 でも classpath は存在しますが、モジュールからは classpath を参照することができません。そこで、モジュールと非モジュールの相互運用を考える必要が出てきます。

非モジュールをモジュールシステムの上で扱うための仕組みが Automatic Module と Unnamed Module です(これに対してモジュールを named module と呼びます)。


Automatic Module は、モジュールパスで読み込まれたモジュール定義の無い Jar です。

全てのパッケージが exports 扱いとなり、モジュールグラフ上のすべてのモジュールを requires している扱いになります。

モジュール名は MANIFEST.MF の Automatic-Module-Name 属性が定義されていれば、その名前がモジュール名となります。この定義が無い場合は Jar ファイル名から自動的に決定されます。

現時点の commons-lang3 などは Automatic-Module-Name 属性を定義することでモジュール対応が行われています。Gradle の場合は以下のように指定します。

tasks.named<Jar>("jar") {
    manifest {
        attributes(
            "Automatic-Module-Name" to "xxxx"
        )
    }
}


Unnamed Module は、classpath から読み込まれたパッケージや型です。

Automatic Module と同様に、全てのパッケージが exports 扱いになり、モジュールグラフ上のすべてのモジュールを requires している扱いになります。

Unnamed Module は全てのパッケージが exports 扱いとなりますが、それを named module から参照することはできません。Automatic Module からは参照することができます。


Gradle における Java Platform Module System

Gradle における Java Platform Module System の運用については以下を参照してください。

blog1.mammb.com