JavaFXアプリケーションでJava 9のモジュールを使うときの注意点

このエントリは JavaFX Advent Calendar 2017 の 1 日目のエントリです。最初を飾るのは初めてです。まだ参加者が少ないので、みんな参加してね!

いよいよJava SE 9がリリースされました。やはり9の注目はJigsawことモジュールシステムですね。特にJavaFXにとってはjavapackagerを用いてパッケージングした際の配布サイズを小さくすることができるので、特に重要ですね。

aoe-tk.hatenablog.com

ですが、JavaFXアプリケーションでモジュールシステムを利用すると早速引っ掛かるポイントがあります。それはFXMLです。

次のような FXML を使った簡単なアプリケーションを見てみましょう。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/9" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <center>
      <StackPane prefHeight="150.0" prefWidth="200.0" BorderPane.alignment="CENTER">
         <children>
            <Label text="This is Label." fx:id="label" />
         </children>
      </StackPane>
   </center>
   <bottom>
      <HBox alignment="CENTER" BorderPane.alignment="CENTER">
         <children>
            <Button alignment="CENTER" mnemonicParsing="false" text="Button" onAction="#click" />
         </children>
         <BorderPane.margin>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </BorderPane.margin>
      </HBox>
   </bottom>
</BorderPane>
package sample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class Controller {
    @FXML
    private Label label;

    @FXML
    void click(ActionEvent event) {
        label.setText("You clicked!");
    }
}

FXMLのいいところとして、ビューXMLとペアになるControllerクラスに対して、XML側に記述したコンポーネントへのアクセスをDIを使って提供している点ですね。publicなフィールドだけでなく、package privateやprivateなフィールドとしての宣言も可能で、その場合は @FXML アノテーションを付与すると、インスタンスがインジェクトされるようになります。イベント実行メソッドも同様に @FXML アノテーションでバインドできます。

ですが、この @FXML アノテーションを使う場合は注意が必要です。次のように module-info.java を作ってみましょう。

module fxml9sample {
    requires javafx.controls;
    requires javafx.fxml;
    exports sample;
}

すると次のような例外が飛びます。 javafx.fxml モジュールに属するクラスがこのControllerクラスに対してリフレクションによるアクセスを試みるためです。

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
(中略)
Caused by: javafx.fxml.LoadException: 
fxml9sample/out/production/fxml9sample/sample/sample.fxml:15

    at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2625)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2603)
(中略)
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private javafx.scene.control.Label sample.Controller.label accessible: module fxml9sample does not "opens sample" to module javafx.fxml
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
    at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:176)
    at java.base/java.lang.reflect.Field.setAccessible(Field.java:170)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleFields(FXMLLoader.java:3495)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.access$3900(FXMLLoader.java:3344)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3460)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3456)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleMembers(FXMLLoader.java:3455)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.getControllerFields(FXMLLoader.java:3394)
    at javafx.fxml/javafx.fxml.FXMLLoader.injectFields(FXMLLoader.java:1170)
    at javafx.fxml/javafx.fxml.FXMLLoader.access$1600(FXMLLoader.java:105)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processValue(FXMLLoader.java:865)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:759)
    at javafx.fxml/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2722)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2552)
    ... 17 more
Exception running application sample.Main

これを防ぐためには、FXMLと対になるControllerクラスが属しているパッケージに対して javafx.fxml モジュールからのリフレクションによるアクセスを許可する必要があります。そのために次のように opens 文を使って javafx.fxml モジュールに対してアクセスを許可する必要があります。

module fxml9sample {
    requires javafx.controls;
    requires javafx.fxml;
    exports sample;
    opens sample to javafx.fxml; // <- これを追加
}

既存のJavaFXアプリケーションをJava 9に移行する際に意外と引っ掛かるポイントなので注意してください。

というわけで、1 日目のエントリは小ネタでした。19 日にも書く予定ですが、そちらはもう少し大きなネタにする予定です。

明日は...執筆時点でまだいない! 誰か書いてー。