JavaOne2010 9/21 -- The Modular Java Platform and Project Jigsaw

Javaには膨大な数のライブラリがあり、我々開発者としてはそれらを使うことで既存の枯れた機能を簡単(?)に利用できてウハウハな訳ですが、ライブラリのJarファイルの管理はどうにも大変ですね。
Jar同士の依存性管理とか、手動でがんばるとありえないし。
というわけで、今まではMavenやIvyなどの完全に外部のプロダクトとしてその辺りのソリューションが提供されてきてたわけですが、ついに本家による対策がなされるときがきたのです、というお話。


経緯とかあまり詳しくないんですけど、

既に組み込みではデファクトのモジュラリティソリューションであるOSGiをそのまま採用しよう

やっぱりやめよう

やっぱりOSGiでいこう

(以下ループ)

みたいなやりとりが過去にあったようです。結局はOSGiとは全然別物になって今回のお披露目となりました。


というわけで、Project Jigsawのセッションです。


名前だけみると、なんかコスプレした人によく分からない場所に監禁されて致死確定のゲームを強いられるふいんきが漂いますが、もちろんそんなことはありません。


さて、Project Jigsawによる新しいモジュールシステムの特徴をぱらぱらと紹介していきます。
間違いがありましたらツッコミお願いします。

Install-time optimizations

新しいモジュールシステムでは、実行時ではなく、Jarファイルを実行環境上にインストールするときに、色々と最適化しておくことで、実行時のロード時間を劇的に改善します。

  • 通常は実行時にクラスファイルのベリファイが行われますが、インストール時に実行しておくことで省略できるようになり、10〜20%程度高速になります。
  • アプリケーション起動時にクラスのリストを記録して、それらを1つのファイルにコピーしておくことで、次回以降の起動が10〜30%程度高速になります。
  • クラスファイルをより効率的なフォーマットに変換して、共有することで10〜20%程度高速化します。

なんか数字だけ見るとめっちゃ速くなりそうなんですけど、実際どうなんですかね。それとも誤読?

依存関係はmodule-info.javaに設定する

Modularityの構成要素は、

Modularity = Grouping
+ Dependence
+ Versioning
+ Encapsulation
+ Optional modules
+ Virtual modules

ってことらしいです。

Grouping

複数のパッケージを1つのモジュールとして扱うための方法です。
といっても、ごくシンプルなもので、たとえば、

// com/foo/Main.java
package com.foo;

import java.io.*;
import java.util.*;

public class Main {
    ...
}

// com/foo/Other.java
package com.foo;

import java.io.*;
import java.util.*;

public class Other {
    ...
}

// com/foo/ui/Shell.java
package com.foo.ui;

import java.io.*;
import java.util.*;

public class Shell {
    ...
}

という3つのクラスがあるときに(パッケージ名に注目)、

// module-info.java
module com.foo { }

のように、module-info.javaというファイルに、見慣れぬmodule記法でパッケージ名の共通部分までを記述すると、com.foo.Mainとcom.foo.Otherとcom.foo.ui.Shellが同一のcom.fooモジュールに含まれる、ということになります。


JarのときはMETA-INF/MANIFEST.MFにMain-Classというエントリを書いて、実行可能Jarのエントリポイントを指定しましたが、Jigsawでは

// module-info.java
module com.foo {
    class com.foo.Main;
}

と書きます。

Dependence

次はモジュール内のクラスが外部ライブラリに依存してる場合です。

// com/foo/ui/Shell.java
package com.foo.ui;

import java.io.*;
import java.util.*;

import org.bar.lib.*;  // ←これと
import edu.baz.util.*; // ←これ。

public class Shell {
    ...
}

のように、Shellクラスがorg.barライブラリとedu.bazライブラリに依存している場合、今までだと、

$ java -cp app.jar:bar.jar:baz.jar ....

などと実行時にクラスパスリストをがんばって書かなければなりませんでしたが、Jigsaw以降はmodule-info.javaで宣言しておくだけOKになります。


つまり「クラスパスが許されるのなんて、Java7までだよねーー」 ということです。*1


さて、module-info.javaは↓こう書きます。

// module-info.java
module com.foo {
    requires org.bar.lib;
    requires edu.baz.util;
}

パッケージ名を書くだけ。jarファイル名とかは気にしません*2。あくまでAPIとしてのパッケージ名に対する依存性を記述します。

Versioning

依存先のAPIはバージョンごとに変化してしまう可能性がありますので、依存しているバージョンを明記するのは良い作法です。

module com.foo @ 1.0.0 {
  requires org.bar.lib @ 2.1-alpha;
  requires edu.baz.util @ 5.2_11;
}

このように@の後にバージョンを記述します。

Encapsulation

全部のモジュールがpublic扱いだと、見えすぎて何かとよくありません。
Javaコードでも全部publicだとちょっとアレですね。
適度に隠蔽した方が見通しもよくシンプルな構造が保てます。


というわけで、依存されるモジュール側でアクセスを許可するモジュールを指定して、限定公開することもできます。

module com.foo.secret {
    permits com.foo.lib;
}

と書くと、com.foo.libからのrequiresのみに限定することができます。

