モジュール機能「Project Jigsaw」とは(2)
依存するモジュールが多い場合や依存の依存などがある場合の宣言方法
利用するパッケージがあるたびに、このような宣言をすることになるのかと思うと少々、面倒に感じるのではないでしょうか。そのため、JavaSEの標準ライブラリでは、java.seモジュールのようにパッケージを集めたモジュールも定義されています。
module java.se { requires transitive java.compiler; requires transitive java.datatransfer; requires transitive java.desktop; requires transitive java.instrument; requires transitive java.logging; requires transitive java.management; requires transitive java.naming; requires transitive java.prefs; requires transitive java.rmi; requires transitive java.scripting; requires transitive java.security.jgss; requires transitive java.security.sasl; requires transitive java.sql; requires transitive java.sql.rowset; requires transitive java.xml; requires transitive java.xml.crypto; }
また、requireにて宣言したモジュールが、さらに別のモジュールに依存している場合はどうしたらいいでしょうか。もちろん、それらの依存も調べてモジュール内で定義すれば利用できます。
しかしこれでは大変なので、transitive
をつけて宣言します。この指定をすると、指定したモジュール内で必要とされているモジュールも自動的に宣言したことになります。
したがって、リスト3のようにtransitive
をつけてjava.seモジュールのrequire
宣言するだけで、それらに依存して標準ライブラリで提供されているモジュールを利用できるようになるわけです。
module com.coltware.main { requires transitive java.se; }
ただし、Stringクラスを使うのにもモジュールの宣言が必要ということはありません。それらはjava.baseという基本的なパッケージに含まれているため、宣言する必要はありません。
これらのJDKのモジュールとパッケージの関係は、Java 9のAPIドキュメントに記載されているのでそちらを参考してください。
実行時にはチェックされない依存を宣言する
コンパイル時には必要であるものの、実行時には不要な場合、リフレクションを用いて実行する場合があります。このケースでは、リスト4のようにstaticを追加して宣言します。
module com.coltware.main { requires static com.colware.mod; }
モジュールを使うケースでは多少気をつけないといけない部分はあるものの、基本的には必要なモジュールを宣言していくだけです。実際の開発時に悩むことはあまりないでしょう。
公開するモジュールを定義する
続いて、自分でモジュールを作成して公開する場合の定義方法です。
まず、図6のように公開したいパッケージと公開したくないパッケージがある場合を想定してみます。
ライブラリを作っていると、そのライブラリ内でのみ使える内部実装を直接見せたくないケースや、そのライブラリ内で共通に使うユーティリティ的なクラスを含めたくなるケースがあります。また、それらはそのライブラリ内の保守運用において変わる可能性があるため、他のライブラリから読ませないようにするケースもあるでしょう。
この場合は、リスト5の通りにexoprts
で公開したいパッケージのみ宣言します。
module com.coltware.mod { exports com.coltware.mod.api; }
公開されていないパッケージを使おうとすると、リスト6のようにコンパイルエラーになります。つまり、そのクラスの存在を知っている開発者であっても、モジュール外から利用することを抑制できます。
main/src/com/coltware/main/Main.java:9: エラー: パッケージcom.coltware.mod.internalは表示不可です com.coltware.mod.internal.InternalHello h; ^ (パッケージcom.coltware.mod.internalはモジュールcom.coltware.modで宣言されていますが、エクスポートされていません) エラー1個
続いて、リスト7のようにメソッドやフィールドに対してディープ・リフレクションを用いている場合です。ディープ・リフレクションとは、setAccessible
を使って、privateメソッドなどを強制的に実行時にアクセスができるようにする機能です。
しかし、exports
での指定ではディープ・リフレクションによる実行はできないようになっています。
Class c = Class.forName("com.coltware.submod.SubmodHello"); Object obj = c.getDeclaredConstructor().newInstance(); for(Method m : c.getDeclaredMethods()){ m.setAccessible(true); // ここでprivateなメソッドなどでも強制的にアクセスできるようにしてしまっている m.invoke(obj); }
実際にこのようなコードをコンパイルしてもエラーは発生しません。しかし、実行時にはリスト8のエラーが発生します。
java.lang.reflect.InaccessibleObjectException: Unable to make private java.lang.String com.coltware.submod.SubmodHello.exec2() accessible: module com.coltware.submod does not "opens com.coltware.submod" to module com.coltware.mod
privateなメソッドやフィールドに強制的にアクセスするケースはあまりないはずですが、こうした処理はミドルウェアなどがよく行います。そのため、使う側が定義するということはあまり一般的ではないでしょう。
したがって、モジュールを定義する側で許可する必要が生じます。この場合は、リスト9のようにopens
宣言でモジュールを公開します。
module com.coltware.submod {
opens com.coltware.submod; // ディープ・リフレクションを許可する指定を追加
exports com.coltware.submod;
}
または、リスト10のようにmodule
宣言の前にopen
宣言しても構いません。
open module com.coltware.submod { exports com.coltware.submod; }
ただし、module
自体にopen
指定をした場合には、exports
しているすべてのパッケージに対してディープ・リフレクションを許可することになってしまいます。この対応が必要になるケースはあまりないはずです。
また、opens
をすると、どんなモジュールからも実行が許可できてしまいます。これでは、そのモジュールを悪用して機能を実行することが容易になってしまいます。
その場合、リスト11のようにto
を用いることで特定のモジュール(ここではcom.coltware.mod
)のみに対して許可することができます。
module com.coltware.submod { exports com.coltware.submod to com.coltware.mod; // toを用いて利用する側のモジュールを指定 opens com.coltware.submod to com.coltware.mod; // toを用いて利用する側のモジュールを指定 }
このように公開する側で細かく公開範囲の指定ができるようになっています。
Java 9のモジュール機能が当たり前のように使われるようになると、ミドルウェア上で動作するプログラムを作る場合に、ミドルウェア側のモジュール名の指定が必要になるケースも出てくるでしょう。
また、ライブラリを作成する側では、公開したパッケージを非公開に変更することは定義上簡単にできますが、実際にそのライブラリを使用している場合、すでに動いていたコードが突然動かなくなるということもあり得ます。
そのため、安易に公開範囲は変えることはできません。また、公開範囲を無条件に広くしておくことはそれだけ、モジュール機能の意味合いが薄れてきます。
このように、モジュールを作成して公開する場合には、これまでよりも注意が必要になります。
最後に
今回は、モジュール機能の基本的な部分のみを紹介しましたが、これだけでも今までのJavaとは制限が大きく異なるということが想像できたのではないでしょうか。また、どうやら、既存のJavaのライブラリをそのまま使うことは難しいと感じられたのではないでしょうか。
しかし、心配する必要はありません。Java 9が大きく変わっても、既存のリソースが使えないのではJavaの理念である「Write Once, Run Anywhere」から大きく外れることになってしまいます。
次回は、このように既存の資産が存在する場合の対応や触れなかったサービスである「ServiceLoader」を使って実装を公開する場合など、今回紹介できなかった部分について解説します。