Java9モジュールシステム概要



Java9から導入されたモジュールシステムについてわかりやすく説明してみる。

※例によって、正確さを欠く部分、多分に想像の部分もあるのだが、気がついた方はご指摘いただきたい。

モジュールシステム概要

「publicがpublic過ぎる」

モジュールシステムが初めて提案されてから、実際に搭載されるまでに10年以上の年月がかかっているのだが、仕様が紆余曲折したからのようだ。やっと実装されたのがJava9になる。

当初から言われていたことはこうだ。

publicが、public過ぎるぞ!(Public is TOO public)

例えば、ある一つのライブラリを複数パッケージに分割した場合に、あるパッケージ内のクラスがライブラリの他の部分から見えるようになるためには、そのクラスをpublicにしなければならないが、そうすると、他すべてからも見えるようになってしまう。本当は見せたく無いのにだ。

以下の図では、本当は公開クラスXだけを見せたいのだが、XとYは別パッケージにあるため、Yをpublicとせざるを得ず、Yもまた外から見えることになってしまう。

そこで、「モジュール単位で」publicをもっと細かく制御できるようにしたわけだ。この「モジュール」とはとりあえず単純にライブラリであるjarファイルと思えばよい(本当はもう少し複雑だが)。

モジュールであるjarファイル内部にあるpublicについてどれを公開するのか、公開しないのかを決めることができる。

※当然だが、protected、private、パッケージprivateはこれまで通りで変更は無い。

モジュールにおけるpublicの意味は、次の三通りになる。

  1. これまで通りに他全てから見える(本当にpublic)
  2. それを定義したモジュールの中でからのみ見える(いわばモジュールプライベート)
  3. 指定された他のモジュールからのみ見える(指定したお友達にのみpublic)

1.はこれまでのpublicと同じ意味だ。

2.は言うまでもなく、publicとなっていても実際にはpublicではなく他からは利用できない。それが定義されたモジュールの中でしか見えない。

3.は特殊であり、見せる対象の他モジュールを指定するというものだ。

モジュールの定義

モジュールの定義としては、module-info.javaに記述し、コンパイルされたmodule-info.classがjarのトップ(パッケージ無し)位置に格納される(本当はjarだけではないのだが、とりあえず)。

上記の三つのpublic種類を表すには以下だ。

module foo {
  // fooクラス中のこのパッケージのpublicは全面公開する
  exports foo.sample1;
  // 指定された他のモジュールbar1, bar2にだけ公開する
  exports foo.sample2 to bar1, bar2;
  // 上記以外のfooモジュールのpublicクラスは他からは見えない
}

お友達機能

上の「exports foo.sample2 to bar1, bar2」という、公開先のモジュールを制限する機能は何のためにあるかと言えば。。。

例えば、JRE内部のモジュールにはマシンメモリにアクセスする機能があるのだが、これはユーザ側には見せたくない。セキュリティ上の問題があるからだ。

しかし、JRE内部の他のモジュールはこれを使いたい。この場合に、単純に「publicでは無い」としてしまうと、誰も使えなくなってしまう。

そこで、JRE内部の他のモジュールをお友達としてexports toで指定する。これにより、指定されたモジュールのみがそこにアクセスできるというわけだ。

もしこの機能が無ければ以前と同じ問題が発生してしまう。JREは複数のモジュールから構成されるのだが、それぞれがpublicかprivateのいずれかしか決められないと、必要以上にpublicにする他は無くなってしまうからだ。

モジュール名の一意性確保

ここで疑問が起こるのだが、もし先の「foo.sample2」パッケージがbar1, bar2というモジュールにのみ公開されているのであれば、こちらが作ったモジュールに、bar1もしくはbar2という名称をつけてしまえば良いのではないか?

しかし、それはできないらしい。Java9は実行時にモジュール名の一意性をチェックするらしく、複数の同じモジュール名称を認めないのである。既にbar1, bar2というモジュール名が存在していれば、その名称を使うことはできない。

