torutkのブログ

ソフトウェア・エンジニアのブログ

JavaアプリケーションのJPMSモジュール化をGradleで

Javaアプリケーションをモジュール化し、Gradleでビルドするようにした

先日のブログの続きです。

torutk.hatenablog.jp

いろいろな問題点に直面してしまいました。その後の紆余曲折を書いていきます。

使用するIDE

Gradleプロジェクトとして作ったディレクトリは、直接NetBeans IDEIntelliJ IDEAで開くことができます。そこで、Gradle化にあたっては、IDE固有のプロジェクト設定ファイルは設けずにGradleのビルド定義だけを置くことにします。リポジトリに置くソースファイルは文字コードUTF-8とします。

NetBeans IDEの注意点

Windows日本語版上のNetBeans IDE 12.0で文字コードUTF-8のソースファイルを持つGradleプロジェクトを開くと文字化けします。回避策はNetBeans IDEの起動オプションで-J-Dfile.encoding=UTF-8を指定します。ただし、JDK 9~14ではこのオプションを指定するとJavaVMのバグによりクラッシュするので、NetBeans IDEJDK 8で動かします。NetBeans IDEJDK 8で動かしても、プロジェクトのビルドは任意のJDKバージョンを設定できます。

IntelliJ IDEAは問題なく

Windows日本語版上のIntelliJ IDEA 2020で文字コードUTF-8のソースファイルを持つGradleプロジェクトを開くと、UTF-8文字コードを認識し文字化けは発生しません。IntelliJ IDEAの内蔵JDKが独自のパッチを当ててクラッシュを避けているようです。

プロジェクトの構造

JPMS/Gradle 対応前の構造

JPMS/Gradle化する前の EarthGadgetアプリケーションのソースコード構造の概要は次です。

EarthGadget
  +- nbproject/                   <-- NetBeans IDEのプロジェクト定義等が格納
  +- src/                         <-- EarthGadgetアプリケーションのソースコード(画像含む)
  +- build.xml                    <-- NetBeans IDEのAntビルド定義
  +- javafx-gadgetsupport/        <-- Gitサブモジュールで取り込んだライブラリリポジトリ

EarthGadget はライブラリとして別リポジトリにあるjavafx-gadgetsupportを使用しています。javafx-gadgetsupport を GitのサブモジュールとしてEarthGadgetのプロジェクトツリー配下に配置し、一緒にビルドする構造としています。EarthGadgetをビルドすると依存関係からjavafx-gadgetsupportをビルドしてバイナリを生成します。

JPMS/Gradle 対応の構造

Gradle化する際には、EarthGadgetをビルドすると依存関係からjavafx-gadgetsupportをビルドするようにする仕組みを維持したいのでその方法を探りました。

独立したGradleプロジェクトとして構成すると、順番にビルドする必要が生じますが、マルチプロジェクトとして構成すると、依存関係をプロジェクトに対して設定できます。

次はマルチプロジェクトのディレクトリ構成です。チュートリアル、ユーザーマニュアルでは、ルートプロジェクトの下に実際のプロジェクトを並べる構成が記載されています。それにならうと、

EarthGadget
  +-- settings.gradle               <-- サブプロジェクト(earthgadget, javafx-gadgetsupport)を定義
  +-- earthgadget/                  <-- EarthGadgetアプリケーションのGradleサブプロジェクト
  +-- javafx-gadgetsupport/         <-- Gitサブモジュールで取り込んだJavaFX GadgetSupportライブラリのGradleサブプロジェクト

という構成です。これでGradle化してビルドができました。 次は、元の構造に近い形でマルチプロジェクトのディレクトリが構成できないかと試してみました。

EarthGadget
  +-- settings.gradle               <-- サブプロジェクト(javafx-gadgetsupport)を定義
  +-- build.gradle                  <-- EarthGadgetアプリケーションのビルド定義
  +-- src/                          <-- EarthGadgetアプリケーションのソースコード(画像含む)
  +-- javafx-gadgetsupport/         <-- Gitサブモジュールで取り込んだJavaFX GadgetSupportライブラリのGradleサブプロジェクト    

マルチプロジェクト構成のルートプロジェクトにEarthGadgetアプリケーションのビルド定義とソース一式を配置、サブプロジェクトにjavafx-gadgetsupportライブラリを配置します。

移行作業

gradle化作業をgitの新たなブランチで行う

JPMS対応およびGradle対応を、gitの新しいブランチ(migrate_to_gradle)を設けてその上で作業します。

  • Gradle化前のEarthGadgetプロジェクトをgithubからクローン
