kagamihogeの日記

kagamihogeの日記です。

Project Jigsaw: Module System Quick-Start Guideをテキトーに訳した

http://openjdk.java.net/projects/jigsaw/quick-start をテキトーに訳した

Project Jigsaw: Module System Quick-Start Guide

このドキュメントでは、モジュール(module)を初めて使う開発者向けにいくつかの例を示します。

例におけるファイルパスはスラッシュでパスセパレータはコロンです。Microsoft Windows上での開発の場合、ファイルパスはバックスラッシュでパスセパレータはセミコロンになります。

  • Greetings
  • Greetings world
  • Multi-module compilation
  • Packaging
  • Missing requires or missing exports
  • Services
  • The linker
  • javac -Xmodule and java -Xpatch

Greetings

最初の例は"Greetings!"と表示するだけのcom.greetingsという名前のモジュールです。このモジュールには2つのソースファイルが含まれており、それぞれモジュール定義(module-info.java)とメインクラスです。

慣例により、モジュールのソースコードはモジュール名のディレクトリに配置します。

    src/com.greetings/com/greetings/Main.java
    src/com.greetings/module-info.java

    $ cat src/com.greetings/module-info.java
    module com.greetings { }

    $ cat src/com.greetings/com/greetings/Main.java
    package com.greetings;
    public class Main {
        public static void main(String[] args) {
            System.out.println("Greetings!");
        }
    }

ソースコードは以下のコマンドによりmods/com.greetingsディレクトリにコンパイルされます。

    $ mkdir -p mods/com.greetings

    $ javac -d mods/com.greetings \
        src/com.greetings/module-info.java \
        src/com.greetings/com/greetings/Main.java

上記の例は以下のコマンドで実行します。

    $ java -modulepath mods -m com.greetings/com.greetings.Main

-modulepathはモジュールパスで、モジュールを含むディレクトリを値として1つ以上指定します。-mオプションでメインモジュールを指定し、スラッシュより後ろがモジュールのメインクラス名です。

Greetings world

2つ目の例はモジュールorg.astroへの依存を宣言するようにモジュール宣言を書き換えます。モジュールorg.astroorg.astroパッケージのAPIをエクスポートします。

    src/org.astro/module-info.java
    src/org.astro/org/astro/World.java
    src/com.greetings/com/greetings/Main.java
    src/com.greetings/module-info.java

    $ cat src/org.astro/module-info.java
    module org.astro {
        exports org.astro;
    }

    $ cat src/org.astro/org/astro/World.java
    package org.astro;
    public class World {
        public static String name() {
            return "world";
        }
    }

    $ cat src/com.greetings/module-info.java
    module com.greetings {
        requires org.astro;
    }

    $ cat src/com.greetings/com/greetings/Main.java
    package com.greetings;
    import org.astro.World;
    public class Main {
        public static void main(String[] args) {
            System.out.format("Greetings %s!%n", World.name());
        }
    }

モジュールは1つずつコンパイルされます。モジュールcom.greetingsコンパイルするためのjavacコマンドではモジュールパスを指定していますが、これはモジュールorg.astroへの参照とorg.astroがエクスポートするパッケージの型を解決可能にするためです。

    $ mkdir mods/org.astro mods/com.greetings

    $ javac -d mods/org.astro \
        src/org.astro/module-info.java src/org.astro/org/astro/World.java

    $ javac -modulepath mods -d mods/com.greetings \
        src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java

この例は最初の例と全く同じコマンドで動作します。

    $ java -modulepath mods -m com.greetings/com.greetings.Main
    Greetings world!

Multi-module compilation

前の例ではモジュールcom.greetingsorg.astroは別々にコンパイルしていました。複数モジュールを1回のjavadcコマンドでコンパイルすることも可能です。

    $ mkdir mods

    $ javac -d mods -modulesourcepath src $(find src -name "*.java")

    $ find mods -type f
    mods/com.greetings/com/greetings/Main.class
    mods/com.greetings/module-info.class
    mods/org.astro/module-info.class
    mods/org.astro/org/astro/World.class

Packaging

これまでの例ではコンパイルしたモジュールはファイルシステム上にexploded*1されています。配布やデプロイ目的には、通常、扱いやすくするためにmodular JARとしてモジュールをパッケージします。modular JARは普通のJARファイルでトップレベルディレクトリにmodule-info.classを持ちます。以下の例はmlibディレクトリにorg.astro@1.0.jarcom.greetings.jarを作成します。

    $ mkdir mlib

    $ jar --create --file=mlib/org.astro@1.0.jar \
        --module-version=1.0 -C mods/org.astro .

    $ jar --create --file=mlib/com.greetings.jar \
        --main-class=com.greetings.Main -C mods/com.greetings .

    $ ls mlib
    com.greetings.jar   org.astro@1.0.jar

