How to solve fxml loading exceptions in compiled JavaFX project using GluonHQ client, Native Image and GraalVM?

Viewed 543

This is a follow up question to this question.

I am attempting to compile a JavaFX project into a Native Image so that it will run natively without the user needing Java installed. The problems with JavaFX and reflection have been solved with the GluonHQ client plugin, so that compilation is now a success.

I have managed to get a simple JavaFX project (the example generated by IntelliJ upon creating a JavaFX project) to compile using the Gluon client maven plugin. However, when running the native image at the command line, it gives a JavaFX fxml loading exception:

Exception in thread "main" java.lang.RuntimeException: Exception in Application start method
        at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:900)
        at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:195)
        at java.lang.Thread.run(Thread.java:834)
        at com.oracle.svm.core.thread.JavaThreads.threadStartRoutine(JavaThreads.java:518)
        at com.oracle.svm.core.posix.thread.PosixJavaThreads.pthreadStartRoutine(PosixJavaThreads.java:192)
Caused by: javafx.fxml.LoadException: 
sample.fxml:8

        at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2629)
        at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2607)
        at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2470)
        at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3241)
        at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3198)
        at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3167)
        at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3140)
        at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3117)
        at javafx.fxml.FXMLLoader.load(FXMLLoader.java:3110)
        at sample.Main.start(Main.java:13)
        at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:846)
        at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:455)
        at com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
        at java.security.AccessController.doPrivileged(AccessController.java:101)
        at com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
        at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
        at com.oracle.svm.jni.JNIJavaCallWrappers.jniInvoke_VA_LIST:Ljava_lang_Runnable_2_0002erun_00028_00029V(JNIJavaCallWrappers.java:0)
        at com.sun.glass.ui.gtk.GtkApplication._runLoop(GtkApplication.java)
        at com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:277)
        ... 3 more
Caused by: com.sun.javafx.fxml.PropertyNotFoundException: Property "alignment" does not exist or is read-only.
        at javafx.fxml.FXMLLoader$Element.processValue(FXMLLoader.java:355)
        at javafx.fxml.FXMLLoader$Element.processPropertyAttribute(FXMLLoader.java:332)
        at javafx.fxml.FXMLLoader$Element.processInstancePropertyAttributes(FXMLLoader.java:242)
        at javafx.fxml.FXMLLoader$ValueElement.processEndElement(FXMLLoader.java:775)
        at javafx.fxml.FXMLLoader.processEndElement(FXMLLoader.java:2842)
        at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2561)
        ... 20 more

It to was only possible to get the native image to work by changing the sample.fxml from this:

<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<GridPane fx:controller="sample.Controller"
          xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
</GridPane>

to this (removing the alignment, hgap and vgap attributes):

<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<GridPane fx:controller="sample.Controller"
          xmlns:fx="http://javafx.com/fxml">
</GridPane>

and then recompiling. The compiled binary then runs as expected.

Reflection has been configured as follows for the Gluon plugin in the POM.xml:

<plugin>
    <groupId>com.gluonhq</groupId>
    <artifactId>client-maven-plugin</artifactId>
    <version>0.1.30</version>
    <configuration>
        <mainClass>sample.NewMain</mainClass>
        <reflectionList>
            <list>sample.Main</list>
            <list>sample.NewMain</list>
            <list>sample.Controller</list>
            <list>javafx.fxml.FXMLLoader</list>
        </reflectionList>
    </configuration>
</plugin>

These FXML loading exceptions are the same once a larger project JavaFX project is compiled with reflection configured. Exceptions always say Caused by: com.sun.javafx.fxml.PropertyNotFoundException: Property [X] does not exist or is read-only. Both projects run fine on the JVM, with no exceptions being thrown. My IDE can detect no errors with the code.

4 Answers

I will expand a little bit more @mipa's answer.

As you may know, FXML is all about reflection: we have an (f)xml file, and a parser (FXMLLoader), that finds classes (GridPane), and properties names (alignment) that are resolved to method names (setAlignment(Pos) and getAlignment()) while parsing that file.

By default, the Client plugin provides for you a reflectionConfig.json file with most of the JavaFX classes and methods that you might use in your FXML files.

As you can read here, this file is generated when you run mvn client:compile (or mvn client:link), and can be found under target/client/$arch-$os/gvm/reflectionconfig-$arch-$os.json (with your target architecture and OS name).

As is now, it contains around 290 classes (Java and JavaFX), with fields and methods.

If you inspect it, you will see, for that given GridPane class:

