以前、Gradle3におけるJavaプロジェクトのビルドについてのエントリーを書きましたが、Gradle3のJVM component modelって、まあ、わりと、Java9のJigsawみたいなものなんですね。で、Jigsaw自体知っている人が多いと思いますが、あらためてJigsawが解決したい問題(と僕が勝手に認識しているもの)とその解決方法について、雑にまとめてみることにしました。
なお、Gradle3のJVM component modelでのビルドについてはこちらを参照。
JVM component modelとJigsawとの関連について、少し言及があるエントリーについてはこちらを参照。
あと、Jigsawについて一番よくまとまっている資料見れば、このエントリーを見る必要はないと思います。
www.slideshare.net
依存性地獄
まあ、rt.jar
を小さく分割して、必要なjarだけをロードするようにして、Javaの起動を速くしたいという問題もあるわけですが、それはそれで置いといて、もう少しだけ高いレベルでみた時に、依存性地獄を解決したいという問題があるかな、と勝手に解釈しています。
例えば、あるアプリケーションを作った時に、次のような依存性のツリーができたとします。
app-1.1.jar ├── service-1.1.jar │ └── util-1.1.jar ←こいつと └── repo-1.1.jar └── util-1.0.jar ←こいつがコンフリクトしてる
上記のツリーの例ではutil-1.1.jar
とutil-1.0.jar
がバージョンのコンフリクトを起こしています。このようなバージョンのコンフリクトが発生することはよくある話です。Gradleでは、特に指定がない限り、最新のバージョンのjarを使用するように依存ツリーを修正します。したがって、アプリケーションの依存性は次のようになります。
app-1.1.jar ├── service-1.1.jar │ └── util-1.1.jar └── repo-1.1.jar └── util-1.1.jar
ただ、このように依存性を解決した時に、コンパイルが落ちてしまうことがあります。「クラスが見つかりません」的なエラーあるいはコンパイルできたとしても、実行時にNoClassDefFoundError
。ただ、このアプリケーションはバージョンが1.0
のときはコンパイルできていたんです。その頃の依存性ツリーはこのような感じでした。
app-1.0.jar ├── service-1.0.jar │ └── util-1.0.jar └── repo-1.0.jar └── util-1.0.jar
なぜ、こんなことに…
依存ライブラリー
さて、問題となっているutil.jar
の中身が次のような構成になっているとします。
util.jar ├── com.util.api <- ユーザーにはこっちだけ使ってもらいたい └── com.util.internal <- 内部用なのでユーザーには使ってもらいたくない
アクセス修飾子
さて、Javaのクラスに付与できるアクセス修飾子には次の3つがあり、アクセス範囲を限定できます。
修飾子 | 利用可能な範囲 |
---|---|
public |
どのパッケージからでも利用可能 |
(package private) |
同一パッケージ内のみ利用可能 |
private (インナークラスのみ) |
同一のクラス内のみ利用可能 |
しかし、api
パッケージにあるインターフェースの実装をinternal
パッケージで行ったとしても、インスタンスを割り当てるなどする必要があるため、どうしてもユーザーには使ってもらいたくないinternal
パッケージのクラスをpublic
にせざるを得ない状況が発生します。
するとユーザーであるrepo.jar
にこんなことをするクラスが現れます。
package com.repo.user; import com.util.internal.SecretImpl; public class Mal { // 実装省略 }
依存性地獄
さて、util.jar
のバージョンがあがり、util-1.1.jar
となりました。internal
パッケージの中身を更新させた結果、com.util.internal.SecretImpl
がなくなりました。別にこれは悪いことではありません。元々内部的に使っていたクラスを削除しただけですので。
するとユーザーであるrepo.jar
はutil-1.0.jar
からコンパイルエラーになってしまうutil-1.1.jar
にあげることができなくなります(普通はこの段階で修正するのですが、リソースの関係上修正できなかった)。仕方なしにutil-1.0.jar
に依存することになります。
そしてアプリケーション全体としてutil.jar
のバージョンコンフリクトが発生して、バージョンが最新のものを利用する選択をおこなったため、アプリケーション全体としてコンパイルエラーまたは、実行時にNoClassDefFoundError
が発生します。
このような状態を依存性地獄とかDependency Hellとか呼びますね。
明示的な公開・依存の設定
依存性地獄の発生までの経緯を見てきましたが、Jigsawではこれらをどのように解決するか雑に見ていきます。
先ほどのutil.jar
のパッケージ構成を再掲します。
util.jar ├── com.util.api <- ユーザーにはこっちだけ使ってもらいたい └── com.util.internal <- 内部用なのでユーザーには使ってもらいたくない
このutil.jar
の公開ポリシーを設定するために、module-info.java
というファイルをルートディレクトリーに作成します。例えば、src/util
以下にutil.jar
のソースコードがある場合、src/util
の下にmodule-info.java
を作成します。そして、作成したmodule-info.java
を次のように記述します。
module util { exports com.util.api; }
exports
でマークされたcom.util.api
パッケージはこのutil
を使うユーザーが利用できるパッケージになります。一方、exports
されなかったcom.util.internal
パッケージはutil
を利用するユーザーからは見ることができません。
一方のutil
のユーザーであるrepo
の方はどのように記述するのか見てみます。repo
のソースコードがsrc/repo
以下にあるとした場合、src/repo/module-info.java
を作成します。
module repo { requires util; exports com.repo.spi; exports com.repo.api; }
このように、util
を利用することを宣言するrequires
を記述します。なお、このrepo
に依存するapp
ではrepo
にて公開されているcom.repo.spi
、com.repo.api
パッケージを利用することができ、util
で公開されているcom.util.api
を利用することができません。
なお、app
でutil
のcom.util.api
を利用できるようにする場合は次のように記述します。
module repo {
requires public util;
exports com.repo.spi;
exports com.repo.api;
}
requires
にpublic
というキーワードを付与することで、依存しているライブラリー共々公開することができるようになります。
では、先ほどの問題になっていたcom.repo.user.Mal
クラスはコンパイルができるのでしょうか?
ここまで読まれた方ならすぐに答えられると思います。答えは「com.util.internal.SecretImpl
クラスが見つからないためコンパイルエラーが発生する」になります。
以上、Jigsawについて雑にまとめました。slideshareの資料のほうが絶対に内容は充実しています(僕のはつまみ食いだけです)。
で、Gradle3におけるJavaプロジェクトのビルド入門(3)というエントリーを今度書きますが、ここに書いたJigsawのような仕組みをGradle3でも実現する方法を紹介する予定です。
【追記】公開した後にさくらばさんから指摘があったので修正した。
@mike_neck たぶん参照している資料が古いからだと思うけど、今はhttps://t.co/A9SP30ohNjにバージョンは書かないです。バージョンはjarコマンドのオプションで設定します。
— Yuichi Sakuraba (@skrb) 2015, 11月 21
module-info.java
にはバージョンを記述せず、コンパイル時に指定するようです。
【誤】
module util@1.0 { exports com.util.api; }
【正】
module util { exports com.util.api; }
で、コンパイル方法は次のとおりですが、まあ、モダンなビルドツールがすぐに対応してくれるでしょう。
$ #とりあえずjavacのコンパイル結果を置いておくディレクトリー $ mkdir tmp $ #生成されたjarを置いておくディレクトリー $ mkdir jars $ # srcディレクトリー以下の全javaファイルをコンパイルしてモジュールごとに分ける $ javac -d -modulesourcepath src $(find src -name "*.java") $ # tmp/util以下のclassファイルをutil-1.0.jarというバージョン1.0のjarに固める $ jar --create --file=jars/util-1.0.jar --module-version=1.0 -C tmp/util $ # tmp/repo以下のclassファイルをrepo-1.0.jarというバージョン1.0のjarに固める $ jar --create --file=jars/repo-1.0.jar --module-version=1.0 -C tmp/repo $ # tmp/service以下のclassファイルをservice-1.0.jarというバージョン1.0のjarに固める $ jar --create --file=jars/service-1.0.jarr --module-version=1.0 -C tmp/service