- はじめに
- 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作成に役立ちます。ただし、ランタイムに脆弱性が発見された場合に、その更新が困難になる点については十分注意する必要があります。