
- はじめに
- Hello World アプリの準備
- Hello World アプリのコンパイルと実行
- jlink でカスタムJREを生成する
- モジュールイメージ
- ランチャーを生成
- Gradle からイメージを作成
- jlink-gradle プラグイン
- まとめ
はじめに
Java Platform Module System (JPMS) に合わせて導入された jlink。
jlink により、必要なモジュールに絞ったアプリケーションの配布イメージを簡単に作成することができます。
ここでは、コマンドラインから jlink を操作することから初め、Gradle によるイメージ作成方法までを説明します。
Hello World アプリの準備
最初にコマンドラインから jlink コマンドを操作することで、イメージ作成の流れを見てみましょう。
簡単なソースを準備します。
$ mkdir jlink $ cd jlink $ touch module-info.java $ mkdir example $ touch example/App.java
アプリケーションは Hello World アプリとしますが、コンソール出力だけでは寂しいので、Swing で描いたウインドウに Hello World を出力するアプリにします。
example/App.java を以下のように編集します。
package example; import javax.swing.*; import java.awt.*; public class App { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame("swing"); frame.setMinimumSize(new Dimension(300, 200)); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JLabel label = new JLabel("Hello World"); frame.getContentPane().add(label); frame.pack(); frame.setVisible(true); }); } }
フレームに Hello World というラベルを追加するだけの簡単なものです。
jlink ではモジュールによる操作が基本になるので、module-info.java も準備します(モジュール定義の無い Automatic Module も扱うことはできます)。
ここでは、swing の依存として java.desktop モジュールの依存を定義します。
module example.app {
requires java.desktop;
}
今回のアプリケーションには、java.desktop モジュールが必要であることを定義しています。
Hello World アプリのコンパイルと実行
作成したソースをコンパイルしましょう。
$ javac module-info.java example/App.java
以下のようにコンパイルされます(簡単のため、クラスファイルはソースと同じディレクトリにいれてしまっています)。

実行します。
$ java --module-path . --module example.app/example.App
--module-path には現在のディレクトリを指定しています。これにより、モジュールの検索で、今回作成した example.app モジュールを見つけることができるようになります。
$JAVA_HOME/jmods も自動的にモジュールパスに追加されるので、今回必要とする java.desktop モジュール、およびそれが依存するモジュールも見つけることができます(java.base モジュールが解決できない場合、$JAVA_HOME/jmods がモジュール・パスに追加されます)。
アプリケーションを実行することで、以下のようなウインドウで HelloWorld が起動します。

