🍍

2.1 Javaモジュールシステム(モジュール定義ファイル、モジュールパス、自動モジュール、無名モジュールなど)Java Advanced編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、斉藤賢哉と申します。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Advanced編』の一部の範囲をカバーしたものです。『Java Advanced編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの基本的なスキルを習得済みで、さらなるレベルアップを目指している方
  • 将来的なキャリアとして、希少性の高い上級エンジニアやアーキテクトを志向している方
  • フリーランスエンジニアとして付加価値の更なる向上を図っている方
  • 「Oracle認定Javaプログラマ」の資格取得を目指している方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

2.1 Javaモジュールシステム

チャプターの概要

このチャプターでは、ライブラリの考え方や、モジュールを管理するための「モジュールシステム」と呼ばれる仕組みについて学びます。

2.1.1 Javaモジュールシステムの概要

モジュールとは

モジュールとは一般的に、複数のプログラムを何らかの機能に応じて1つにまとめたもの、という意味です。
Javaには、モジュールを管理するための「モジュールシステム」と呼ばれる仕組みが、言語仕様として備わっています。Javaにおけるモジュールはパッケージの上位概念にあたり、複数のパッケージをさらに1つにまとめたものです。Javaには当初このような仕組みはなく、比較的後のバージョン、具体的にはJava 9において導入されました。
それではなぜ、モジュールという概念を、仕組みとして導入する必要があったのでしょうか。その必要性を理解する上で、まず前提としてライブラリとアプリケーションの違いを説明します。

アプリケーションとライブラリの関係

本コースでは便宜上、ライブラリもアプリケーションも、広い意味でモジュールの一環である、と位置付けることにします。
まずライブラリとは、機能としての汎用性が高く、部品として他のプログラムから呼び出されることを前提にしたソフトウェアです。
それに対してアプリケーションとは、特定の業務仕様を実現するために開発されたソフトウェアです。アプリケーションの開発では、何らかのライブラリを利用することによって、開発を効率化するのが一般的です。またライブラリ自体も、必要に応じて他のライブラリを呼び出して利用するケースが大半です。
このようにライブラリとアプリケーションには、呼び出す側、呼び出される側という点において、依存関係がある、という点を理解する必要があります。
例えば以下の図では、アプリケーションAはライブラリXとYに依存しており、同じようにアプリケーションBもライブラリYに依存しています。そしてライブラリXとYもまた、他のライブラリであるZに依存している、という関係になっていることを表しています。

【図2-1-1】アプリケーションとライブラリの関係
image.png

モジュールシステム導入前の課題

モジュールシステム導入前は、ライブラリを開発したり配布したりする上で、幾つかの課題がありました。

アクセス制御の限界

第一にアクセス制御の問題です。
Javaでは、クラスやそのメンバーへのアクセス制御は、アクセス修飾子によって行います。ライブラリは、機能に応じて様々なクラスを作成するため、内部的に複数のパッケージによって構成されます。このときライブラリ内において、パッケージを跨る形でクラスやメンバーを公開する必要がある場合、必然的にアクセス修飾子はpublicを指定することになります。
ただしアクセス修飾子をpublicにしてしまうと、ライブラリ内だけではなく、そのライブラリを利用する他のライブラリやアプリケーションにまで、公開範囲が自動的に広がってしまいます。他のライブラリやアプリケーションに対してはアクセス不可にしたい、という要件があったとしても、アクセス修飾子の機能では、それを実現することはできないのです。

【図2-1-2】アクセス修飾子によるアクセス制御の限界
image.png

クラスパスの限界

続いてもう1つの課題は、クラスパスの問題です。
Javaではライブラリは通常、JARファイルの形式で配布されます。アプリケーションでは、コンパイル時または実行時に、利用するライブラリのJARファイルにクラスパスを設定します。
ただしクラスパスの仕組みは、必要なクラスをクラスパス内から検索するだけに留まり、そのJARファイルが本当に意図したライブラリなのかを保証することはできません。またクラスパスに複数のJARファイルが設定されている場合、異なるJARファイルに同じFQCNを持つクラスが含まれていても、これを検知することはできません。

【図2-1-3】クラスパスの限界
image.png

モジュールシステムが導入されたことにより、これらの課題が解決されました。

2.1.2 モジュールシステムの仕組み

モジュールシステムの目的

