Java 8 で作成した JavaFX アプリケーションを Java 9 で動かす Again

この記事は JavaFX Advent Calendar 2017 の 9 日目です。

とりあえず JavaFX アプリケーションのスケルトンを自動生成できるようにはなったがそこから先がなかなか思い浮かばないので、4 日目@yumix_h が見事に失敗した JavaFX 9 へのマイグレーションをやってみます。

ファイナル・アンサー : pom.xml の source と target を 9 に設定して終わり

ふざけているのか!? と言われそうですが、いたって真面目です。

誰が Java 9 では module-info.java が必須だなんて言いましたか? module-info.java があればアクセス・レベルを少し細かく制御できますが、なければないなりに動きます。これが Java の後方互換性の威力です。

正確には、module-info.java がないクラス群/JAR は automatic module という扱いになり、すべての public クラスがエクスポートされます。本来は後方互換性のために用意されているものですが、単一モジュールで構成されたアプリケーションに対して module-info.java でアクセス制御してもあまり意味はありませんから、特にお題の asteroid-viewer に関しては automatic module とすれば良いでしょう。

automatic module がどのような挙動を示すかについては @opengl_8080 さんの「Jigsaw 勉強メモ」に細かく記載されていますので、そちらを参照してください。

本来はこれで終わりなのですが、さすがに手抜き 3 連続 (一応、これでも数日間悩んでますが...) は怒られそうなので、module-info.java を入れるところまではやってみましょう。

まず、module-info.java の場所ですが、Maven 標準レイアウトの場合 src/main/java の直下です。Jigsaw の説明はだいたい 2 つ以上のモジュールの同時開発で説明するからプロジェクトのレイアウトは複雑になるしどこに module-info.java を置けばいいのかわからなくなるのです。重要なので繰り返しますが、ある単一モジュールの Maven プロジェクトに module-info.java を配置する場合、その場所は src/main/java (Maven 標準レイアウトの場合) となります。

Eclipse 4.7.1a の場合、プロジェクトを右クリックして構成メニューから 'Create module-info.java' を選択すれば exports と requires を自動的に抽出してファイルを作ってくれます。IntelliJ はまだ試していませんが、きっともっと便利でしょう。もちろんスクラッチで作成しても大した手間にはなりません。FXML を使用しているのならば、1 日目@aoetk さんが教えてくれた opens も追加します。出来上がりの module-info.java は以下のようになります。

module asteroid {
    exports asteroid.net.yumix.asteroid.color;
    exports asteroid.net.yumix.asteroid;

    requires javafx.base;
    requires javafx.controls;
    requires javafx.fxml;
    requires javafx.graphics;
    
    opens asteroid.net.yumix.asteroid to javafx.fxml;
}

これを src/main/java 直下に置くのですよ。少なくとも @yumix_h は堂々と間違えたので、しつこいですが三度繰り返しました。

実はこれで終わるほど世の中甘くはないです。Maven の maven-compiler-plugin が Java 9 に正式対応したのは本稿執筆時点での最新版である 3.7.0 からなので、原則として pom.xml に maven-compiler-plugin:3.7.0 を明示しなければなりません。少なくとも Eclipse 4.7.1a は暗黙的に使用されるのが maven-compiler-plugin:3.1 なので (Effective POM タブをクリックしよう)、Java 9 ではコンパイルができません。また、JAR を作成する場合は maven-assembly-plugin:3.0.0 および maven-jar-plugin:3.0.0 も必要なのでこれらも明示します。そして出来上がった pom.xml が以下のものです。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>net.yumix</groupId>
  <artifactId>asteroid-viewer</artifactId>
  <version>0.0.2-SNAPSHOT</version>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>9</maven.compiler.source>
    <maven.compiler.target>9</maven.compiler.target>
  </properties>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
      </plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.0</version>
</plugin> </plugins> </build> </project>

これで出来上がった JAR は、以下のように実行します。

java -p . -m asteroid/net.yumix.asteroid.AsteroidViewer

-p (--module-path) オプションで JAR ファイル/クラス群のあるディレクトリを指定します。-m (--module) オプションでモジュール名 (ここでは asteroid)、スラッシュで区切ってメイン・クラスを指定します。

JavaFX よりも Jigsaw の比率がはるかに大きい記事でしたが、JDK 9 Early Access の automatic module がない頃に既存の JavaFX アプリケーションを上手く動かせずに悩んだ身としては、JDK 9 GA 以降では何もしなくてもいいことを含めて勉強になりました。

p.s. 蛇足ですが、今回改修したものを https://github.com/khasunuma/asteroid-viewer にアップロードしました。

追補

実際のところ、JDK 9 側はモジュールで整理されており、特に一部のモジュール (JAXB や JAX-WS など Java EE 由来の API) はそのままでは使用できません (java.base モジュールの依存関係から除外されている)。たとえ呼び出し側が automatic module であってもお構いなしです。言語仕様上は後方互換性が保たれていても、標準 API の後方互換性が損なわれているわけです。

解決方法は、実行時に VM オプション --add-modules を使用して、そのままでは参照できないモジュールを指定してあげることです。先日作成したスケルトンで使用している afterburner.fx は本稿執筆時点では Java 9 に対応できていませんが、実行時に --add-modules java.xml.ws.annotation を付加すれば動作するようになります。ピンポイントでモジュールを指定するのが面倒であれば、Eclipse 4.7.1a も使用している --add-modules=ALL-SYSTEM を付ければ大丈夫です。むしろこちらのふるまいをデフォルトにしてくれた方が良かったのと思うのは私だけでしょうか?