では、ここで作成したアプリケーションからイメージを作成しましょう。
jlink でカスタムJREを生成する
作成したアプリケーション専用のカスタムJREを作成します。
jlink コマンドを以下のように実行します。
$ jlink --module-path . --add-modules example.app --output custom-jre
example.appモジュールが追加された、カスタムのJRE(Java Runtime Environment) が生成されます。
jlink の代表的なオプションは以下となります。
--module-path modulepath: モジュール・パスを指定(-p)- デフォルトのモジュール・パスは
`$JAVA_HOME/jmods - オプションが指定されていても
java.baseモジュールを解決できない場合は$JAVA_HOME/jmodsをモジュール・パスに追加
- デフォルトのモジュール・パスは
--add-modules mod [, mod...]: 指定のモジュールを追加--compress={0|1|2}: リソースを圧縮(0: 圧縮なし,1: 定数文字列の共有,2: ZIP)--no-header-files: ヘッダー・ファイルを除外--no-man-pages: マニュアル・ページを除外--output: 出力先を指定
カスタムJREは以下のようになります。

このカスタムJREは、example.appモジュールと、それが依存するモジュールが含まれており、bin にある java コマンドでアプリケーションを単独で実行できます。
custom-jre/bin/java --module example.app/example.App
カスタムのJREを実行することで、以下のようなウインドウが起動します。

このカスタムJREは、プラットフォームに Java が存在せずとも、単独で起動するアプリケーションになっており、カスタムJREをzipアーカイブとして単独配布することができます。
モジュールイメージ
先程生成したカスタムJREのlib ディレクトリには modules というファイルが生成されています(以下は表示上、 lib ディレクトリのファイルを省略したものです)。

このmodules に含まれるモジュールを確認しましょう。
以下のように確認できます。
$ custom-jre/bin/java --list-modules example.app java.base@20.0.1 java.datatransfer@20.0.1 java.desktop@20.0.1 java.prefs@20.0.1 java.xml@20.0.1
ランタイム・イメージ内のモジュールがリストされます。
作成した example.app モジュールと、それが依存するモジュールのみが含まれていることが確認できます(ソースファイルとクラスファイルを同じディレクトリに入れてしまっているので、ソースファイルまで含まれていますが、通常は別ディレクトリになるので、ソースファイルは含みません)。
直接 modules の中身を確認するには jimage コマンドで対象の modules を指定します。
$ jimage list --verbose custom-jre/lib/modules | head -n 15
jimage: custom-jre/lib/modules
Module: example.app
Offset Size Compressed Entry
117678 1423 0 example/App.class
117157 521 0 example/App.java
24522 223 0 module-info.class
110960 49 0 module-info.java
Module: java.base
Offset Size Compressed Entry
119101 41 0 META-INF/services/java.nio.file.spi.FileSystemProvider
119142 1357 0 apple/security/AppleProvider$1.class
120499 2003 0 apple/security/AppleProvider$ProviderService.class
大量なので最初の15行だけ表示しました。modules に含まれるファイルが確認できます。
jimage で extract を指定すれば、modules に含まれるファイルを取り出すこともできます。
Module: の行に絞ることで、含まれるモジュールを確認することができます。
$ jimage list custom-jre/lib/modules | grep Module: Module: example.app Module: java.base Module: java.datatransfer Module: java.desktop Module: java.prefs Module: java.xml
example.app が依存する java.desktop が、推移依存するモジュールを含めて表示されていることが確認できます。

なお、java.prefs はなぜかAPIドキュメントに記載されていませんが、以下のように java.desktop が依存するモジュールです。
module java.desktop {
requires java.prefs;
requires transitive java.datatransfer;
requires transitive java.xml;
...
ランチャーを生成
jlink で --launcher を指定することで、アプリケーション用のランチャーを生成できます。
以下のように実行すれば
jlink --module-path . --add-modules example.app --output custom-jre --launcher app=example.app/example.App
bin 内に app というランチャーが生成されます。

ランチャーを使うことで、java コマンドを使うことなくアプリケーションを実行できます。
$ custom-jre/bin/app
同じようにアプリケーションが起動します。

Gradle からイメージを作成
コマンドライン操作で、jlink コマンドの操作は理解できましたが、通常の開発をコマンドラインから行うことはありません。
ここでは Gradle でイメージ作成を行ってみましょう。
プロジェクトを作成します。
$ mkdir example $ cd example $ gradle init --type java-application --dsl kotlin --test-framework junit-jupiter --project-name example --package example
テストコードは今回は使わないので削除してしまいます。
module-info.java ファイルを準備します。
$ rm app/src/test/java/example/AppTest.java $ touch app/src/main/java/module-info.java
app/build.gradle.kts は以下のようなります。
plugins {
application
}
repositories {
mavenCentral()
}
dependencies {
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(20))
}
}
application {
mainModule.set("example.app")
mainClass.set("example.App")
}
example/App.java と module-info.java は前述と同様の内容に編集します。
同じ内容なのでここでは省略します。
ここまでで、./gradlew run によりアプリケーションが起動できるようになっているはずです。
では、jlink の操作に移りましょう。といっても、Exec タスクの commandLine でコマンドを指定するだけです。
以下のようになります。
tasks.register<Exec>("jlink") { group = "Build" description = "Generate Java runtime image" dependsOn("jar") val javaHome = System.getProperty("java.home") val buildDir = layout.buildDirectory.get().asFile doFirst { delete("$buildDir/image") } commandLine( "$javaHome/bin/jlink", "--module-path", listOf("$buildDir/libs", configurations.runtimeClasspath.get().asPath, "$javaHome/jmods").joinToString(File.pathSeparator), "--add-modules", application.mainModule.get(), "--launcher", "${application.applicationName}=${application.mainModule.get()}/${application.mainClass.get()}", "--output", "$buildDir/image", "--strip-debug", "--no-man-pages", "--compress", "2") }
先のコマンドライン操作の例に加え、jlinkコマンドには --strip-debug, --no-man-pages, --compress などを指定しました。これによりイメージサイズの削減が行なえます。
作成した jlink タスクを実行します。
$ ./gradlew jlink
以下のようにapp/build/image にランタイムイメージが作成されます。

ランタイムイメージの作成だけであれば、独自タスクを定義することで事足りますし、プラグインに頼る必要はありません(プラグインがメンテナンスされなくなるケースも多いので)。
非モジュール・ライブラリとの相互運用や、異なるプラットフォーム向けのランタイムイメージを同時に作成したいといった場合には、プラグイン使ってしまったほうが簡単です。
jlink-gradle プラグイン
org.beryx.jlink プラグインを使ってみましょう。
plugins {
application
id("org.beryx.jlink") version "2.26.0"
}
repositories {
mavenCentral()
}
dependencies {
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(20))
}
}
application {
mainModule.set("example.app")
mainClass.set("example.App")
}
プラグインにより、jlink タスクが登録されるので、以下のようにイメージ作成が可能です。
$ ./gradlew jlink
jlinkZip タスクにより、イメージがzipアーカイブとして作成されます。
$ ./gradlew jlinkZip
さらに jpackage タスクで(jpackageコマンドによる)インストーラを作成することもできます。
$ ./gradlew jpackage
jpackage は Windows の場合は別途 WiX を導入しておく必要があります。
jpackageコマンドについては以下を参照してください。
以下のように配布用パッケージ image.zip、インストーラapp-1.0.dmg が生成されているのが確認できます。

その他 org.beryx.jlink プラグインについては以下の公式ドキュメントを参照してください。
まとめ
今更感はありますが、Java Platform Module System (JPMS) で導入された jlink コマンドの使い方について、コマンドライン操作から、Gradle での利用までを説明しました。
アプリケーションの配布の他、Docker イメージに含めるスリムなJRE作成に役立ちます。ただし、ランタイムに脆弱性が発見された場合に、その更新が困難になる点については十分注意する必要があります。