アプリケーションやライブラリを開発する上で、モジュールシステム化するかどうかは、開発者自身で決めることができます。具体的には、パッケージ構成のディレクトリ内に、モジュール定義ファイルと呼ばれるファイルを配置すると、モジュールシステムが適用されたものと見なされます。モジュール定義ファイルの記述方法については、後述します。
モジュールシステムでは、モジュール定義ファイルを含んだ1つのJARファイルが、基本的には1つのモジュールになります。
モジュールシステムを適用すると、以下の2つの要件が実現可能になります。

一点目:アクセス修飾子問題の解決

まず一点目は、モジュール定義ファイルの記述内容に従って、パッケージ単位に公開範囲をきめ細かく定義可能になる、という点です。
公開範囲には、以下のいずれかのレベルがあります。

(1)すべてのモジュールに対して外部公開する
(2)特定のモジュールに対してのみ外部公開する
(3)他のモジュールには公開しない(自モジュール内のみ)

(1)から(3)のうちどのレベルにするかについては、モジュール定義ファイルの記述内容によって決まります。

二点目:クラスパス問題の解決

もう一点は、正しいライブラリ(JARファイル)が「モジュールパス」と呼ばれるパス上に配置されていることが保証されるようになる、という点です。またモジュールパスに複数のJARファイルが指定されている場合、異なるJARファイルに同じFQCNを持つクラスが含まれていると、エラー検知が可能です。
なおモジュールパスとは、モジュールシステムにおいて使われるモジュールの検索パスで、クラスパスの代わりに使われます。javaコマンドでクラスを実行するとき、モジュールパスを指定するための方法については後述します。

モジュール定義ファイルとは

モジュール定義ファイルの名前は、"module-info.java"に決まっています。このファイルは、拡張子からもわかるとおりJavaのソースファイルの一種として扱われ、コンパイルされると"module-info.class"が生成されます。
モジュール定義ファイルでは、モジュールを以下のように宣言します。

【構文】モジュールの宣言
module モジュール名 {
    ........
}

モジュール名も一種の識別子のため、識別子のネーミングルールに則ってさえいれば、任意の名前を付けることができます。ただしモジュール名もFQCNと同じように、「世界中で一意になるような名前を付ける」というのが基本的な考え方なので、ドメイン名を逆さにして使うケースが一般的です。必然的にモジュール名は、そのモジュールが包含するパッケージ名に近い名前になりますが、必ずしも両者を対応させる必要はありません。
なおモジュール定義ファイルは、JARファイルのルートディレクトリ直下となる位置に配置します。

モジュール定義ファイルの基本的な記述方法

ここでモジュール定義ファイルの具体的な記述方法を説明するために、以下の2つのモジュールを例として取り上げます。

  • モジュール名=pro.kensait.foo … ライブラリ
  • モジュール名=pro.kensait.hoge … アプリケーション

これら2つのモジュール内のクラスが、以下のようなディレクトリに配置されているものとします。

+-- foo-lib  ← pro.kensait.fooモジュール
|    +-- module-info.class 【1】
|    +-- pro
|         +-- kensait
|              +-- foo
|                   +-- api
|                        +-- StringUtil.class 【2】
|                   +-- internal
|                        +-- Common.class 【3】
|
+-- hoge-app  ← pro.kensait.hogeモジュール
     +-- module-info.class 【4】
     +-- pro
          +-- kensait
               +-- hoge
                    +-- Application.class 【5】

まずライブラリであるpro.kensait.fooモジュール(以降fooモジュール)から見ていきます。
仮にpro.kensait.foo.apiパッケージ(StringUtilなどのクラス【2】)は、アプリケーションなどすべての他のモジュールに外部公開するが、それ以外のパッケージ(Commonなどのクラス【3】)は外部公開したくない、という要件があるものとします。
そのような場合、モジュール定義ファイル【1】は以下のように記述します。

module-info.java
module pro.kensait.foo {
    exports pro.kensait.foo.api; // 外部公開するパッケージ名
}

exportsキーワードに、外部公開するパッケージを指定します。このようにすると、pro.kensait.foo.apiに所属するStringUtilクラスは、すべてのモジュールに外部公開されます。一方pro.kensait.foo.internalパッケージに所属するCommonクラスは、他のモジュールからはアクセス不可になります。

