ノエリアラボラトリー

日々の研究と開発の成果を書くよ

Java9 Project Jigsawのモジュールがわからないので調べた

Java9でモジュールが導入されて特にJavaFXではコンパイルがこけるようになった。 由々しき事態なのでこの機会に調べようと思ったがまだちゃんとした情報が全然ない(リリース1週間後くらいに書き始めたので10/28現在はもっとあるかもしれない)。 仕方ないのでJLSで調べることにした。頑張って英語を読んだのでメモを残しておく。

間違っていることがあったらどうか指摘してほしい。

Chapter 7. Packages and Modules

Modules and javac

github.com

github.com

qiita.com

基本形式

[open] module hoge.foo.bar {
    // directives
}

上記の例ではhoge.foo.barというモジュールが作られる。

openの有無の違いはモジュール内の全パッケージを公開するか否か。 無い場合は明示的にopensexportsで公開するパッケージを指定しなければモジュールの外からパッケージを参照することはできない。

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

ディレクティブって言葉がわかりにくいんだけど、使えるキーワードくらいに思っておけばいいらしい。

モジュールで記述できるディレクティブは以下の通り

  • requires {transitive | static} module ;
  • exports package (to module (, module)) ;
  • opens package (to module (, module)) ;
  • uses type ;
  • provides type with (type (, type)) ;

requiresexports/opensおよびusesprovidesは対になるものと考えて良いと思われる。 説明を読む限りこれらはあるモジュールに対するクライアント/ホストとしての振る舞いを規定するものであると言える。

requires

requiresディレクティブはモジュールにおけるインポートに相当する。

module hoge.foo.bar {
    requires java.desktop;
    requires transitive javafx.base;
    requires static lombok;
}

以上のように書くとそれぞれのモジュールがhoge.foo.barで使えるようになる。

当たり前かもしれないが、同じパッケージを二度宣言すると怒られる。

transitiveという修飾子は推移的なインポートであるらしい。では「推移的」と一体何か。

簡単に言えば、「インポートしたモジュールがインポートしているパッケージ」も暗黙的に使えるようにするというものである。

上記の例で言えば、hoge.foo.barrequiresで指定すると、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;
}

詳しい例は以下を見るとわかりやすいと思う。

example_uses-provides

その他の事項

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は実行時にopensexportsに指定するパッケージを追加できる。

--addmodulesはよくわかっていないが、実行時にJava9以前のモジュール依存の定義されていないライブラリに対するrequiresのような働きをするものという認識。

おわりに

モジュールには厳密には3種類のものがあるらしいが、色々とめんどくさそうなので省いた。 そのうち検証したい。