I'm running into an error running the native image produced by GraalVM (latest GraalVM built on JDK 11) via the Gluon Client Plugin.
javafx.fxml.LoadException: Error resolving onAction='#loginAction', either the event handler is not in the Namespace or there is an error in the script. fxml/LoginScreen.fxml:17
The compililation step works fine:
mvn clean client:build
I see the binary in a folder called "projectname/target/client/x86_64-linux/binaryname"
The above error produced when I run the executable via "./binaryname"
The FXML line of code which its complaining about on line 17 is:
<Button fx:id="_loginButton" layoutX="516.0" layoutY="174.0" mnemonicParsing="false" onAction="#loginAction" prefHeight="28.0" prefWidth="94.0" text="Login" />
The backing code logic are as follows and marked with @FXML:
@FXML
void loginAction(ActionEvent event) throws InterruptedException {
LoginService loginservice = new LoginService(_usernameTextField.getText(), _passwordTextField.getText());
According to a JavaFX common errors list the problem is usually because the onAction event does not have the same name as specified in the controller - Introduction to JavaFX for Beginner Programmers - Pg 27 . However this is not the case, my program's naming is accurate. Using the JavaFX maven plugin (seperate from the GluonClient) using
maven javafx:run
the program starts up correctly and works as expected. If I need to post more information, please let me know.
Here is my pom.xml (I only replaced only the name of my package below)
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>com-demo-management-ui</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<client.plugin.version>0.1.26</client.plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjfx/javafx-fxml -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.3</version>
<configuration>
<mainClass>com.demo.Launcher</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>com.gluonhq</groupId>
<artifactId>client-maven-plugin</artifactId>
<version>${client.plugin.version}</version>
<configuration>
<!-- Uncomment to run on iOS: -->
<!-- <target>ios</target> -->
<mainClass>com.demo.Launcher</mainClass>
<graalvmHome>/opt/graalvm-ce-java11-20.2.0-dev/</graalvmHome>
</configuration>
</plugin>
</plugins>
</build>
<pluginRepositories>
<pluginRepository>
<id>gluon-releases</id>
<url>http://nexus.gluonhq.com/nexus/content/repositories/releases/</url>
</pluginRepository>
</pluginRepositories>
And finally, here is the code where I set the controller (This is a method call, which I exchange my views as I need them, thus controller is passed as an argument in the creating a view):
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/"+baseController.getFxmlFile()));
fxmlLoader.setController(baseController);
Parent parent = fxmlLoader.load();
Scene scene = new Scene(parent);
Stage stage = new Stage();
stage.setFullScreen(fullScreen);
stage.setMaximized(setMaximized);
stage.setScene(scene);
stage.show();
If you have a look at HelloFXML
sample in the Client samples repository, you will see it uses the typical FXML file with a controller:
<AnchorPane fx:id="pane" ... fx:controller="hellofx.HelloController">
In your case, you don't have the controller in the FXML file, but you provide it like:
fxmlLoader.setController(new hellofx.HelloController());
As you know, the FXMLLoader
uses reflection to instantiate controller, controls and methods that are found while parsing the FXML file.
Either way, when you click the button that triggers the loginAction
method, the FXMLLoader process that with this call:
MethodHelper.invoke(method, controller, params);
that uses reflection to handle such event.
With GraalVM, reflection is an issue, and you have to "help" it a little bit, by providing the classes/methods/fields that are going to be used reflectively at some point. Find more about it here.
The Client plugin already takes care for you of adding the JavaFX core classes and methods. You can see what's added in target/client/x86_64-darwin/gvm/reflectionconfig-x86_64-darwin.json
.
However, your custom classes have to be added to that file. There are two ways you can do that:
reflectionList
, you will provide the custom class/es that will be used reflectively:<plugin>
<groupId>com.gluonhq</groupId>
<artifactId>client-maven-plugin</artifactId>
<version>${client.plugin.version}</version>
<configuration>
<reflectionList>
<list>hellofx.HelloController</list> <!-- your custom classes -->
</reflectionList>
<mainClass>${mainClassName}</mainClass>
</configuration>
</plugin>
This has the effect of opening all class methods/fields to reflection. You will see the result in the json file as:
{
"name" : "hellofx.HelloController",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredFields" : true,
"allPublicFields" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true
}
...
This should be enough to fix your issue.
reflectionconfig.json
) to META-INF/substrate/config
instead:[
{
"name":"hellofx.HelloController",
"methods":[{"name":"loginAction","parameterTypes":["javafx.event.ActionEvent"] }]
}
]
This will fix it as well. Of course, it might require adding other methods you have in the controller too (like initialize
).
This will open to reflection only this method, so it has a lower impact in memory footprint, and follows what the plugin does with the JavaFX core classes.