次に、アプリケーションであるpro.kensait.hogeモジュール(以降hogeモジュール)を見ていきます。
hogeモジュールは、ライブラリであるfooモジュールのStringUtilクラスを呼び出して利用しているものとします。
そのような場合、モジュール定義ファイル【4】は以下のように記述します。

module-info.java
module pro.kensait.hoge {
    requires pro.kensait.foo; // 必要としているモジュール名
}

requiresキーワードに、必要としているモジュール名を指定します。これでこのモジュールに所属するクラス(Applicationクラス)からは、fooモジュールにアクセス可能になります。ただし前述したように、fooモジュール側のモジュール定義により、StringUtilクラスにはアクセス可能ですが、Commonクラスはアクセス不可になります。

特定モジュールへの外部公開

前項の例では、fooモジュールのモジュール定義ファイルにおいて、pro.kensait.foo.apiパッケージをすべてのモジュールに外部公開していました。
モジュール定義ファイルでは、パッケージを特定のモジュールに対してのみ公開することも可能です。
具体的には以下のように記述します。

module-info.java
module pro.kensait.foo {
    exports pro.kensait.foo.api to pro.kensait.bar;
}

このようにすると、pro.kensait.foo.apiパッケージが、pro.kensait.barモジュールに対してのみ公開されます。

推移的な依存関係の解決

例えばfooモジュール、barモジュール、bazモジュール、hogeモジュールという4つのモジュールがあり、それぞれ以下のような依存関係があるものとします。

【図2-1-4】依存関係の例
image.png

hogeモジュールは、fooモジュールをrequiresしているためアクセスが可能ですが、fooモジュールが依存しているbarモジュール、bazモジュールには、アクセス不可の状態になっています。もしhogeモジュールからこれらのモジュールへのアクセスが必要な場合は、モジュール定義ファイルにおいて、barモジュール、bazモジュールへのrequiresキーワードを追加すればOKです。
ただしモジュール定義ファイルを以下のように記述すると、fooモジュールがrequiresしているモジュールに、推移的にアクセスが可能になります。

module-info.java
module pro.kensait.hoge {
    requires transitive pro.kensait.foo;
}

このようにtransitiveキーワードを追加すると、指定したモジュールから推移的にアクセスが可能になるため、必要なモジュール名をすべて列挙する必要はなくなります。

【図2-1-5】推移的な依存関係
image.png

モジュールパスとルートモジュール

モジュールシステムを適用した場合、javaコマンドによってメインクラスを実行するとき、オプションとしてモジュールパスを指定します。
具体的には、以下のようにjavaコマンドを投入します。

java --module-path  モジュールパス  --module  実行したいモジュールとそのクラス

例えば、C:/modulesというディレクトに、既出のfoo-lib.jar(ライブラリ)とhoge-app.jar(アプリケーション)を配置したとします。このときhoge-app.jarに含まれるメインクラス(Applicationクラス)を実行するためには、以下のようにコマンドを投入します。

java --module-path C:/modules --module pro.kensait.hoge/pro.kensait.hoge.Application

このように--module-pathオプションに、JARファイルを配置したディレクトリを指定します。
また--moduleオプションには、実行対象のモジュール名と、スラッシュでつないでメインクラスのFQCNを指定します。
このとき--moduleオプションに指定されたモジュールが、このJavaアプリケーションにおけるルートモジュールになります。モジュールシステムでは、ルートモジュールを起点にモジュール間のグラフ構造(モジュールグラフ)を辿り、整合性をチェックします。
具体的には以下のようなチェックをします。

  • モジュールグラフ上に、必要としているモジュール(ライブラリ)が配置されていること。
  • モジュールグラフ内の各モジュールに、同一のFQCNを持つクラスが複数存在しないこと。

なおモジュールシステムでは、必要としているモジュールのバージョンを指定したり、モジュールグラフを辿りながら必要なモジュールをダウンロードする機能は提供されません。これらの機能は、MavenやGradleといった、実績のあるOSSのビルドツールによって実現する、というのが前提になっています。

Java SEクラスライブラリとbaseモジュール

Java SEのクラスライブラリは、モジュールシステム化されています。
javaコマンドに--list-modulesオプションを付けて実行すると、以下のようにJava SEクラスライブラリが包含するモジュールの一覧を、確認することができます。

java --list-modules
java.base@17.0.2
java.compiler@17.0.2
java.datatransfer@17.0.2
....中略....
jdk.xml.dom@17.0.2
jdk.zipfs@17.0.2

