英文:
How to solve fxml loading exceptions in compiled JavaFX project using GluonHQ client, Native Image and GraalVM?
问题
这是对此问题的跟进提问。
我正在尝试将一个 JavaFX 项目编译成本地映像,以便在用户无需安装 Java 的情况下本地运行该项目。JavaFX 和反射方面的问题已经通过 GluonHQ 客户端插件得到解决,因此编译现在成功进行。
我已经成功使用 Gluon 客户端 Maven 插件编译了一个简单的 JavaFX 项目(这是在创建 JavaFX 项目时由 IntelliJ 自动生成的示例)。然而,在命令行上运行本地映像时,会出现 JavaFX fxml 加载异常:
主线程中的异常 "main" java.lang.RuntimeException: Application start 方法中的异常
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: 属性 "alignment" 不存在或只读。
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
仅通过更改 sample.fxml,从这个:
<?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>
到这个(移除 alignment、hgap 和 vgap 属性):
<?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>
然后重新编译。编译后的二进制文件可以正常运行。
Gluon 插件在 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>
当使用反射配置编译一个较大的 JavaFX 项目时,这些 FXML 加载异常也会出现。异常信息始终是 Caused by: com.sun.javafx.fxml.PropertyNotFoundException: Property [X] 不存在或只读。
这两个项目在 JVM 上运行良好,没有抛出任何异常。我的集成开发环境在代码中未检测到任何错误。
英文:
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.
答案1
得分: 5
我将稍微扩展一下 @mipa 的回答。
正如你所了解的,FXML 主要涉及反射:我们有一个 (f)xml 文件和一个解析器 (FXMLLoader
),它在解析该文件时会找到类 (GridPane
) 和属性名 (alignment
),这些属性名会被解析为方法名 (setAlignment(Pos)
和 getAlignment()
)。
默认情况下,客户端 插件 会为您提供一个带有大多数可能在 FXML 文件中使用的 JavaFX 类和方法的 reflectionConfig.json
文件。
正如您可以在这里阅读的那样(https://docs.gluonhq.com/client/#_reflectionlist),当您运行 mvn client:compile
(或 mvn client:link
) 时,将会生成这个文件,可以在 target/client/$arch-$os/gvm/reflectionconfig-$arch-$os.json
找到它 (其中包括您的目标架构和操作系统名称)。
目前为止,它大约包含了 290 个类(Java 和 JavaFX),其中包括字段和方法。
如果您检查它,您会看到对于给定的 GridPane
类:
,
{
"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"] }
]
}
,
正如您可以注意到的那样,它包含了构造函数和 GridPane
类中的所有静态方法,因此与客户端插件一起使用可以正常工作:
<GridPane>
<Label text="a label" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
</GridPane>
但是,alignment
方法没有被包括在内,这就是您的 FXML 失败的原因。
有两种可能的解决方案:
1. 配置文件
根据配置文件 部分的说明,您可以向您的项目中添加自己的文件,并添加缺失的方法:
-
在
src/main/resources/META-INF/substrate/config/
下创建文件reflectionconfig.json
。 -
添加缺失的方法:
[
{
"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":[] }
]
}
]
- 再次运行
mvn client:build client:run
,这次应该会正常工作。
如果您再次检查 target/client/$arch-$os/gvm/reflectionconfig-$arch-$os.json
文件,您会看到您的 JSON 文件的内容已经包含在末尾,现在所有在您的 FXML 文件中使用的 GridPane
方法都可以用于反射。
2. 反射列表
或者,您可以直接将整个类添加到反射列表中:
<reflectionList>
<list>javafx.scene.layout.GridPane</list>
</reflectionList>
运行后,检查 JSON 文件,您将看到:
{
"name" : "javafx.scene.layout.GridPane",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredFields" : true,
"allPublicFields" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true
}
选项 1 与选项 2 的区别在于,现在您正在告诉 GraalVM 将该类的所有声明的和公共的构造函数、字段和方法都添加到其反射列表中,无论它们是否被使用,这可能会对编译时间和内存占用产生(小)影响。理想情况下,选项 1 优于选项 2。
只提供所需的类/方法将是最佳选择,但正如 @mipa 指出的,这将需要一些工具,可以发现哪些是这些。同时,您将不得不运行一些迭代,以确定默认的 JSON 文件是否包含您的 FXML 文件中使用的所有类/方法,如果没有,则将缺失的类/方法添加到您的反射文件中(或者只是将类名添加到反射列表中)。
英文:
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
undersrc/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).
答案2
得分: 1
你需要将更多通过FXML加载的类添加到反射列表中。例如,我还不得不添加javafx.geometry.HPos
和javafx.geometry.VPos
。这个过程的复杂性在于其中的一致性并不稳定。有些类已经默认包含在内,而其他一些则没有。你需要进行一些试验。有时,甚至必须指定类的父类,如果已在那里定义属性的话。为了我的目的,我编写了一个小工具来简化这个过程:https://github.com/mipastgt/JFXToolsAndDemos#fxml-checker
英文:
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
答案3
得分: 1
我只想在José的回答中再补充一个特定情况,并希望他能对此进行澄清。
另一个令人讨厌的问题是,有时仅将所需加载的类放入反射列表是不足够的。例如,如果您想加载一个 ProgressBar 并将其类放入反射列表中,仍然会出现以下错误: "ProgressBar 属性“progress”不存在或只读。" 原因是属性 "progress" 在 ProgressBar 的父类中定义,因此您还必须将 ProgressIndicator 也添加到列表中。
为什么会这样,有什么办法可以防止这种情况发生呢?
英文:
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?
答案4
得分: 0
这真让人沮丧,但最终我把所有事情都搞定了,以下是一些建议。
如果你有一个 fxml 文件,并且该文件在 Controller 中加载,只需将 Controller 添加到 reflectionList 中即可。
resourceReflection.json 文件不包含 JavaFX 类的所有属性,请检查是否在 json 文件中添加了所有属性,如果没有,只需将 fx 类添加到 reflectionList 中,因此添加
javafx.scene.layout.GridPane
将会添加 GridPane 的所有属性。
检查你在 fxml 中的导入类,其中一些可能没有添加到 json 文件中,在发现时将其添加到 reflectionList 中,例如如果你的 fxml 文件中有 RadioButton、ComboBox,你在生成的 json 文件中找不到它们。
所以:
- 将生成的 resourceReflection.json 复制并添加到 META-INF.substrate.config 目录中。
- 将所有你的类添加到 reflectionList 中,用于加载 fxml 文件的 View 不需要添加。
- 将 e.printStackTrace() 添加到你的 fxml 加载器中,以检查异常情况。
- 运行 mvn clean client:build client:run。
编码愉快!Gluon 团队干得好。
英文:
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 <reflectionList>
<list>javafx.scene.layout.GridPane</list>
</reflectionList> 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论