Java Platform Module System は Java SE 9 以降で導入された仕様であり、いわゆるクラスパス地獄だとか JAR 地獄といわれる問題を回避するために提案されました。
モジュールの定義情報は module-info.class
(ソースとしては module-info.java
) に格納されています:
module モジュール名 {
requires 依存するモジュール;
exports 公開するパッケージ;
}
モジュールシステムにより、依存関係が明確になったり、呼び出されたくないクラスを非公開にできたりします。実行時のオプションも変化しており、モジュールの場所を示す --module-path
と、実行するモジュール・クラスを示す --module
を指定します:
java --module-path "<実行するモジュールを含むJAR>.jar;<依存モジュールを含むJAR1>.jar;<依存モジュールを含むJAR2>.jar;..." --module <モジュール名>/<クラス名>
一方、module-info.class
がない場合は「自動モジュール」(モジュールパスにある場合。クラスパスの場合は「無名モジュール」)として、以下のように扱われます1:
- すべてのパッケージを
exports
している - モジュールグラフに読み込まれたすべてのモジュールを
requires
している
多少の複雑性はありますが、地獄を回避する代償なので仕方ありません。さて、そうなると既存の非モジュールなライブラリをオレオレモジュールにして JAR 地獄を回避したいという気持ちも湧いてきます2。そのような場合は、既存の JAR ファイルを元にモジュール定義情報を書き加えてしまいましょう。
ModiTect で非モジュールな JAR をモジュール化してみる
ModiTect はモジュールシステムを操作する Maven プラグインです。たとえば jetty-javax-servlet-api などの Jetty が内部的に使用するライブラリで、非モジュールな JAR をモジュール化するのに使用されています3。
今回の場合、以下の流れとなるようにプロジェクトを作成します:
- 依存関係として元の JAR を取得する(今回のサンプルでは
commons-codec
を題材としました) -
maven-dependency-plugin
で依存関係のソースを展開する -
build-helper-maven-plugin
で依存関係のソースをソースコードとして追加する - コンパイルする
-
moditect-maven-plugin
でモジュール定義情報を追加する - 再度 JAR 化する
pom.xml
はこのようになります(全文はこちら):
<plugins>
<!-- 依存関係のソースを target/sources/ に展開する -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<?m2e ignore?>
<id>unpack-source</id>
<phase>generate-sources</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<classifier>sources</classifier>
<failOnMissingClassifierArtifact>false</failOnMissingClassifierArtifact>
<outputDirectory>${project.build.directory}/sources</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- 展開した依存関係のソースをソースコードとして追加する -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/sources</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<!-- コンパイル -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
<release>11</release>
</configuration>
</plugin>
<!-- module-info.class を追加する -->
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<executions>
<execution>
<id>add-module-info</id>
<phase>package</phase>
<goals>
<goal>add-module-info</goal>
</goals>
<configuration>
<overwriteExistingFiles>true</overwriteExistingFiles>
<module>
<!-- ここに module-info.java の内容を記述する -->
<moduleInfoSource>
// オレオレモジュール名
module com.github.yokra9.moditect {
// エクスポートするパッケージを指定
exports org.apache.commons.codec.binary;
}
</moduleInfoSource>
</module>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
オレオレモジュール JAR を利用する側のプロジェクトでは、module-info.java
でオレオレモジュールを requires
してあげます:
module com.github.yokra9.moditectSample {
requires com.github.yokra9.moditect;
}
エクスポートしているパッケージは org.apache.commons.codec.binary
のみなので、それ以外のパッケージをインポートするとコンパイルエラーになります:
// コンパイルが通る
import org.apache.commons.codec.binary.Base64;
// コンパイルエラーになる
import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.net.URLCodec;
上述の通り実行時にはモジュールパスとメインモジュール・メインクラスを指定する必要がありますが、ModuleMainClass
が定義された JAR ファイルではクラス名の指定を省略できます。
# メインモジュールとオレオレモジュールにパスを通し、メインモジュールとメインクラスを指定する
java --module-path "moditect-sample-0.0.1.jar;yokra9-commons-codec-0.0.1.jar" --module com.github.yokra9.moditectSample/com.github.yokra9.moditectSample.Binary
# maven-jar-plugin v3.1.2 以降では configuration.archive.manifest.mainClass を定義していると自動的に ModuleMainClass も設定される
java --module-path "moditect-sample-0.0.1.jar;yokra9-commons-codec-0.0.1.jar" --module com.github.yokra9.moditectSample
ということで com.github.yokra9.moditectSample
を require
しているソースからしか呼び出せない安全(?)な JAR を作成できました。このようなリパッケージド版 JAR を活用できる場面の一例が、モジュールパス内に同ライブラリの複数バージョンが設置されうるケースです。たとえば、自作ライブラリの依存として commons-codec
を追加したいとして、そのライブラリのユーザが別バージョンの commons-codec
を使用したい可能性もあります。このとき、自作ライブラリ側でリパッケージド版を使用しておけばユーザ側は自由にバージョンを選択できます。
さて、モジュール情報がいじれるということは、逆に exports
されていないパッケージを exports
させることも可能な訳ですが、こちらはあまりお勧めできません。そもそも外部から利用されないことを前提としたパッケージですから、特に断りなく仕様が変わったり使えなくなったりする可能性があります。あまりアクロバティックな使い方はせず、JAR 地獄を回避する用途にとどめることをお勧めします。
参考リンク
- ModiTect
- モジュールシステムを学ぶ
- sbt-assembly で FAT JAR を生成しようとしたら module-info.class の重複で怒られた話
- Javaのモジュールシステムを理解しよう(その1)
- Automatic Moduleを(続)
- m2eでプロジェクトインポートすると出るエラー "Plugin execution not covered by lifecycle configuration"
-
Java Platform Module System についてはこちらの記事に詳しいです:モジュールシステムを学ぶ ↩
-
単純に非モジュールなライブラリを
requires
したいだけなら、jar --describe-module --file <JARファイル>
で確認できる自動モジュール名を指定します。 ↩