Java9 Project Jigsawのモジュールがわからないので調べた
Java9でモジュールが導入されて特にJavaFXではコンパイルがこけるようになった。 由々しき事態なのでこの機会に調べようと思ったがまだちゃんとした情報が全然ない(リリース1週間後くらいに書き始めたので10/28現在はもっとあるかもしれない)。 仕方ないのでJLSで調べることにした。頑張って英語を読んだのでメモを残しておく。
間違っていることがあったらどうか指摘してほしい。
Chapter 7. Packages and Modules
基本形式
[open] module hoge.foo.bar { // directives }
上記の例ではhoge.foo.barというモジュールが作られる。
open
の有無の違いはモジュール内の全パッケージを公開するか否か。
無い場合は明示的にopens
かexports
で公開するパッケージを指定しなければモジュールの外からパッケージを参照することはできない。
モジュールディレクティブ
ディレクティブって言葉がわかりにくいんだけど、使えるキーワードくらいに思っておけばいいらしい。
モジュールで記述できるディレクティブは以下の通り
- requires {transitive | static} module ;
- exports package (to module (, module)) ;
- opens package (to module (, module)) ;
- uses type ;
- provides type with (type (, type)) ;
requires
とexports
/opens
およびuses
とprovides
は対になるものと考えて良いと思われる。
説明を読む限りこれらはあるモジュールに対するクライアント/ホストとしての振る舞いを規定するものであると言える。
requires
requires
ディレクティブはモジュールにおけるインポートに相当する。
module hoge.foo.bar { requires java.desktop; requires transitive javafx.base; requires static lombok; }
以上のように書くとそれぞれのモジュールがhoge.foo.bar
で使えるようになる。
当たり前かもしれないが、同じパッケージを二度宣言すると怒られる。
transitive
という修飾子は推移的なインポートであるらしい。では「推移的」と一体何か。
簡単に言えば、「インポートしたモジュールがインポートしているパッケージ」も暗黙的に使えるようにするというものである。
上記の例で言えば、hoge.foo.bar
をrequires
で指定すると、hoge.foo.bar
がエクスポートしてるパッケージのほかに、javafx.base
がエクスポートしているパッケージも「Indirect Exports」としてエクスポートされる。
static
という修飾子はコンパイル時にのみ必要とされるオプションという扱いらしい。
いったいどんな状況でそんなものが必要とされるのかと思うかもしれない。私は思った。
どうやら、PAPA(Pluggable Annotation Processing Api)でアノテーションをゴリゴリ処理するのに使えるらしく、その用途で最も身近な例としてLombokが挙げられる。
他にも、起動時の引数に特定のパラメータを渡したときにしか呼び出されないクラスとかはこれにあたるかもしれない。
余談だが、モジュールの定義されていないjarファイルを読み込んだ場合、そのjarファイルの名前からモジュール名が付けられるらしいが沼になるのでここでは触れない。
exports/opens
exports
およびexports
ディレクティブはモジュールにおける公開制限を指定する。
なんでそれ分かれているのという感じはあるが、以下のような違いが微妙にあるらしい。
ディレクティブ | コンパイル時の参照 | privateリフレクション |
---|---|---|
exports | 〇 | ✕ |
opens | ✕ | 〇 |
ここで言う参照とは、public/protectedなメンバおよび型へのアクセスのこと。
privateリフレクションは実行時にprivaveなメンバおよび型にリフレクションを使用してアクセスすること。
これらのディレクティブは、to
を用いることで適用するモジュール群を指定することも可能。
詳しくは以下の例を見ていただきたい。
module hoge.foo.bar { requires javafx.controls; requires javafx.fxml; exports sample to javafx.graphics; opens sample to javafx.fxml; }
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.layout.BorderPane?> <BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.SampleController"> <center> <fx:include fx:id="test" source="test.fxml" /> </center> </BorderPane>
package sample; import javafx.fxml.FXML; import javafx.fxml.Initializable; import java.net.URL; import java.util.ResourceBundle; public class SampleController implements Initializable { @FXML private TestController testController; @Override public void initialize(URL location, ResourceBundle resources) {} }
この例では、testController
はfxmlが読み込まれた段階でリフレクションによりバインドされる。
sample
パッケージはjavafx.graphics
から見えていなければならず、javafx.fxml
モジュールからのprivateフィールドへのリフレクションを許可していなければならない。
そのため、上記のモジュール設定がなければコンパイル時にエラーが発生する。
requires
と同様に同じパッケージを2回以上'exports'やopens
で指定してはいけないし、1つのto
に対して同じモジュールは1度しか指定してはいけない。
uses/provides
私は全然使ったことないのだが、サービスプロバイダ(Service Provider Interface:SPI)という機能がJavaには存在する。
インタフェースに対する実装を分離してjarファイルを切り替えることで機能を変えることができるものだ。 詳しくはひしだまさんが解説してくれているのでここでは説明は省略する。
Jarファイルメモ(Hishidama's java-archive Memo)
uses
/provides
は従来マニフェストファイルに記載していたSPIの構成情報をモジュールで設定できるようにしたものだ。
uses
は現在のモジュールでjava.util.ServiceLoader
がサービスを検出できるようにするために使う。指定するのはサービスとして使用する型。
provides
は使用するサービスの型 with サービスの実装型の形で指定する。なお、実装は複数指定できる。
uses
/provides
も同じ型を2回以上指定できない。
サービスおよびサービスプロバイダの要件として、以下のような条件があるらしい(これはモジュールというよりSPIの仕様っぽい)
- サービスはクラス、インタフェース、アノテーションのいずれかでなければならない
- サービスは他のモジュールで宣言されていても良いが、アクセス可能でなければならない(サービスを含むパッケージが
exports
に指定されていなければならない) - サービスプロバイダはpublicなクラス、インタフェース、staticな内部クラスでなければならない(これはインスタンス化できなければならないということだと思われる)
- サービスプロバイダはプロバイダメソッド(引数なしのpublic staticメソッド)かプロバイダコンストラクタ(引数なしpublicコンストラクタ)を持っていなければならない
hoge.service
モジュールのservice.MyService
インタフェースをhoge.impl
モジュールのimpl.MyServiceImpl
クラスで実装する場合、モジュールの設定は以下のようになる。
module hoge.service { exports service; uses service.MyService; }
module hoge.impl { exports impl; provides service.MyService with impl.MyServiceImpl; }
詳しい例は以下を見るとわかりやすいと思う。
その他の事項
module-info.javaをどこに置くべきかということについて
srcの下にパッケージが続いていて、モジュールが1つしかないならsrc直下にmodule-info.javaがあってもコンパイル上何の問題はない。 ただ、これは運用上あまりよろしくない。
公式的にはStructuring the source treeという構造が紹介されている。
割とめんどくさい話ではあるけど、パッケージの前にモジュールのパス名を付けたディレクトリを作れということらしい。
hoge.foo
モジュールにhoge.foo.Main.java
があり、hoge.bar
モジュールにhoge.bar.Main.java
がある場合ディレクトリは以下のような構造になる。
src/ /hoge.foo/hoge/foo/Main.java /module-info.java /hoge.bar/hoge/bar/Main.java /module-info.java
オプションフラグ
-modulepath
or -p
はモジュールのルートを指定するオプション。jarファイルも指定できるらしい。
-modulesourcepath
は-sourcepath
のモジュールのための互換機能らしい。
--add-opens
,--add-exports
は実行時にopens
やexports
に指定するパッケージを追加できる。
--addmodules
はよくわかっていないが、実行時にJava9以前のモジュール依存の定義されていないライブラリに対するrequires
のような働きをするものという認識。
おわりに
モジュールには厳密には3種類のものがあるらしいが、色々とめんどくさそうなので省いた。 そのうち検証したい。