JDK9でのjavapackagerについて

はじめに

以前自分の blog にて JDK に付属しているツールである javapackager について紹介したことがあります。このツールは主にクライアントサイド Java アプリケーションを配布可能な形態でパッケージングするためのツールです。ネイティブインストーラも生成することができます。

aoe-tk.hatenablog.com

このエントリではネイティブパッケージに含まれるランタイムについて、次のようなことを述べていました。

昔は JDK を丸ごと放り込むという豪快な感じになっていましたが、最近は結構スリムアップしました。JDK9 の Jigsaw が入るともっと効率よくなるでしょう。

そして遂に Java9 がリリースされました。JDK9 の javapackager のマニュアル には次のような記載があります。

For self-contained applications, the Java Packager for JDK 9 packages applications with a JDK 9 runtime image generated by the jlink tool.

確かに jlink と連動してランタイムイメージを作ると記載されています。つまり、module-info.java を作っていれば、必要なモジュールだけを含んだランタイムイメージを作成することになり、アプリケーションの配布サイズが小さくなることが期待されます。というわけで早速 JDK9 の javapackager を試してみることにしました。

パッケージ対象となるアプリケーション

まず、パッケージ対象となるアプリケーションを作ります。とてもシンプルな JavaFX アプリケーションとして作ります。