module com.foo.lib {
    requires com.foo.secret;
}

は許可されているのでOKですが、

module com.foo.app {
    requires com.foo.secret;
}

はそれ以外のモジュールからの依存になるのでNGです。なんかエラーになるんですかね。

Optional modules

もしあったら使うけど、無くても代替手段があるから別にいいよ、というオプション扱いのモジュールも指定できます。

module com.foo.app {
    requires com.foo.lib;
    requires optional com.foo.extra; // ←これ
}
Virtual modules

異なるモジュールを集約(Aggregation)した仮想モジュールを作ることもできます。

module com.foo.app {
  requires com.foo.lib.db;
  requires com.foo.lib.ui;
  requires com.foo.lib.util;
}
 
// Virtual modules
module com.foo.lib {
  provides com.foo.lib.db;
  provides com.foo.lib.ui;
  provides com.foo.lib.util;
}

上記の設定の場合、実際のモジュール依存関係としては、com.foo.app→com.foo.lib となります。


モジュールの実体とかインストールとか

Packagingの構成要素は、

Packaging = Module files
+ Libraries
+ Repositories
+ Native packages

ってことらしいです。

Module files
$ ls
src/
mods/

$ javac -modulepath mods src/com.foo.app/...

$ ls mods
com.foo.app/
com.foo.extra/
com.foo.lib/

$ ls -R mods/com.foo.app
mods/com.foo.app/com/foo/app/Main.class
mods/com.foo.app/com/foo/app/Other.class
mods/com.foo.app/com/foo/ui/Shell.class

-classpath の代わりに、-modulepathオプションが提供されます。
とはいえ、一応-classpathオプションも継続で提供されるようです。一安心。

資料にあったコード例をみると、ここに指定したディレクトリにモジュールごとのクラスファイルが格納されるようです。この辺、実際に手を動かしてないので細かいところは不明です。


次に、モジュールごとに、クラスファイルをjmodという形式でパッケージングします。

jpkgというコマンドが追加されています。

$ jpkg -modulepath mods jmod com.foo.app com.foo.extra com.foo.lib

$ ls *.jmod
com.foo.app@1.0.0.jmod
com.foo.extra@0.9a.jmod
com.foo.lib@1.0.2.jmod

というわけで、直下にjmodファイルができました。

Libraries

サードパーティから提供されているモジュールファイルをローカルのリポジトリにインストールして、自分のアプリからのモジュール依存性解決パスにいれる、などのようにモジュールをライブラリとして管理・利用するためにjmodコマンドが提供されています。


jmodのコマンド例が資料にのってますが、説明文がないので解釈に全然自信がないです。
おそらくは、↓のような話かと思われます・・・。

  • アプリ専用のモジュールライブラリ(?)*3の格納用に、カレントディレクトリ配下のmlibディレクトリを生成します。
$ jmod -L mlib create
  • あらかじめとあるディレクトリにダウンロードしておいた、サードパーティのモジュールをインストールします。
$ jmod -L mlib install \
      $EXT/edu.baz.util@*.jmod \
      $EXT/org.bar.lib@*.jmod$
  • 前述の例において、自前で生成しておいたモジュールも同じくインストールします。
$ ls *.jmod
com.foo.app@1.0.0.jmod
com.foo.extra@0.9a.jmod
com.foo.lib@1.0.2.jmod
$ jmod -L mlib install *.jmod
  • モジュールライブラリの中身を確認します。
$ jmod -L mlib ls
om.foo.app @ 1.0.0
com.foo.extra @ 0.9a
com.foo.lib @ 1.0.2
edu.baz.util @ 5.2_11
org.bar.lib @ 2.1-alpha
  • -Lオプションでモジュールライブラリを指定しつつ、-mで実行したいmainメソッドを含むモジュールを指定して、アプリを起動します。
$ java -L mlib -m com.foo.app
Welcome to Foo, v1.0.0 ...


なお、$JRE/lib/modules配下に、以下のようなJREの標準モジュール群が格納されています。

Repositories

Mavenリポジトリのように外部サーバに置いてあるjmodファイルをhttp経由で取得してインストールすることもできるよー、という話。

こんな感じ。

$ jmod add-repo http://jig.sfbay

$ jmod install -n jdk.tools
Modules needed: jdk.tools@7-ea
Bytes to download: 1.2M
Bytes to install: 2.3M

$ jmod install jdk.tools
Downloading jdk.tools@7-ea ...
Configuring jdk.tools@7-ea ...
Native packages

ざっくり言うと、jmod形式だけじゃなくて、rpmとかdebとかOS依存のネイティブなパッケージ形式にも対応しているんだよー、という話。


JDKの標準APIもモジュール化

現状では、パッケージ間の依存性の図をかくと、50のノードと171の依存関係の線が引けます。
これが、モジュラーシステムを導入すると、27のノードと95の依存関係になるそうです。



こんなところで。長すぎた・・・。

*1:Jigsawは2012年のJava8で導入予定

*2:というか、後述のようにjarファイルじゃなくてjmodファイルで管理されるわけですし

*3:mlibから類推しただけの用語なのでご注意