つまりは、モジュール名称もJavaパッケージ名称と同様に、ドメインを逆向きにしたものにせよ、そのパッケージ名称にちなんだものにせよということだ。例えば、モジュール内のパッケージ名称が、

com.gwtcenter.sample, com.gwtcenter.sample.foo, com.gwtcenter.bar

のようなものであれば、当然モジュール名称は「com.gwtcenter.sample」にするのが自然だ。

モジュール依存の指定が必要

実はmodule-info.javaには、publicを公開にする・しないの他に必須のものがある。それは、「他のどのモジュールに依存しているのか」をすべて記述しなければならないのだ。

もちろん、既に各クラスにはimportによって依存クラスが指定されているのだが、その上にさらに、module-info.javaの中に「それらのimportするクラスが所属するモジュール名」を記述しなければならないのである。

※ただし、StringやらInteger、java.utilパッケージ等などのjavaの基本的なクラスの所属するモジュール「java.base」はデフォルトで指定されていることになっているので記述は必要ない。

例えば、JREに含まれるjava.sqlパッケージを使用したい場合、たとえクラス中に「import java.sql.*」と記述していても、その上でさらにモジュールに以下を記述する必要がある(java.sqlはjava.baseモジュールには含まれていない)。

module foo {
  requires java.sql;
  ... 他の定義 ...
}

かなり面倒な話になっているのだが、しかしこれで得られるメリットは大きいのである。モジュール間の依存関係を得ることができるわけだ。

モジュール間の依存関係とその利用

このように、基本的には「jarごとにあるモジュール定義」(本当は違うのだが)によって、モジュール間の依存関係が決定されている。アプリもモジュールの一つであることに注意する。

当然だが、この依存関係は一方向であり、ループしたり、相互参照はしない。そんなことになっていれば、エラーになるようだ。

この依存関係を得ることによって、何がうれしくなるかと言えば、「本当に必要なモジュールが何かがわかる」ということである。

例えば、JREには巨大なライブラリ(モジュール集合体)が含まれており、その中にはAWTやらSwingやらのGUI関連のものがあるのだが、サーバサイドJavaには必要無い。それがモジュールの依存関係によって明確になり、不要なものを排除することができるのである。

これは何を意味しているかと言えば、ある実行環境にJavaをインストールする場合に、インストールと言うよりもむしろ、「必要なものだけを取捨選択して、Javaと共にアプリをインストール」ことができるわけである。

つまりは、必要最小限の「カスタムJRE」を作成してそれをアプリと共にデプロイすることができるということだ。

jlinkとjmod

このカスタムJREを作り出す機能がjlinkとjmodという仕組みだ。これは、CやC++での実行形式を作ること似ている。これらにおいては、実行形式を作成するのにリンカを使うのだが、その際に指定されたライブラリの「本当に必要な部分」のみを自動的に実行形式に格納する仕組みがある。

これまでのJavaでは、使っていそうなものすべてを実行環境に入れ込むことしかなかった。

  • そもそもJREライブラリ内部がごちゃごちゃに絡み合っており、切り離しが難しかった
  • プログラムの字面上は参照していなくとも、リフレクションによって実際には利用しているかもしれない

等の理由があったからだ。モジュールシステムの導入によって、これらの問題が解決した。

  • JREライブラリ内部がすっきりとモジュール分割された
  • たとえリフレクションを使っても、requires宣言していないモジュールは使用できない。

ということのようだ。そして、Java9以降のJavaのインストールを見ればわかるのだが、これまではrt.jarという一つの巨大なライブラリだったのだが、それが消えて、jmodsフォルダの中に*.jmodという複数のファイルが現れている。

このjmodというのが、要するにリンク用のライブラリということらしい、これをjlinkで処理することにより、カスタムJREが作成できる。。。らしい、まだ実際には試していないのだが。

移行の方法

このように、夢のようなJavaモジュールシステムなのだが、しかし、当然のことながら移行は大変である。Java9以降では、モジュールシステムに対応したライブラリ構造になっているのだが、しかし、アプリ側はそう簡単には行かない。

