简体   繁体   中英

fx:include with "resources" throws MissingResourceException but works in Java Code

I'm trying to use the resource tag inside fx:include in a FXML file.

What I don't understand is, that loading the resource bundle manually with ResourceBundle.getBundle() works completely fine. I already tried a lot of variants in the fxml like:

  • "lang_challenges"

  • "wand555/github/io/challengesreworkedgui/lang_challenges"

package wand555.github.io.challengesreworkedgui;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;

public class ChallengeApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        ResourceBundle.clearCache();
        ResourceBundle bundle = new ResourceBundleWrapper(ResourceBundle.getBundle("wand555/github/io/challengesreworkedgui/lang_challenges"));
        System.out.println(bundle.getString("challenge.name")); // outputs "abc"
        FXMLLoader loader = new FXMLLoader(ChallengeApplication.class.getResource("overview.fxml"), bundle);
        Parent root = loader.load();
        Scene scene = new Scene(root, 1000, 1000);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }


    // this class effectively does nothing, but it will be loaded by the
    // application class loader
    // instead of the system class loader.
    private static class ResourceBundleWrapper extends ResourceBundle {

        private final ResourceBundle bundle;

        ResourceBundleWrapper(ResourceBundle bundle) {
            this.bundle = bundle;
        }

        @Override
        protected Object handleGetObject(String key) {
            return bundle.getObject(key);
        }

        @Override
        public Enumeration<String> getKeys() {
            return bundle.getKeys();
        }

        @Override
        public boolean containsKey(String key) {
            return bundle.containsKey(key);
        }

        @Override
        public Locale getLocale() {
            return bundle.getLocale();
        }

        @Override
        public Set<String> keySet() {
            return bundle.keySet();
        }

    }
}

overview.fxml :

<?xml version="1.0" encoding="UTF-8"?>

<AnchorPane prefHeight="500.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wand555.github.io.challengesreworkedgui.controllers.OverviewController">
   <children>
      <VBox>
         <children>
            <StackPane alignment="CENTER_LEFT">
               <children>
                  <Button fx:id="exportButton" mnemonicParsing="false" onAction="#onExport" text="Export" />
               </children>
            </StackPane>
            <HBox prefHeight="100.0" prefWidth="200.0" spacing="25.0">
               <children>
                  <fx:include fx:id="challengesOverview" source="challenges/challenges_overview.fxml" resources="lang_challenges" charset="utf-8"/>
                  <Separator orientation="VERTICAL" prefHeight="200.0" />
                  <fx:include source="goals/goal_overview.fxml" />
                  <Separator orientation="VERTICAL" prefHeight="200.0" />
               </children>
            </HBox>
         </children>
      </VBox>
   </children>
</AnchorPane>

The lang_challenges.properties contains a single key-value pair

challenge.name=abc

The `lang_challenges_de.properties' has the same content

challenge.name=abc

And this is the error message I'm getting

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:465)
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:364)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1082)
Caused by: java.lang.RuntimeException: Exception in Application start method
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:901)
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:196)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: javafx.fxml.LoadException: 
/Users/felixnaumann/Documents/ChallengesReworked/ChallengesReworkedGUI/target/classes/wand555/github/io/challengesreworkedgui/overview.fxml:21

    at javafx.fxml@19/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2707)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2685)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2548)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.load(FXMLLoader.java:2516)
    at challenges.reworked.gui/wand555.github.io.challengesreworkedgui.ChallengeApplication.start(ChallengeApplication.java:22)
    at javafx.graphics@19/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:847)
    at javafx.graphics@19/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:484)
    at javafx.graphics@19/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at javafx.graphics@19/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
    at javafx.graphics@19/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
Caused by: java.util.MissingResourceException: Can't find bundle for base name lang_challenges, locale de_DE
    at java.base/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2045)
    at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1683)
    at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1575)
    at java.base/java.util.ResourceBundle.getBundle(ResourceBundle.java:1280)
    at javafx.fxml@19/javafx.fxml.FXMLLoader$IncludeElement.processAttribute(FXMLLoader.java:1100)
    at javafx.fxml@19/javafx.fxml.FXMLLoader$Element.processStartElement(FXMLLoader.java:230)
    at javafx.fxml@19/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:755)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2808)
    at javafx.fxml@19/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2634)
    ... 9 more
Exception running application wand555.github.io.challengesreworkedgui.ChallengeApplication

Whew, finally figured it out after digging deep into the source code of ResourceBundle and ClassLoader .

How to fix it:

Really easy way:

Put your .properties files at the root of the resources folder. Then use it inside fxml

<fx:include source="second.fxml" resources="second_bundle"/>

and then everything should work fine. However this approach is not suitable for larger projects which subdivide the resource bundles into different packages.

Using sub-packages in resources package

Give the fully qualified path name in the resources tag. So for example if your package structure from src is com/example/demo/ ( also in the resources folder ) then use

<fx:include source="second.fxml" resources="com/example/demo/second_bundle"/>

But we are not done yet. You need to open the package to all modules in the module-info.java , explicitly using

opens com.example.demo to javafx.fxml;

does not work. Instead you need to write

opens com.example.demo;

My two cents

Honestly to me this sounds like a bug. I debugged the entire loading process and when the second_bundle is loaded from inside the fxml file, the caller module is javafx.fxml . And as the java doc for getResourceAsStream (which is internally called when loading) states:

A package name is derived from the resource name. If the package name is a package in the module then the resource can only be located by the caller of this method when the package is open to at least the caller's module. If the resource is not in a package in the module then the resource is not encapsulated.

So in theory opening your package to just javafx.fxml should work, but it doesn't...

Full demo project

This demo project uses sub-packages.

Project structure:

项目结构

HelloApplication :

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        ResourceBundle bundle = new ResourceBundleWrapper(ResourceBundle.getBundle("test_bundle"));
        System.out.println(bundle.getString("first.button")); // outputs "English/Deutsch"

        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("first.fxml"), bundle);
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }

    // this class effectively does nothing, but it will be loaded by the
    // application class loader
    // instead of the system class loader.
    private static class ResourceBundleWrapper extends ResourceBundle {

        private final ResourceBundle bundle;

        ResourceBundleWrapper(ResourceBundle bundle) {
            this.bundle = bundle;
        }

        @Override
        protected Object handleGetObject(String key) {
            return bundle.getObject(key);
        }

        @Override
        public Enumeration<String> getKeys() {
            return bundle.getKeys();
        }

        @Override
        public boolean containsKey(String key) {
            return bundle.containsKey(key);
        }

        @Override
        public Locale getLocale() {
            return bundle.getLocale();
        }

        @Override
        public Set<String> keySet() {
            return bundle.keySet();
        }

    }
}

first.fxml :

<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
      fx:controller="com.example.demo.HelloController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>

    <Label fx:id="welcomeText"/>
    <Button text="%first.button" onAction="#onHelloButtonClick"/>
    <fx:include source="second.fxml" resources="com/example/demo/second_bundle"/>
</VBox>

second.fxml :

<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>

    <Label fx:id="welcomeText"/>
    <Button text="%second.button"/>
</VBox>

test_bundle.properties :

first.button=English

second_bundle.properties :

second.button=Hello

module-info.java :

module com.example.demo {
    requires javafx.controls;
    requires javafx.fxml;
    
    opens com.example.demo;
    exports com.example.demo;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM