torutkのブログ

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

Javaアプリケーションのモジュール化でGradleを使おうとしたら色々と

Javaアプリケーションをモジュール化する際の様々な障害

以前、JavaFXでちょっとしたデスクトップガジェットプログラムを作ってみました。 これは、ちょっとしたユーティリティライブラリ(次の日記)と、

torutk.hatenablog.jp

それを使ったガジェット風アプリケーションとなります。

GitHub - torutk/EarthGadget: JavaFX 3d earth rounding 他

このプログラムを、JPMS(Java Platform Module System)のモジュール対応とJDK14で導入されたjpackageを使ったインストーラー作成に対応させることにしました。

環境は次です。

項目 内容 備考
OS Windows 10 バージョン1909 Pro 64bit 日本語版
IDE Apache NetBeans IDE 12.0 on Oracle JDK 11.0.7 Oracle JDKは、OTNライセンス(開発用途)の下無償利用
ビルド・実行Java Liberica JDK 14.0.1 JavaFX同梱のFull版
使用リポジトリ GitHub ソースファイルは文字コードUTF-8、改行コードLFで管理

メインクラスを持つモジュール対応JARファイルを生成する

メインクラスを持つモジュール対応JARファイルは、モジュール定義(module-info.class)にMainClass属性を埋め込む必要があります。この埋め込みはjarコマンドで--main-classオプションで指定した場合に行われます。

ガジェット風アプリケーションのビルド環境は、NetBeans IDEでAntをビルドツールとして使うプロジェクトです。 ところが、現時点でAntにはメインクラスを持つモジュール対応JARファイルを作る機能がありません。

自分でメインクラスを持つモジュール対応JARファイルを生成するタスクを書くか、Ant以外の別なビルドツールに移行するかを検討しました。

自分でモジュール対応JARファイルを生成するタスクをAntで書くというのは、今後NetBeansでプロジェクトを作成するときに毎回Antのビルドファイルに手を入れることになるので望ましい姿ではありません。

Ant以外のビルドツールとしては、mavenかgradleかが候補に上がります。現時点ではどちらもメインクラスを持つモジュール対応JARファイルの生成に対応しています。

今回開発に使う環境は個人のプログラム作成用途で、自宅以外でも出張・旅行などの移動先や移動途中などでも使用します。移動先ではWi-Fiが使えるとは限らず、また自宅でも特にコロナ緊急事態宣言中には時折インターネット接続ができない状況があり、インターネット接続ができなくても十分使えるgradleに移行することにしました。

NetBeansのプロジェクトをGradleにする

NetBeansには、Gradleプラグインが用意されており、これをインストールします。 次に、新規プロジェクトを作成([File]メニュー > [New Project]で、[Java with Gradle] > [Java Application])します。Java wtih Gradle で選択可能なプロジェクトの種類は次の3つです。

  • Java Application
  • Java Class Library
  • Multi-Project Build

複数のモジュール対応JARを1つのNetBeansプロジェクトで管理するならMulti-Project Buildです。 今回は1プロジェクト1JARを生成するので、ガジェット風アプリケーションはJava Applicationを選択しました。

問題1 UTF-8Javaソースファイルを開くと文字化け

NetBeansのGradleプロジェクトに、GitHubから取り出したガジェット風アプリケーションのJavaソースファイルを登録し、開こうとすると次の警告がでます。

The file D:/work/EarthGadget/src/main/java/com/torutk/gadget/earth/EarthGadgetApp.java cannot be safely opened with encoding windows-31j. Do you want to continue opening it?

[Yes]で開くと、日本語が壊れて(文字化けして)表示されます。困りました。 ファイル先頭のコピーライト記述コメントで、「©」が「ツゥ」に化けているので、UTF-8文字コードC2 A9を、Shift JISとして解釈しています。

プロジェクトのプロパティを開いて設定を探したのですが、Antプロジェクトの設定にはあったEncoding設定が見つかりません。

この事象は、NetBeansのバグデータベースに登録されていました。

issues.apache.org

  • build.gradleに以下を追加 → 文字化けは解消せず(上記チケットにもうまくいかないと記載あり)
tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}
  • NetBeansの起動オプションに-J-Dfile.encoding=UTF-8を追記 → 上記チケットのコメントで回避方法として紹介されていたが、JDK 11およびJDK 14ではNetBeans起動時にJavaVMがクラッシュ
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffb6413fd3e, pid=14828, tid=4892
#
# JRE version: Java(TM) SE Runtime Environment 18.9 (11.0.7+8) (build 11.0.7+8-LTS)
# Java VM: Java HotSpot(TM) 64-Bit Server VM 18.9 (11.0.7+8-LTS, mixed mode, tiered, compressed oops, g1 gc, windows-amd64)
# Problematic frame:
# C  [awt.dll+0x8fd3e]
#
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#

---------------  S U M M A R Y ------------

Command Line: -Dnetbeans.importclass=org.netbeans.upgrade.AutoUpgrade -Dfile.encoding=UTF-8 -XX:+UseStringDeduplication -Xss2m -Djdk.gtk.version=2.2 -Dapple.laf.useScreenMenuBar=true 
  :(中略)
---------------  T H R E A D  ---------------

Current thread (0x0000000029be5000):  JavaThread "AWT-EventQueue-0" [_thread_in_native, id=4892, stack(0x000000002c910000,0x000000002cb10000)]

Stack: [0x000000002c910000,0x000000002cb10000],  sp=0x000000002cb0cd90,  free space=2035k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C  [awt.dll+0x8fd3e]

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j  sun.awt.windows.WComponentPeer._setFont(Ljava/awt/Font;)V+0 java.desktop@11.0.7
j  sun.awt.windows.WComponentPeer.setFont(Ljava/awt/Font;)V+7 java.desktop@11.0.7
j  sun.awt.windows.WWindowPeer.initialize()V+42 java.desktop@11.0.7
j  sun.awt.windows.WFramePeer.initialize()V+1 java.desktop@11.0.7
j  sun.awt.windows.WComponentPeer.<init>(Ljava/awt/Component;)V+83 java.desktop@11.0.7
j  sun.awt.windows.WCanvasPeer.<init>(Ljava/awt/Component;)V+2 java.desktop@11.0.7
j  sun.awt.windows.WPanelPeer.<init>(Ljava/awt/Component;)V+2 java.desktop@11.0.7
j  sun.awt.windows.WWindowPeer.<init>(Ljava/awt/Window;)V+2 java.desktop@11.0.7
j  sun.awt.windows.WFramePeer.<init>(Ljava/awt/Frame;)V+2 java.desktop@11.0.7
j  sun.awt.windows.WToolkit.createFrame(Ljava/awt/Frame;)Ljava/awt/peer/FramePeer;+5 java.desktop@11.0.7
j  java.awt.Frame.addNotify()V+20 java.desktop@11.0.7
j  java.awt.Window.pack()V+28 java.desktop@11.0.7
j  org.netbeans.core.startup.Splash.center(Ljava/awt/Window;)V+1
j  org.netbeans.core.startup.Splash$SplashRunner.run()V+11
 :(後略)

スプラッシュ画面を表示しようとして、awt.dll でメモリアクセス違反が発生しています。 OpenJDKのバグデータベースにはこの事象が登録されています。解決はJDK 15(2020年9月リリース予定)となっています。

bugs.openjdk.java.net

これは、Windows上のJDKGUI(awtライブラリ、Swingも内部で使用)を使ったプログラムをJVMオプション-Dfile.encoding=UTF-8を指定して実行すると発生します。UTF-8以外の指定を試すと(例えばeucjp、windows-31j)クラッシュは発生しませんでした。