アプリ本体は何とでもなるとしても、その利用するJRE以外のライブラリjarにはmodule-info.javaが含まれていないからだ。したがって、完全に理想的なモジュールシステムに移行するには、世にあるMavenリポジトリに格納されているjarファイルすべてでモジュール定義がされなければならない。。。はずなのだが、これに対処する方法も用意されている。

アプリにmodule-info.javaを置いてモジュール宣言

いったんアプリを「モジュール」と宣言した場合、トップにmodule-info.javaファイルを置いた場合だが、それは「モジュールシステムを使う」という宣言になる。

モジュールシステムをまだ使いたくない場合には、このファイルを作成しなければ良い話だ。レガシーなJavaプログラムはすべてそうなっているので、ほぼ問題は起こらない(JRE内部の無理矢理使用等を除く)。

しかし、モジュールシステムにしたいのだが、使用するライブラリjarは、ほぼモジュールシステムに対応していないという場合はどうなるのだろうか?

モジュール宣言の無いjarのモジュール名称1

アプリがモジュール宣言した場合、モジュール宣言の無いライブラリjarを使う場合にも、それをモジュールとしてrequiresしなければならないのだが、この名前がどうなるかと言えば、単純にjarファイル名称からハイフン以降を除いたものになるらしい。

例えば、「guice-4.0.jar」というjarファイル名の場合には、「guice」がモジュール名となる。

module foo {
  requires guice;
}

しかし、試しにEclipseでこれをやってみたところ、このguiceという名前について「この名称は不安定だからやめた方がいいよ」という警告が出てきた。さすがにエラーにはしないが。

モジュール宣言の無いjarのモジュール名称2

よりまともなやり方としては、各jar製作者がマニフェストファイルに、Automatic-Module-Nameという属性でモジュール名(のみ)を指定することである。jarファイルには必ずマニフェストファイルがあるのだが、Automatic-Module-Name属性はJava9以前には存在しないため、それらで読み込まれた場合には単純に無視されるだけになる。

この例としては以下だ。

Manifest-Version: 1.0
Automatic-Module-Name: com.fasterxml.jackson.core
Bnd-LastModified: 1504831683959
Build-Jdk: 1.7.0_79
Built-By: tatu
Bundle-Description: Core Jackson processing abstractions (aka Streaming 
 API), implementation for JSON

どうせ修正するなら、module-info.javaを入れればいいじゃないかと思うが、それは面倒だ。なぜなら、module-info.javaはjava9以降でないと作れないからだ。他はjava9以前の形式の.classファイルでmodule-info.classだけJava9以降の.class形式にしないといけない。つまり、コンパイラを変えねばならない。

※これについてはhttp://www.ne.jp/asahi/hishidama/home/tech/java/version.html#h_class_versionを参照のこと。

バージョンごとに.classを変える方法

結局のところ、Java9以前でも動作しつつ、Java9以降のモジュール機能を使うには、コンパイラを変えて.classを作り出さねばならないわけだが、これをよりラジカルに行う方法もある。それが「マルチリリースjar」という仕様だ。

これは一つのjarファイルの中に複数のバージョンの.classファイル(別のJavaバージョンでコンパイルされた.classファイル)を格納できるというものなのだが、あまりに面倒だし、個人的には全く感心しない。

それよりも、jarファイル自体をjavaバージョンごとに変更した方が良いと思われる。

ライブラリjar自体の依存はどう解釈されるのか?

ともあれ、module-infoが無い場合、つまりjarファイル名称から作られたモジュール名称や、Automatic-Module-Nameからモジュール名称を得ることはできるのだが、しかし、このjarファイルが、例えばJREのどのモジュールに依存するのかはわからない。当然想像できるように「JREのすべてのモジュールに依存する」とみなされる。

したがって、モジュール名が安定だろうが不安定だろうが、「必要最小限のカスタムJRE」を作れないことになる。。。と思ったのだが、実際にはjdepsを使って依存関係を取得することができるようだ。

これについては別投稿とする。

その他の参考