,
  {
    "name" : "javafx.scene.layout.GridPane",
    "methods":[
      {"name":"<init>","parameterTypes":[] },
      {"name":"setRowIndex","parameterTypes":["javafx.scene.Node","java.lang.Integer"] },
      {"name":"getRowIndex","parameterTypes":["javafx.scene.Node"] },
      {"name":"setColumnIndex","parameterTypes":["javafx.scene.Node","java.lang.Integer"] },
      {"name":"getColumnIndex","parameterTypes":["javafx.scene.Node"] },
      {"name":"setColumnSpan","parameterTypes":["javafx.scene.Node","java.lang.Integer"] },
      {"name":"getColumnSpan","parameterTypes":["javafx.scene.Node"] },
      {"name":"setRowSpan","parameterTypes":["javafx.scene.Node","java.lang.Integer"] },
      {"name":"getRowSpan","parameterTypes":["javafx.scene.Node"] },
      {"name":"getRowConstraints","parameterTypes":[] },
      {"name":"getColumnConstraints","parameterTypes":[] },
      {"name":"setHgrow","parameterTypes":["javafx.scene.Node","javafx.scene.layout.Priority"] },
      {"name":"getHgrow","parameterTypes":["javafx.scene.Node"] },
      {"name":"setVgrow","parameterTypes":["javafx.scene.Node","javafx.scene.layout.Priority"] },
      {"name":"getVgrow","parameterTypes":["javafx.scene.Node"] },
      {"name":"setMargin","parameterTypes":["javafx.scene.Node","javafx.geometry.Insets"] },
      {"name":"getMargin","parameterTypes":["javafx.scene.Node"] }
    ]
  }
,

As you can notice, it contains the constructor and all the static methods in GridPane, so this works with the Client plugin:

<GridPane>
    <Label text="a label" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
</GridPane>

however, the alignment methods are not included, and that's why your fxml fails.

There are two possible solutions:

1. Config files

Following the Config files section, you can add your own file to your project and add the missing methods:

  • Create the file reflectionconfig.json under src/main/resources/META-INF/substrate/config/

  • Add the missing methods:

[
  {
    "name" : "javafx.scene.layout.GridPane",
    "methods":[
      {"name":"setAlignment","parameterTypes":["javafx.geometry.Pos"] },
      {"name":"getAlignment","parameterTypes":[] },
      {"name":"setHgap","parameterTypes":["double"] },
      {"name":"getHgap","parameterTypes":[] },
      {"name":"setVgap","parameterTypes":["double"] },
      {"name":"getVgap","parameterTypes":[] }
    ]
  }
]
  • Run again mvn client:build client:run, this time it should work.

If you inspect again the target/client/$arch-$os/gvm/reflectionconfig-$arch-$os.json file, you will see that the content of your json file has been included at the end, and now all the used GridPane methods are available for reflection.

2. Reflection List

Alternatively, you could simple add the whole class to the reflectionList:

<reflectionList>
    <list>javafx.scene.layout.GridPane</list>
</reflectionList>

After you run it, inspecting the json file you will see:

  {
    "name" : "javafx.scene.layout.GridPane",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredFields" : true,
    "allPublicFields" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true
  }

The difference with the option 1 is now that you are telling GraalVM to add all the declared and public constructors, fields and methods of that class to its reflection list, whether they are used or no, which might have a (small) impact in compilation time and memory footprint. Ideally, the option 1 above is better than option 2.

Providing only the required classes/methods will be best, but as @mipa points out, this will require some tooling that could discover which are those. In the meantime, you will have to run some iterations to find out if all the classes/methods used in your FXML files are included or not by the default json file, and add the missing ones to your reflection file (or simply the classes name to the reflection list).

You have to add more of the classes, which are loaded via FXML, to the reflexion list. E.g. I also had to add javafx.geometry.HPos and javafx.geometry.VPos. This is complicated by the fact that this is not consistent. Some classes are already included by default - others are not. You will need some experimentation here. Sometimes I even had to specify the parent of a class if a property is defined there already. For my own purposes I have written a little tool to make this easier: https://github.com/mipastgt/JFXToolsAndDemos#fxml-checker

I'd just like to add one more specific case to José's answer and maybe he can also clarify that.

Another annoying problem is that sometimes it is not sufficient to just put the class you want to load into the reflection list. E.g., if you want to load a ProgressBar and have put this class into the reflection list, you will still get the following error: ProgressBar Property "progress" does not exist or is read-only. The reason is that the property "progress" is defined in the super-class of ProgressBar and so you have to add ProgressIndicator to the list as well.

Why is this so and can it be prevented somehow?

This is so frustrating, but finally I get everything right, Here is some advice. If you have fxml file and the the file is loaded in Controller, just add the Controller in the reflectionList is fine. The resourceReflection.json does not contains all properties for javafx class, check if all properties is added in json, if not just add the fx class in the reflectionList, so add javafx.scene.layout.GridPane will add all properties of GridPane. Check your import class in fxml, some of them may not get added in json file, add to reflectionList whenever you find it, for example if you have RadioButton, ComboBox in fxml file, you won't find them in generated json file. So 1. Copy and add generated resourceReflection.json in META-INF.substrate.config directory. 2. Add all YOUR class to reflectionList, the View which used to load fxml file don't need to be added. 3. Add e.printStackTrace() to your fxml loader to check what happen in exception. 4. mvn clean client:build client:run.

Happy Coding! Good Job Gluon team.

Related