ということで、現時点ではNetBeans IDEJDK 8で動かすしかなさそうです。

問題2 NetBeans IDEJDK 8で動かすと
  • nb-javac ライブラリ(プラグイン)をインストールするよう案内が出る

NetBeans IDEからjavacを扱う際に、JDK 8までは標準のjavacでは機能・制御が不十分のため、NetBeans用にパッチを当てたnb-javacを使っています。JDK 9以降でNetBeans IDEを動かす場合は不要(ない方がよい?)のため、不整合が生じるかもしれません。

  • HIDPI対応等が劣化する

JDK 9ではWindows上でHiDPIディスプレイに対応していますが、JDK 8では非対応のためツールバーのアイコンが小さすぎるといった弊害があります。ダイアログにおいて文字が欠けてしまう等の問題があります。

問題3 AntベースプロジェクトからGradleベースプロジェクトへの移行

GitHubリポジトリに登録しているNetBeansのAntプロジェクトをNetBeansのGradleプロジェクトへ移行しようとすると、

  • 新規プロジェクト作成では空のディレクトリを指定する必要がある
  • ディレクトリ構造が異なる
    • Antベースのプロジェクトでは、srcディレクトリ下にパッケージに相当するディレクトリツリー
    • Gradleベースの単一モジュールプロジェクトでは、src/main/java ディレクトリ下にパッケージに相当するディレクトリツリー
    • Gradleベースの複数モジュールプロジェクトでは、src/<モジュール名>/classesディレクトリ下にパッケージに相当するディレクトリツリー
  • Antベースのプロジェクトでは画像ファイルをソースファイルと同じディレクトリに置いてるが、Gradleベースのプロジェクトでは画像ファイルをソースファイルと同じディレクトリに置いても成果物(JARファイル)に取り込まれない

といった事態に直面します。

後の問題対応を含めると、NetBeans上からプロジェクト作成をするのではなく、コマンドラインからGradleのコマンドを叩いてプロジェクトを生成させるのがよいのではと思いました(要試行)。

問題4 NetBeansのプロジェクトプロパティ設定から設定できる項目がほとんどない

また、設定を見回って気付いたこととして、GradleプロジェクトではNetBeansのプロジェクトプロパティ設定画面から設定できる項目がほとんどありません。ライブラリの参照も表示されるだけで追加・変更はできません。

ビルドに関する諸設定は、build.gradleファイルをエディタで直接編集するようになっています。

お試しにNetBeansの新規プロジェクト作成で、GradleのJava Application種類を選択すると、次のbuild.gradleファイルが生成されます。

apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'application'


description = 'Gradle Java Application Sample'
    group = 'com.torutk.hello'

mainClassName = 'com.torutk.hello.HelloApp'

repositories {
    jcenter()
}

dependencies {
    testImplementation     'junit:junit:4.13'
}

description、group、mainClassNameは、新規プロジェクト作成時にウィザードから設定した内容です。 javaのソースレベルは、デフォルトではNetBeans IDEを動かしているJDKバージョンとなるので、プロジェクトで使用するレベルを明示的にビルド定義に記述します。groupは、成果物をmavenリポジトリに上げる等がないなら未定義でもよいかも。

問題5 使用するGradleバージョン

[Tools] > [Options] から、[Java] > [Gradle] で Categories欄を[Execution]にすると、GradleのバージョンやOffline設定等ができます。 NetBeans IDEにGradleプラグインを入れた後、Gradleのバージョンは6.3となっています。 そのマシンで初めてGradleプロジェクトを作成するときは、まだGradle(バージョン6.3)が存在しないのでインターネットからダウンロードします。 そのため、オフラインで作業する場合は、一度インターネットに接続した環境でダミーの新規プロジェクトを作成する等の対処が必要です。

