如何使用 GluonHQ 客户端、Native Image 和 GraalVM 解决编译的 JavaFX 项目中的 fxml 加载异常?

cs-*_*-uk 2 java linux javafx maven graalvm

这是这个问题的后续问题。

我正在尝试将 JavaFX 项目编译为本地映像,以便它可以在本地运行,而无需用户安装 Java。GluonHQ客户端插件解决了JavaFX和反射的问题,现在编译成功。

我设法使用 Gluon 客户端 maven 插件编译了一个简单的 JavaFX 项目(IntelliJ 在创建 JavaFX 项目时生成的示例)。但是,在命令行中运行本机映像时,会出现 JavaFX fxml 加载异常:

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
Run Code Online (Sandbox Code Playgroud)

只有通过更改 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>
Run Code Online (Sandbox Code Playgroud)

对此(删除对齐、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>
Run Code Online (Sandbox Code Playgroud)

然后重新编译。编译后的二进制文件然后按预期运行。

POM.xml 中的 Gluon 插件的反射配置如下:

<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>
Run Code Online (Sandbox Code Playgroud)

一旦使用配置的反射编译较大的项目 JavaFX 项目,这些 FXML 加载异常是相同的。异常总是说Caused by: com.sun.javafx.fxml.PropertyNotFoundException: Property [X] does not exist or is read-only.两个项目在 JVM 上都运行良好,没有抛出异常。我的 IDE 无法检测到代码错误。

Jos*_*eda 5

我将进一步扩展@mipa 的答案。

您可能知道,FXML 完全是关于反射的:我们有一个 (f)xml 文件和一个解析器 ( FXMLLoader),用于查找在解析该文件时解析为方法名称 ( and ) 的类 ( GridPane) 和属性名称 ( alignment) .setAlignment(Pos)getAlignment()

默认情况下,客户端插件为您提供一个reflectionConfig.json文件,其中包含您可能在 FXML 文件中使用的大多数JavaFX 类和方法。

正如您在此处阅读的那样,该文件是在您运行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"] }
    ]
  }
,
Run Code Online (Sandbox Code Playgroud)

如您所见,它包含构造函数和所有静态方法GridPane,因此它适用于客户端插件:

<GridPane>
    <Label text="a label" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
</GridPane>
Run Code Online (Sandbox Code Playgroud)

但是,这些alignment方法不包括在内,这就是您的 fxml 失败的原因。

有两种可能的解决方案:

1. 配置文件

Config files部分之后,您可以将自己的文件添加到您的项目中并添加缺少的方法:

  • reflectionconfig.json在下创建文件src/main/resources/META-INF/substrate/config/

  • 添加缺少的方法:

[
  {
    "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 Code Online (Sandbox Code Playgroud)
  • 再次运行mvn client:build client:run,这次应该可以了。

如果你再次检查target/client/$arch-$os/gvm/reflectionconfig-$arch-$os.json文件,你会看到你的json文件的内容已经包含在最后,现在所有使用的GridPane方法都可以用于反射。

2. 反射列表

或者,您可以简单地将整个类添加到反射列表中:

<reflectionList>
    <list>javafx.scene.layout.GridPane</list>
</reflectionList>
Run Code Online (Sandbox Code Playgroud)

运行后,检查 json 文件,您将看到:

  {
    "name" : "javafx.scene.layout.GridPane",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredFields" : true,
    "allPublicFields" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true
  }
Run Code Online (Sandbox Code Playgroud)

与选项 1 的不同之处在于现在您告诉 GraalVM 将该类的所有声明和公共构造函数、字段和方法添加到其反射列表中,无论是否使用它们,这可能会对编译产生(小)影响时间和内存占用。理想情况下,上述选项 1 优于选项 2。

最好提供所需的类/方法,但正如@mipa 指出的那样,这将需要一些工具来发现哪些是那些。同时,您将不得不运行一些迭代来确定 FXML 文件中使用的所有类/方法是否包含在默认 json 文件中,并将缺少的添加到反射文件中(或仅将类名添加到到反射列表)。