モジュールシステム化されたアプリケーションやライブラリを開発する場合、モジュール定義ファイルを作成し、必要に応じてこれらのモジュールをrequiresに指定してください。
ただしこの中でも、java.baseモジュールだけは別扱いです。このモジュールは、特にrequiresに指定しなくても暗黙的に利用することができます。

2.1.3 モジュール化未対応ライブラリとの共存

モジュール化未対応ライブラリとの共存

モジュールシステムは、Java 9で導入された比較的新しい仕組みです。現時点でもコミュニティやベンダーによって提供されているライブラリが、すべからくモジュールシステムに対応した、というわけではありません。
そのためモジュールシステムには、モジュール化したライブラリとモジュール化未対応のライブラリとが、共存するための機能が提供されています。それが後述する自動モジュールや無名モジュールです。

自動モジュール

JARファイルがモジュール定義ファイルを持っていなかったとしても、モジュールパス上に配置された場合は、自動的にモジュールとして扱われれます。これを、自動モジュールと呼びます。
自動モジュールの名前は、以下のルール1、2の順に決まります。

  1. JARファイル内のMETA-INF/MANIFEST.MFファイルにおいて、Automatic-Module-Name属性にモジュール名が定義されている場合は、その名前。
    例えば"Automatic-Module-Name: pro.kensait.foo"といった具合に定義される。
  2. JARファイルの名前から拡張子とバージョン番号を除去し、非英数字をドットで置き換えたもの。
    例えば、"pro-kensait-foo-1.3.2.jar"というJARファイルの場合は、モジュール名は"pro.kensait.foo"。

自動モジュールでは、包含されるすべてのパッケージがexportsされたものと見なされ、外部公開されます。またモジュールグラフ上のすべてのモジュールを、requiresしたものと見なされます。

無名モジュール

モジュール定義ファイルを持たないJARファイルが、モジュールパスではなくクラスパスから読み込まれた場合は、無名モジュールという特殊なモジュールとして扱われれます。
無名モジュールは、クラスパスによってJARファイルを読み込む処理が現時点でも残存していることを踏まえ、互換性維持のために用意された機能です。無名モジュールでは、自動モジュールと同じように、包含されるすべてのパッケージがexportsされたものと見なされ、外部公開されます。またモジュールグラフ上のすべてのモジュールを、requiresしたものと見なされます。
ただし無名モジュールは名前を持たないため、モジュールシステム化されたモジュールからは、アクセスすることができません。そもそもモジュールシステムは、クラスパスの課題を解決するための仕組みとして導入されたものなので、このような制約は自明といえます。
なお自動モジュールからは、無名モジュールに対してアクセスが可能です。

アプリケーション開発する上でのモジュールシステム利用方針

アプリケーションをモジュールシステム化した場合、そのモジュールと他のモジュールとの関係は、以下のようになります。

【図2-1-6】モジュールシステムと自動モジュール・無名モジュールの関係
image.png

モジュールシステムは前述したように、ライブラリの開発や配布に関する諸問題を解決するために導入された仕組みです。
ただしライブラリのためだけではなく、アプリケーションを開発する上でも、モジュールシステム化には一定の効果があります。まず依存するライブラリがモジュール化対応していれば、ライブラリによって定義された公開範囲に従って、アクセスが自動的に制御されます。また依存するライブラリがモジュール化対応している・いないに関わらず、ライブラリをモジュールパスから読み込むようにすれば、自動モジュールとして扱われるため、正しいライブラリがモジュールグラフ上に配置されていることが保証されます。
以上から今後のJavaによるアプリケーション開発では基本的にはモジュールシステムを適用し、すべてのライブラリをモジュールパスから読み込むことをお勧めします。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. 一般論として、アプリケーションとライブラリの違いについて。
  2. モジュールシステム導入前には「アクセス制御の限界」や「クラスパスの限界」といった課題があったこと。
  3. モジュールシステム導入の目的について。
  4. モジュール定義ファイルの記述方法について。
  5. 特定モジュールへの外部公開方法や推移的な依存関係の解決方法について。
  6. モジュールパスの設定方法やモジュールグラフの構築について。
  7. モジュール化未対応ライブラリとの共存方法や、自動モジュールや無名モジュールについて。

Discussion