この例では、モジュールorg.astroにはバージョン1.0を示すためのパッケージ化が行われています。モジュールcom.greetingsはメインクラスがcom.greetings.Mainと示すためのパッケージ化が行われています。これにより、メインクラスを指定することなくモジュールcom.greetingsを実行可能です。

    $ java -mp mlib -m com.greetings
    Greetings world!

また、-modulepathの代わりに-mpを使うことでコマンドラインを短縮しています。

jarツールには新規オプション(jar -help参照)が多数追加され、その内の1つに、modular JARとしてパッケージ化されたモジュールの宣言を表示するものがあります。

    $ jar --print-module-descriptor --file=mlib/org.astro@1.0.jar

    Name:
      org.astro@1.0
    Requires:
      java.base [ MANDATED ]
    Exports:
      org.astro

Missing requires or missing exports

次に、前述の例においてcom.greetingsモジュール宣言からrequiresを誤って削除した場合に何が起きるのかについて見ていきます。

    $ cat src/com.greetings/module-info.java
    module com.greetings {
        // requires org.astro;
    }

    $ javac -modulepath mods -d mods/com.greetings \
        src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
    src/com.greetings/com/greetings/Main.java:2: error: package org.astro does not exist
    import org.astro.World;
                    ^
    src/com.greetings/com/greetings/Main.java:5: error: cannot find symbol
            System.out.format("Greetings %s!%n", World.name());
                                                 ^
      symbol:   variable World
      location: class Main
    2 errors

モジュール宣言を修正したとして、それとは別のミスとしてorg.astroモジュール宣言からexportsを削除してみます。

    $ cat src/com.greetings/module-info.java
    module com.greetings {
        requires org.astro;
    }
    $ cat src/org.astro/module-info.java
    module org.astro {
        // exports org.astro;
    }

    $ javac -modulepath mods -d mods/com.greetings \
       src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
    src/com.greetings/com/greetings/Main.java:2: error: package org.astro does not exist
    import org.astro.World;
                    ^
    src/com.greetings/com/greetings/Main.java:5: error: cannot find symbol
            System.out.format("Greetings %s!%n", World.name());
                                                 ^
      symbol:   variable World
      location: class Main
    2 errors

Services

サービスによりservice consumersモジュールとservice providersモジュール間に疎結合が実現できます。

以下の例にはservice consumerモジュールとservice providerモジュールがあります。

  • モジュールcom.socketはネットワークソケット用のAPIをエクスポートします。このAPIcom.socketパッケージにあり、エクスポートされます。このAPIは別の実装を許容するためにpluggableです。サービスの型は同モジュール内のcom.socket.spi.NetworkSocketProviderで、com.socket.spiもエクスポートされます。
  • モジュールorg.fastsocketはservice providerモジュールです。com.socket.spi.NetworkSocketProviderの実装を提供します。エクスポートするパッケージはありません。

以下はモジュールcom.socketソースコードです。

    $ cat src/com.socket/module-info.java
    module com.socket {
        exports com.socket;
        exports com.socket.spi;
        uses com.socket.spi.NetworkSocketProvider;
    }

    $ cat src/com.socket/com/socket/NetworkSocket.java
    package com.socket;

    import java.io.Closeable;
    import java.util.Iterator;
    import java.util.ServiceLoader;

    import com.socket.spi.NetworkSocketProvider;

    public abstract class NetworkSocket implements Closeable {
        protected NetworkSocket() { }

        public static NetworkSocket open() {
            ServiceLoader<NetworkSocketProvider> sl
                = ServiceLoader.load(NetworkSocketProvider.class);
            Iterator<NetworkSocketProvider> iter = sl.iterator();
            if (!iter.hasNext())
                throw new RuntimeException("No service providers found!");
            NetworkSocketProvider provider = iter.next();
            return provider.openNetworkSocket();
        }
    }


    $ cat src/com.socket/com/socket/spi/NetworkSocketProvider.java
    package com.socket.spi;

    import com.socket.NetworkSocket;

    public abstract class NetworkSocketProvider {
        protected NetworkSocketProvider() { }

        public abstract NetworkSocket openNetworkSocket();
    }