D:\work> git clone --recursive https://github.com/torutk/EarthGadget.git
Cloning into 'EarthGadget'...
remote: Enumerating objects: 79, done.
remote: Total 79 (delta 0), reused 0 (delta 0), pack-reused 79
Unpacking objects: 100% (79/79), 412.56 KiB | 625.00 KiB/s, done.
Submodule 'javafx-gadgetsupport' (https://github.com/torutk/javafx-gadgetsupport.git) registered for path 'javafx-gadgetsupport'
Cloning into 'D:/work/EarthGadget/javafx-gadgetsupport'...
remote: Enumerating objects: 18, done.
remote: Counting objects: 100% (18/18), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 74 (delta 0), reused 13 (delta 0), pack-reused 56
Submodule path 'javafx-gadgetsupport': checked out 'd7cbb2d2d9605769d40769133a55ef5a60049261'
D:\work>
  • Gradle化の作業を行うブランチをmasterから新規作成
D:\work> cd EarthGadget
D:\work\EarthGadget> git checkout -b migrate_to_gradle
Switched to a new branch 'migrate_to_gradle'

D:\work\EarthGadget> git branch
  master
* migrate_to_gradle

D:\work\EarthGadget> cd javafx-gadgetsupport

D:\work\EarthGadget\javafx-gadgetsupport> git checkout -b migrate_to_gradle
Switched to a new branch 'migrate_to_gradle'
 
D:\work\EarthGadget\javafx-gadgetsupport> git branch
  master
* migrate_to_gradle

D:\work\EarthGadget\javafx-gadgetsupport> 
javafx-gadgetsupportライブラリのGradle対応

Gradleプロジェクトとするには、gradle initコマンドで必要なファイル群を作成します。その際、種類をbasicにしておくと、雛形のソースコードやそのコードを使いサンプルのライブラリへの依存がないビルド定義ファイルが生成されます。

  • gradle init でGradleプロジェクトの作成
D:\work\EarthGadget\javafx-gadgetsupport> gradle init --type basic --dsl groovy --project-name javafx-gadgetsupport
  • NetBeans固有のプロジェクト定義を削除
D:\work\EarthGadget\javafx-gadgetsupport> git rm -r build.xml nbproject
rm 'build.xml'
rm 'nbproject/build-impl.xml'
rm 'nbproject/genfiles.properties'
rm 'nbproject/project.properties'
rm 'nbproject/project.xml'
  • Javaのライブラリを生成するGradleビルド定義を記述
plugins {
    id 'java-library'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
    modularity.inferModulePath = true
}

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
    }
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

ライブラリをビルドするので、プラグインjava-libraryを指定します。 Javaソースコードのバージョンとコンパイル後のクラスファイルのバージョンをJava SE 11に指定します。 JPMSモジュール対応の設定をします。 ソースファイルのディレクトリは、gradleのデフォルトであるsrc/main/java以下ではなく、src以下にあることを設定します。 ソースファイルの文字コードUTF-8であることを指定します。

  • module-info.java をsrc直下に新規作成
module com.torutk.gadget.support {
    requires javafx.controls;
    requires transitive java.prefs;
    exports com.torutk.gadget.support;
}

モジュール名は、代表公開パッケージと同じ名前としています。 requiresにはこのライブラリが使用するライブラリを提供するモジュールを指定しています。 このライブラリのAPIでは、Java Preferences APIの型を使っていますが、ライブラリ利用者はAPIの利用箇所にほぼ限定してJava Preferences APIを使うので、transitiveを指定し、ライブラリ利用者のモジュール定義にjava.prefsモジュールを指定しなくてもよいようにしています。

EarthGadgetアプリケーションのGradle対応
  • gradle init でGradleプロジェクトの作成
D:\work\EarthGadget> gradle init --type basic --dsl groovy --project-name EarthGadget
  • NetBeans固有のプロジェクト定義を削除
D:\work\EarthGadget> git rm -r build.xml nbproject
rm 'build.xml'
rm 'nbproject/build-impl.xml'
rm 'nbproject/genfiles.properties'
rm 'nbproject/project.properties'
rm 'nbproject/project.xml'
  • settings.gradle にjavafx-gadgetsupportライブラリのサブプロジェクトを指定
rootProject.name = 'EarthGadget'
include 'javafx-gadgetsupport'
  • Javaのアプリケーションを生成するGradleビルド定義を記述
plugins {
    id 'application'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
    modularity.inferModulePath = true
}

dependencies {
    implementation project(':javafx-gadgetsupport')
}

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
        resources {
            srcDirs = ['src']
        }
    }
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

application {
    mainModule = 'com.torutk.gadget.earth'
    mainClass = 'com.torutk.gadget.earth.EarthGadgetApp'
}