また、JavaのJPMSモジュール対応しているのはGradleの6.4以降なので、新規プロジェクト作成後にGradleのバージョンを更新する作業が発生します。

  • Use Standard Gradle Version は6.3のみ選択可

    • いったん6.3でプロジェクト作成後、Gradleバージョンを更新するとインターネットからダウンロードが発生し、それ以降は6.5等の新しいバージョンを選択可能
  • Customで、別途マシンにインストールしたGradleディレクトリを指定することが可能

[Tools] > [Options] から、[Java] > [Gradle] で Categories欄を[Execution]とし、Gradle Distribution領域で[Custom]ラジオボタンを選択、[Browse]ボタンでマシンにインストールしたGradleのディレクトリを指定します。

問題6 ライブラリの参照

ユーティリティライブラリのNetBeansプロジェクトと、それを利用するガジェット風アプリケーションのNetBeansプロジェクトで構成しています。 ガジェット風アプリケーションのbuild.gradleファイルに、どのようにユーティリティライブラリのNetBeansプロジェクト参照を記述するのかが問題でした。

Gradleでは、マルチプロジェクト構成を作ることで複数のプロジェクトをまとめてビルドすることが可能です。

しかし、シングルプロジェクト構成ではプロジェクトを参照することが難しいので、個別にビルドすることとし、ユーティリティライブラリのNetBeansプロジェクトが生成するJARファイルをガジェット風アプリケーションのNetBeansプロジェクトから参照するようにします。

ローカルのJARファイルを参照させるには、repositories {...} で flatDirによるローカルのディレクトリ指定をするか、dependencies {...} で、fileTreeで指定するといった方法があります。

ユーティリティライブラリのNetBeansプロジェクトは、gitのsubmodule機能を用いてガジェット風アプリケーションのディレクトリ下に置いているので、相対パスでライブラリを参照します。

dependencies {
    implementation fileTree(dir: "javafx-gadgetsupport/dist", includes: ['*.jar'])
}
  • javafx-gadgetsupport ライブラリプロジェクトは、Antベースのままmodule-info.javaを追加してモジュール対応しています。

次に、ライブラリ参照を従来のクラスパスではなくモジュールパスとして扱うよう定義します。

java {
    modularity.inferModulePath = true
}

依存関係で定義するライブラリがモジュール対応JARのときは、クラスパスではなくモジュールパスとして扱います。

問題7 画像ファイルがJARに取り込まれない

Antベースのプロジェクトで、ソースファイルと画像ファイルとを同一ディレクトリに置いていましたが、Gradleでビルドすると画像ファイルが生成されるJARファイルに含まれなくなりました。

これは、Gradleがデフォルトではソースファイルのディレクトリ(src/main/java以下)にあるファイルはコンパイル対象として扱い、それ以外のファイルは無視しています。リソースファイル(プロパティファイルや画像ファイルなど)は、リソースファイルのディレクトリ(src/main/resources以下)に置いたものがJARファイルに取り込まれます。

Gradleのデフォルトのソースディレクトリ構造ではなく、Antベースのソースディレクトリ構造として扱うには次の記述をします。

sourceSets {
    main {
        java {
            srcDir 'src'
        }
        resources {
            srcDir 'src'
        }
    }
}
問題8 Gradleのユーザーマニュアルは1200ページ以上もある

ドキュメントが充実していることはとても素晴らしいですが、分量が多いです。

問題9 マルチプロジェクトのビルド定義記述が

マルチプロジェクトのビルドを書こうとしますが、いくつか嵌りポイントがあって頓挫しました。

  • ルートプロジェクトのbuild.gradleに 共通の定義を記述すべく、subprojects {...} の中にplugins { ... } 形式を記述すると、Could not find method plugins() for arguments とエラーに

プラグインの指定の記述方法が2種類(apply plugin: xxx と plugins { id xxx })あり、plugins {...} はビルド定義のトップに記述しなくてはならずsubprojects{..}の中には記述できないようです。apply pluginsの書き方をする必要があります。