以下はモジュールorg.fastsocketソースコードです。

    $ cat src/org.fastsocket/module-info.java
    module org.fastsocket {
        requires com.socket;
        provides com.socket.spi.NetworkSocketProvider
            with org.fastsocket.FastNetworkSocketProvider;
    }

    $ cat src/org.fastsocket/org/fastsocket/FastNetworkSocketProvider.java
    package org.fastsocket;

    import com.socket.NetworkSocket;
    import com.socket.spi.NetworkSocketProvider;

    public class FastNetworkSocketProvider extends NetworkSocketProvider {
        public FastNetworkSocketProvider() { }

        @Override
        public NetworkSocket openNetworkSocket() {
            return new FastNetworkSocket();
        }
    }

    $ cat src/org.fastsocket/org/fastsocket/FastNetworkSocket.java
    package org.fastsocket;

    import com.socket.NetworkSocket;

    class FastNetworkSocket extends NetworkSocket {
        FastNetworkSocket() { }
        public void close() { }
    }

説明単純化のために、上記モジュールは同時にコンパイルしています。実際には、service consumerモジュールとservice providerモジュールは別々にコンパイルされることがほとんどだと思われます。

    $ mkdir mods
    $ javac -d mods -modulesourcepath src $(find src -name "*.java")

最後に、上記APIを使うためにモジュールcom.greetingsを修正します。

    $ cat src/com.greetings/module-info.java
    module com.greetings {
        requires com.socket;
    }

    $ cat src/com.greetings/com/greetings/Main.java
    package com.greetings;

    import com.socket.NetworkSocket;

    public class Main {
        public static void main(String[] args) {
            NetworkSocket s = NetworkSocket.open();
            System.out.println(s.getClass());
        }
    }
    $ javac -d mods/com.greetings/ -mp mods $(find src/com.greetings/ -name "*.java")

実行します。

    $ java -mp mods -m com.greetings/com.greetings.Main
    class org.fastsocket.FastNetworkSocket

出力結果として、service providerが配置されてNetworkSocketのファクトリーとしてそのservice providerが使われていることが確認できます。

The linker

jlinkはリンカーツールです。複数のモジュールをリンクし、推移依存(transitive dependences)に従ってカスタムのモジュール実行時イメージ(custom modular run-time image)(JEP 220を参照)を作成するのに使います。

このツールは今のところ、modular JARもしくはJMODフォーマットでパッケージ化されたモジュールパス上のモジュールを要求します。JDKのビルドはスタンダードおよびJMODフォーマットのJDK固有モジュールをパッケージします。

以下の例はモジュールcom.greetingsと推移依存を含む実行時イメージを作成します。

    jlink --modulepath $JAVA_HOME/jmods:mlib --addmods com.greetings --output greetingsapp

$JAVA_HOME/jmodsディレクトリにはjava.base.jmodとその他の標準およびJDKモジュールが含まれます。OpenJDKの自前ビルドを使う場合、jmodファイルは$BUILDOUTPUT/images/jmodにあり、$BUILDOUTPUTはビルド出力ディレクトリです。

モジュールパス上のmlibディレクトリにはモジュールcom.greetings用のアーティファクトが含まれます。

jlinkツールはイメージ生成をカスタマイズするための高度なオプションを多数サポートしており、詳細はjlink --helpを参照してください。

現時点では、jlinkツールはデフォルトでservice providerモジュールのリンクを行うため、生成される実行時イメージには予期しないモジュールが含まれる可能性があります。

javac -Xmodule and java -Xpatch

Doug Lea's CVSからjava.util.concurrentクラスをチェックアウトする開発者はソースファイルのコンパイルとクラスのデプロイに-Xbootclasspath/pを使うことになると思われます。

-Xbootclasspath/pが無いと、モジュール置換はモジュール内のクラスのオーバーライドするための-Xpatchオプションとなります*2。また、既存モジュールのパッケージのクラスのコンパイル時にjavacは警告を出します。既存モジュールのクラスをコンパイルするにはjavac-Xmoduleオプションが必要です。

以下の例はjava.util.concurrent.ConcurrentHashMapの新バージョンをコンパイルし、実行時にこれを使用しているものです。

    javac -Xmodule:java.base -d mypatches/java.base \
        src/java.base/java/util/concurrent/ConcurrentHashMap.java

    java -Xpatch:mypatches ...

More information

Feedback

使用法に関する質問や実際に使ってみた感触をjigsaw-devへフィードバックをお願い致します。モジュールシステムの設計に関する具体的な提案はJSR 376 Expert Group's comments listに送信してください。

*1:jarやwarと異なり中身のファイルがディレクトリ上に存在している様を示すのに使われる。なおexplodeには爆発するとか破裂させる、とかの意味がある。タブンだけど、warやjarのように固まっているのではなく、ファイルがディレクトリに散らばっているのをexplodeと表現しているのでは?と思われる。exploded deploymentとかでぐぐるとそんな気がする。

*2:-Xbootclasspath/p has been removed, its module replacement is the option -Xpatch to override classes in a module.よくわからん