アプリケーションをビルドするので、プラグインにapplicationを指定します。 Javaソースコードのバージョンとコンパイル後のクラスファイルのバージョンをJava SE 11に指定します。 JPMSモジュール対応の設定をします。 依存関係でjavafx-gadgetsupportを指定します。マルチプロジェクトでは依存関係に別のサブプロジェクトを指定することができます。 ソースファイルのディレクトリは、gradleのデフォルトであるsrc/main/java以下ではなく、src以下にあることを設定します。また、リソースファイルのディレクトリも、gradleのデフォルトであるsrc/main/resources以下ではなく、src以下にあることを設定します。 ソースファイルの文字コードUTF-8であることを指定します。 アプリケーションとしてJPMSのメインモジュールと、メインクラスを指定します。

  • module-info.java をsrc直下に新規作成
module com.torutk.gadget.earth {
    requires com.torutk.gadget.support;
    requires javafx.controls;
    opens com.torutk.gadget.earth to javafx.graphics;
}

モジュール名は、代表パッケージと同じ名前としています。 requiresにはこのアプリケーションが使用するライブラリを提供するモジュールを指定しています。 JavaFXのアプリケーションでは、アプリケーション側で作成したクラスに対してJavaFXライブラリ側からリフレクションをかけるので、opensでJavaFXライブラリから実行時にアクセスすることを許可しています。

ビルドと実行
  • ビルド
D:\work\EarthGadget> gradle build
  :
  • 実行
D:\work\EarthGadget>gradle run

WindowsインストーラMSI)の作成

EarthGadgetアプリケーションには、javapackagerコマンドを使ってWindowsインストーラーを作成するバッチファイルを含めていました。しかしjavapackagerコマンドはJDK 11で削除されてしまいました。

javapackagerコマンドの後継として、JDK 14からjpackageコマンドが含まれるようになりました(incubator段階)。今回は、JDK 14のjpackageコマンドを使ってWindowsインストーラー(MSI)を生成するようバッチファイルを修正します。

ツール種類 ツール
JDK Liberica JDK 14
MSIインストーラー作成ツール WiX Toolset 3.10
@echo off

%JAVA_HOME%\bin\jpackage ^
--type msi ^
--win-upgrade-uuid 38d49c58-102e-486b-bfac-8a0f6e796a93 ^
--win-menu ^
--win-menu-group "Tiny Gadgets" ^
--win-shortcut ^
--app-version 0.2.2 ^
--description "Earth 3D rounding on desktop" ^
--name "EarthGadget" ^
--dest build\installer ^
--vendor Takahashi ^
--module-path build\libs;javafx-gadgetsupport\build\libs ^
--module com.torutk.gadget.earth/com.torutk.gadget.earth.EarthGadgetApp ^
--verbose
オプション 意味 指定例
--type インストーラー種類を指定 msi
--win-upgrade-uuid バージョンアップ時に、既にインストール済みの前のバージョンがあればそれを置き換えるための識別子
--win-menu スタートのメニューに追加する
--win-menu-group スタートメニューのフォルダ名
--win-shortcut デスクトップにショートカットを作成
--app-version インストーラに設定するバージョン番号
--description 説明文字列
--name アプリケーション名、実行ファイルの基底名
--dest 作成したインストーラファイルの格納先
--vendor 作成者・組織名
--module-path インストーラに含めるモジュールの置かれているディレクトリのリスト
--module 実行するメインモジュール名
  • --module では、MainClass属性を持つモジュールであればモジュール名のみ指定、MainClass属性を持たないモジュールであればモジュール名/メインクラス名を指定
  • インストーラー実行時にインストールディレクトリを選択可能とするには、--win-dir-chooser オプションを指定
  • インストーラーのインストール先をシステム共通の場所(C:\Program Files 以下)ではなく個人のディレクトリ下とするには、--win-per-user-install オプションを指定
  • JavaVMオプションを指定したい場合は、--java-optionsに指定、インストール後の設定ファイル(<インストールディレクトリ>\app\<アプリ名>.cfg)のJavaOptions項にオプションが記載
  • jpackageコマンドが裏で実行するjlinkの生成ファイルやWiX Toolsetの設定・オブジェクトファイルを確認したい場合は、--tempで空のディレクトリを指定

バッチファイル実行時に、PATHにJDK 14、WiX Toolsetを入れておきます。

インストーラーのファイルサイズとインストール後のサイズ

インストーラーファイルの大きさは、28MBでした。 インストール後のインストールディレクトリ以下の大きさは、85MBでした。

JavaFXを同梱するLiberica JDK 14のインストールディレクトリ以下の大きさは400MB強なので、JPMSモジュール機構による必要なモジュールを抜粋したJava実行イメージの削減効果がよく出ています。

jpackage 感想

jlinkを内部で実行するので、jpackageコマンドだけでインストーラが生成されるのはよいです。

バージョンアップ時に、既にインストール済みの古いバージョンがある場合、アップグレードインストール(古いバージョンが削除され新しいバージョンがインストールされる)を可能にするためUUIDを渡せるのはjavapackagerの時に比べて改善されています。