Come risolvere le eccezioni di caricamento fxml nel progetto JavaFX compilato utilizzando il client GluonHQ, Native Image e GraalVM?

Aug 22 2020

Questa è una domanda successiva a questa domanda.

Sto tentando di compilare un progetto JavaFX in un'immagine nativa in modo che venga eseguito in modo nativo senza che l'utente abbia bisogno di Java installato. I problemi con JavaFX e la reflection sono stati risolti con il plugin client GluonHQ, quindi la compilazione è ora un successo.

Sono riuscito a ottenere un semplice progetto JavaFX (l'esempio generato da IntelliJ durante la creazione di un progetto JavaFX) da compilare utilizzando il plug-in Maven client Gluon. Tuttavia, quando si esegue l'immagine nativa dalla riga di comando, fornisce un'eccezione di caricamento fxml JavaFX:

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

Era possibile far funzionare l'immagine nativa solo modificando sample.fxml da questo:

<?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>

a questo (rimuovendo gli attributi alignment, hgap e 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>

e quindi ricompilare. Il file binario compilato viene quindi eseguito come previsto.

Reflection è stato configurato come segue per il plugin Gluon in 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>

Queste eccezioni di caricamento FXML sono le stesse una volta che un progetto JavaFX di un progetto più grande viene compilato con la riflessione configurata. Le eccezioni dicono sempre che Caused by: com.sun.javafx.fxml.PropertyNotFoundException: Property [X] does not exist or is read-only.Entrambi i progetti funzionano correttamente sulla JVM, senza eccezioni generate. Il mio IDE non può rilevare errori con il codice.

Risposte

4 JoséPereda Aug 22 2020 at 17:51

Espanderò un po 'di più la risposta di @ mipa.

Come forse saprai, FXML è tutto basato sulla riflessione: abbiamo un file (f) xml e un parser ( FXMLLoader), che trova classi ( GridPane) e nomi di proprietà ( alignment) che vengono risolti in nomi di metodo ( setAlignment(Pos)e getAlignment()) durante l'analisi di quel file .

Per impostazione predefinita, il plug-in Client fornisce un reflectionConfig.jsonfile con la maggior parte delle classi e dei metodi JavaFX che potresti utilizzare nei tuoi file FXML.

Come puoi leggere qui , questo file viene generato quando esegui mvn client:compile(o mvn client:link) e può essere trovato sotto (con l'architettura di destinazione e il nome del sistema operativo).target/client/$arch-$os/gvm/reflectionconfig-$arch-$os.json

Come ora, contiene circa 290 classi (Java e JavaFX), con campi e metodi.

Se lo ispezioni, vedrai, per quella data GridPaneclasse:

,
  {
    "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"] }
    ]
  }
,

Come puoi notare, contiene il costruttore e tutti i metodi statici GridPane, quindi funziona con il plugin Client:

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

tuttavia, i alignmentmetodi non sono inclusi ed è per questo che il tuo fxml fallisce.

Ci sono due possibili soluzioni:

1. File di configurazione

Seguendo la sezione File di configurazione , puoi aggiungere il tuo file al tuo progetto e aggiungere i metodi mancanti:

  • Crea il file reflectionconfig.jsonsottosrc/main/resources/META-INF/substrate/config/

  • Aggiungi i metodi mancanti:

[
  {
    "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":[] }
    ]
  }
]
  • Esegui di nuovo mvn client:build client:run, questa volta dovrebbe funzionare.

Se esamini nuovamente il file, vedrai che il contenuto del tuo file json è stato incluso alla fine e ora tutti i metodi utilizzati sono disponibili per la riflessione.target/client/$arch-$os/gvm/reflectionconfig-$arch-$os.jsonGridPane

2. Elenco di riflessione

In alternativa, puoi semplicemente aggiungere l'intera classe a reflectionList:

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

Dopo averlo eseguito, ispezionando il file json vedrai:

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

La differenza con l'opzione 1 è ora che stai dicendo a GraalVM di aggiungere tutti i costruttori, campi e metodi dichiarati e pubblici di quella classe alla sua lista di riflessione, siano essi usati o meno, il che potrebbe avere un (piccolo) impatto nella compilazione impronta di tempo e memoria. Idealmente, l'opzione 1 sopra è migliore dell'opzione 2.

Fornire solo le classi / metodi richiesti sarà la cosa migliore, ma come sottolinea @mipa, ciò richiederà alcuni strumenti che potrebbero scoprire quali sono quelli. Nel frattempo, dovrai eseguire alcune iterazioni per scoprire se tutte le classi / metodi utilizzati nei tuoi file FXML sono inclusi o meno dal file json predefinito e aggiungere quelli mancanti al tuo file di riflessione (o semplicemente il nome delle classi alla lista di riflessione).

1 mipa Aug 22 2020 at 02:34

Devi aggiungere più classi, che vengono caricate tramite FXML, all'elenco di riflessioni. Ad esempio, ho anche dovuto aggiungere javafx.geometry.HPose javafx.geometry.VPos. Ciò è complicato dal fatto che ciò non è coerente. Alcune classi sono già incluse per impostazione predefinita, altre no. Avrai bisogno di qualche sperimentazione qui. A volte dovevo persino specificare il genitore di una classe se una proprietà è già definita lì. Per i miei scopi ho scritto un piccolo strumento per renderlo più semplice:https://github.com/mipastgt/JFXToolsAndDemos#fxml-checker

1 mipa Aug 22 2020 at 19:33

Vorrei solo aggiungere un caso più specifico alla risposta di José e forse può anche chiarire questo.

Un altro fastidioso problema è che a volte non è sufficiente inserire semplicemente la classe che si desidera caricare nell'elenco di riflessione. Ad esempio, se si desidera caricare un ProgressBar e hanno messo questa classe nella lista di riflessione, si continua a ottenere il seguente errore: ProgressBar Property "progress" does not exist or is read-only. Il motivo è che la proprietà "progress" è definita nella superclasse di ProgressBar e quindi è necessario aggiungere anche ProgressIndicator all'elenco.

Perché è così e può essere prevenuto in qualche modo?

MingxingChen Sep 16 2020 at 12:09

È così frustrante, ma finalmente riesco a sistemare tutto, ecco alcuni consigli. Se hai un file fxml e il file è caricato in Controller, basta aggiungere il Controller in reflectionList. ResourceReflection.json non contiene tutte le proprietà per la classe javafx, controlla se tutte le proprietà sono state aggiunte in json, altrimenti aggiungi semplicemente la classe fx in reflectionList, quindi aggiungi javafx.scene.layout.GridPane aggiungerà tutte le proprietà di GridPane. Controlla la tua classe di importazione in fxml, alcuni di essi potrebbero non essere aggiunti nel file json, aggiungili a reflectionList ogni volta che lo trovi, ad esempio se hai RadioButton, ComboBox nel file fxml, non li troverai nel file json generato. Quindi 1. Copiare e aggiungere resourceReflection.json generato nella directory META-INF.substrate.config. 2. Aggiungi tutta la TUA classe a reflectionList, non è necessario aggiungere la vista utilizzata per caricare il file fxml. 3. Aggiungi e.printStackTrace () al tuo caricatore fxml per controllare cosa succede in eccezione. 4. mvn clean client: build client: run.

Buona programmazione! Buon lavoro Gluon team.