package aoetk.sample;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class SampleApp extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception{
        primaryStage.setTitle("Packager Sample");
        StackPane stackPane = new StackPane();
        stackPane.getChildren().add(new Text("Packager Sample"));
        primaryStage.setScene(new Scene(stackPane, 300, 275));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

ウィンドウの真ん中にテキストを表示するだけのとてもシンプルな JavaFX アプリケーションです。これを対象にパッケージングを行ってみます。

JDK8でのパッケージング

まず、JDK8 でコンパイルし、パッケージングしてみます。パスを JDK8 に通します。

>set PATH=C:\Program Files\Java\jdk1.8.0_144\bin;%PATH%

>javac -version
javac 1.8.0_144

>where javapackager
C:\Program Files\Java\jdk1.8.0_144\bin\javapackager.exe

JDK8 の javac でコンパイルし、JAR を作ります。

>javac -d bin src\aoetk\sample\SampleApp.java

>javapackager -createjar -nocss2bin -appclass aoetk.sample.SampleApp -srcdir bin -outdir artifact -outfile packager-sample.jar

>dir artifact
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は ACFE-5623 です

 C:\Users\aoe\develop\packager-sample\artifact のディレクトリ

2017/10/08  00:32    <DIR>          .
2017/10/08  00:32    <DIR>          ..
2017/10/08  00:32             1,354 packager-sample.jar
               1 個のファイル               1,354 バイト
               2 個のディレクトリ  366,187,679,744 バイトの空き領域

この JAR に対し、ネイティブインストールイメージを作ります。今回はインストール後のイメージサイズを知りたいので、 -native の引数に image を渡してインストールイメージのみを作ります。

>javapackager -deploy -native image -outdir package -outfile packager-sample -srcdir artifact -srcfiles packager-sample.jar -appclass aoetk.sample.SampleApp -name "jdk8-sample" -title "JDK8Sample" -BappVersion=1.0 -Bwin.menuGroup="JDK8Sample"
アプリケーション・バンドルを作成しています: C:\Users\aoe\develop\packager-sample\package内のjdk8-sample
"モジュール: [java.rmi, java.sql, javafx.web, jdk.charsets, java.logging, java.xml.crypto, java.xml, jdk.xml.dom, jdk.jfr, java.datatransfer, jdk.packager.services, jdk.httpserver, javafx.base, jdk.net, java.desktop, java.naming, javafx.controls, java.prefs, java.security.sasl, jdk.naming.rmi, jdk.zipfs, java.base, jdk.crypto.ec, jdk.management.agent, java.management, java.sql.rowset, javafx.swing, jdk.jsobject, jdk.sctp, java.smartcardio, jdk.unsupported, jdk.jdwp.agent, jdk.scripting.nashorn, java.instrument, java.security.jgss, jdk.management, java.compiler, javafx.graphics, jdk.security.auth, java.scripting, javafx.fxml, jdk.dynalink, javafx.media, jdk.accessibility, java.management.rmi, jdk.naming.dns, jdk.security.jgss, jdk.localedata]をランタイム・イメージに追加しています。"
警告: Windows Defenderが原因でJavaパッケージャが機能しないことがあります。問題が発生した場合は、リアルタイム・モニタリン グを無効にするか、ディレクトリ"C:\Users\aoe\AppData\Local\Temp\"の除外を追加することにより、問題に対処できます。
結果のアプリケーション・バンドル: C:\Users\aoe\develop\packager-sample\package

これで jdk8-sample ディレクトリの下にインストールイメージが作られます。これはインストール後にインストールディレクトリに展開される構成そのものです。

f:id:aoe-tk:20171009000534p:plain

ディレクトリサイズは次のように 167MB と、単純なアプリケーションにしては随分大きなサイズになっていることが分かります。

f:id:aoe-tk:20171009000554p:plain

JDK9でのパッケージング

では Java9 で導入されたモジュールシステムを利用してパッケージングしてみることにしましょう。環境を JDK9 に変更します。

>set PATH=C:\Program Files\Java\jdk-9\bin;%PATH%

>javac -version
javac 9

>where javapackager
C:\Program Files\Java\jdk-9\bin\javapackager.exe

モジュールの設定を行います。まずはこのアプリケーションがどのモジュールを利用しているかを調べてみましょう。

>jdeps -s artifact\packager-sample.jar
packager-sample.jar -> java.base
packager-sample.jar -> javafx.base
packager-sample.jar -> javafx.graphics

java.base モジュールの他に javafx.base モジュール、 javafx.graphics モジュールに依存していることが分かります。 javafx.graphics モジュールは javafx.base モジュールに依存しているので module-info.java は次のように javafx.graphics モジュールへの依存を記載すれば OK です。

module aoetk.sample.packager {
    requires javafx.graphics;
    exports aoetk.sample;
}

これを JDK9 のコンパイラを使ってコンパイルし、JAR を作ります。

>javac -d bin src\module-info.java src\aoetk\sample\SampleApp.java

>javapackager -createjar -nocss2bin -appclass aoetk.sample.SampleApp -srcdir bin -outdir artifact -outfile packager-sample.jar

>dir artifact
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は ACFE-5623 です

 C:\Users\aoe\develop\packager-sample\artifact のディレクトリ

2017/10/08  00:52    <DIR>          .
2017/10/08  00:52    <DIR>          ..
2017/10/08  00:52             1,642 packager-sample.jar
               1 個のファイル               1,642 バイト
               2 個のディレクトリ  366,004,936,704 バイトの空き領域

これを同じように javapackager を用いてインストールイメージを作るのですが、モジュールを使ったアプリケーションをパッケージングする場合は指定する引数が異なります。ですが、現在の javapackager のマニュアル はこの変更に追いついていません...。執筆時点で javapackager のコマンドラインオプションについて正確な記述があったのは JEP 275: Modular Java Application Packaging のみでした。

具体的には -srcdir-srcfiles-appclass の指定が無くなり、モジュールを使った Java アプリケーションを java コマンドで実行するときと同じようにモジュールパス (対象モジュールの JAR が置かれているディレクトリ) を -p (もしくは --module-path) で、実行クラスを -m (もしくは --module) でモジュール名とクラス名を組み合わせて指定します。

>javapackager -deploy -native image -outdir package -outfile packager-sample -p artifact -m aoetk.sample.packager/aoetk.sample.SampleApp -name "jdk9-sample" -title "JDK9Sample" -BappVersion=1.0 -Bwin.menuGroup="JDK9Sample"
アプリケーション・バンドルを作成しています: C:\Users\aoe\develop\packager-sample\package内のjdk9-sample
モジュールaoetk.sample.packagerは存在しません。
"モジュール: [aoetk.sample.packager]をランタイム・イメージに追加しています。"
モジュールaoetk.sample.packagerは存在しません。
警告: Windows Defenderが原因でJavaパッケージャが機能しないことがあります。問題が発生した場合は、リアルタイム・モニタリン グを無効にするか、ディレクトリ"C:\Users\aoe\AppData\Local\Temp\"の除外を追加することにより、問題に対処できます。
結果のアプリケーション・バンドル: C:\Users\aoe\develop\packager-sample\package

これで jdk9-sample ディレクトリの下にインストールイメージが作られます。

f:id:aoe-tk:20171009000635p:plain

ディレクトリサイズを見ると 84.9MB と JDK8 の場合に比べて半分程度になっていることが分かります。確かに Jigsaw の効果が出ていますね!

f:id:aoe-tk:20171009000656p:plain

インストールイメージの中身について

なお、インストールイメージの中身を調べてみると、こちらも興味深いものがありました。 アプリケーションや JRE の JAR が見当たらない のです! runtime ディレクトリの下を覗いてみると、何やら modules というそれっぽいファイルがあります。

f:id:aoe-tk:20171009000713p:plain

このファイル、JAR か JMOD ファイルかと思いきや、特に ZIP 圧縮されていません。バイナリエディタで覗いてみると時々 CAFEBABE が登場しており、単にクラスファイルを 1 つのファイルにまとめたもののように見受けられます。色々調べてみましたが、このファイルが何であるかの解説を見つけられませんでした。誰か知っている人いますか?

(2017/10/10) 追記

id:MATSUZAKI 様よりコメントで情報を頂きました。jimage 形式のファイルであるとのことです。jimage については Java Magazine の Vol.25 に説明がありました。

jimage形式は、モジュール化されたランタイムに必要なクラスやリソースを管理するコンテナの形式です。 jimageファイルは、従来のようなzipベースの圧縮ではなく、クラスやリソースを高速に検索できるようにインデックスが付けられています。jimageのコンテンツ領域には、そのイメージのすべてのクラスとリソースが含まれており、位置情報にひも付けて管理されています。

JDK には jimage というコマンドがあったので (なお、現時点での JDK9 のドキュメントにはこのコマンドについての説明は見当たらず...) 、このコマンドで中身を閲覧してみました。

>jimage list modules
jimage: modules

Module: aoetk.sample.packager
    META-INF/MANIFEST.MF
    aoetk/sample/SampleApp.class
    module-info.class

Module: java.base
    META-INF/services/java.nio.file.spi.FileSystemProvider
    com/sun/crypto/provider/AESCipher$AES128_CBC_NoPadding.class
    com/sun/crypto/provider/AESCipher$AES128_CFB_NoPadding.class
    com/sun/crypto/provider/AESCipher$AES128_ECB_NoPadding.class
    ...

確かにクラスファイルやリソースファイルが含められていますね。

まとめ

ということで、Project Jigsaw の恩恵で、Java SE 9 からはアプリケーションのインストールイメージをより絞って配布が可能であることが分かりました。OracleOracle Java SEサポート・ロードマップ において、Java アプリケーションの配布は (予め配布先に Java をインストールさせるのではなく) JRE も一緒にバンドルした自己完結型パッケージングでの配布を推奨しています。jlink と javapackager を最大限に活用してきましょう。