我该如何确定JavaFX应用程序所需的FXML文件、CSS文件、图像和其他资源的正确路径?

38

我的JavaFX应用程序需要能够找到FXML文件以使用FXMLLoader加载它们,还包括样式表(CSS文件)和图像。当我尝试加载这些文件时,我经常会遇到错误,或者我正在尝试加载的项目在运行时根本没有加载。

对于FXML文件,我看到的错误消息包括:

Caused by: java.lang.NullPointerException: location is not set

对于图像,堆栈跟踪包括

Caused by: java.lang.IllegalArgumentException: Invalid URL: Invalid URL or resource not found

我该如何找到这些资源的正确资源路径?


由于JavaFX标签上有很多关于加载资源的问题,因此我将此Q&A发布为社区维基。如果您认为问题或答案可以改进,请编辑它们。 - James_D
3
好主意和好答案 :) 添加到标签 wiki 的 FAQ 中,这样我们可以轻松地找到它来关闭重复问题。 - kleopatra
此外,您可以检查目标目录中的所有已编译类。有时,IDE 不希望编译这些文件,因此您无法在运行时获取它们。 - psyopus
@psyopus 这个问题在答案的“故障排除”部分有讨论。 - James_D
1个回答

71

简短的回答:

  • 使用 getClass().getResource(...)SomeOtherClass.class.getResource(...) 创建指向资源的 URL
  • 将绝对路径(以前导的 /)或相对路径(不带前导的 /)传递给 getResource(...) 方法。路径是包含资源的,其中 . 替换为 /
  • 不要使用 .. 在资源路径中。如果应用程序被打包为 jar 文件,它将无法工作。如果资源不在类的相同包或子包中,请使用绝对路径。
  • 对于 FXML 文件,直接将 URL 传递给 FXMLLoader
  • 对于图像和样式表,在 URL 上调用 toExternalForm() 以生成要传递给 ImageImageView 构造函数的 String,或者添加到 stylesheets 列表中。
  • 要排除故障,检查您的 build 文件夹(或 jar 文件)中的内容,而不是您的 source 文件夹。
  • 在获取资源时将 src 放入路径中总是错误的。 src 目录仅在开发和构建时可用,而不是在部署和运行时。

完整答案

目录

  1. 本答案的范围
  2. 资源在运行时加载
  3. JavaFX使用URL来加载资源
  4. 资源名称的规则
  5. 使用getClass().getResource(...)创建资源URL
  6. 组织代码和资源
  7. Maven(和类似的)标准布局
  8. 故障排除

本答案的范围

请注意,本答案仅涉及加载作为应用程序一部分和与之捆绑的资源(例如FXML文件、图像和样式表)。因此,例如加载用户从运行应用程序的计算机上的文件系统中选择的图像将需要不在此处介绍的不同技术。

资源在运行时加载

了解有关加载资源的第一件事是,它们当然是在运行时加载的。通常,在开发过程中,应用程序是从文件系统运行的:即,用于运行它的类文件和资源是文件系统上的单个文件。但是,一旦构建了应用程序,它通常就会从JAR文件执行。在这种情况下,诸如FXML文件、样式表和图像之类的资源不再是文件系统上的单个文件,而是JAR文件中的条目。因此:

代码不能使用FileFileInputStreamfile: URL加载资源

JavaFX使用URL来加载资源

JavaFX使用URL加载FXML、图像和CSS样式表。

FXMLLoader明确期望传递一个java.net.URL对象(可以通过static FXMLLoader.load(...)方法、FXMLLoader构造函数或setLocation()方法传递)。

无论是Image还是Scene.getStylesheets().add(...),都需要代表URL的String。如果传递的URL没有方案,它们将相对于类路径进行解释。这些字符串可以通过在URL上调用toExternalForm()以强健的方式创建。

为资源创建正确的URL的推荐机制是使用Class.getResource(...),它在适当的Class实例上调用。可以通过调用getClass()(给出当前对象的类)或ClassName.class来获得这样的类实例。Class.getResource(...)方法采用表示资源名称的String

资源名称的规则

  • 资源名称是由/分隔的路径名。每个组件表示一个包或子包名称组件。
  • 资源名称区分大小写。
  • 资源名称中的各个组件必须是有效的Java标识符

最后一点有一个重要的结果:

...不是有效的Java标识符,因此它们不能用于资源名称

尽管这些在应用程序从文件系统运行时可能会起作用,但这实际上更像是getResource()实现的意外。当应用程序打包为Jar文件时,它们将失败。

同样,如果您在不区分仅通过大小写差异的文件名的操作系统上运行,则在从文件系统运行时使用错误的大小写命名资源将起作用,但在从jar文件运行时将失败。

以前导/开头的资源名称是绝对的:换句话说,它们是相对于类路径解释的。没有前导/的资源名称被解释为相对于调用getResource()的类。

稍微变化一下,可以使用getClass().getClassLoader().getResource(...)。提供给ClassLoader.getResource(...)的路径不得/开头,它始终是绝对的,即相对于类路径。还应注意,在模块化应用程序中,使用ClassLoader.getResource()访问资源在某些情况下受到强封装规则的限制,并且包含资源的包必须无条件打开。有关详细信息,请参阅documentation

使用getClass().getResource()创建资源URL

要创建资源URL,请使用someClass.getResource(...)。通常,someClass表示当前对象的类,并使用getClass()获取。但是,这不一定是这种情况,如下一节所述。

  • If the resource is in the same package as the current class, or in a subpackage of that class, use a relative path to the resource:

     // FXML file in the same package as the current class:
     URL fxmlURL = getClass().getResource("MyFile.fxml");
     Parent root = FXMLLoader.load(fxmlURL);
    
     // FXML file in a subpackage called `fxml`:
     URL fxmlURL2 = getClass().getResource("fxml/MyFile.fxml");
     Parent root2 = FXMLLoader.load(fxmlURL2);
    
     // Similarly for images:
     URL imageURL = getClass().getResource("myimages/image.png");
     Image image = new Image(imageURL.toExternalForm());
    
  • If the resource is in a package that is not a subpackage of the current class, use an absolute path. For example, if the current class is in the package org.jamesd.examples.view, and we need to load a CSS file style.css which is in the package org.jamesd.examples.css, we have to use an absolute path:

     URL cssURL = getClass().getResource("/org/jamesd/examples/css/style.css");
     scene.getStylesheets().add(cssURL.toExternalForm());
    

    It's worth re-emphasizing for this example that the path "../css/style.css" does not contain valid Java resource names, and will not work if the application is bundled as a jar file.

代码和资源的组织

我建议按照与UI相关部分进行划分,将代码和资源组织成包。以下是Eclipse中的源代码布局示例:

enter image description here

使用这种结构,每个资源在同一个包中都有一个类,因此很容易为任何资源生成正确的URL:

FXMLLoader editorLoader = new FXMLLoader(EditorController.class.getResource("Editor.fxml"));
Parent editor = editorLoader.load();
FXMLLoader sidebarLoader = new FXMLLoader(SidebarController.class.getResource("Sidebar.fxml"));
Parent sidebar = sidebarLoader.load();

ImageView logo = new ImageView();
logo.setImage(newImage(SidebarController.class.getResource("logo.png").toExternalForm()));

mainScene.getStylesheets().add(App.class.getResource("style.css").toExternalForm());

如果你有一个只包含资源而没有类的包,例如下面布局中的images

enter image description here

你甚至可以考虑创建一个“标记接口”,仅为查找资源名称而存在:

package org.jamesd.examples.sample.images ;
public interface ImageLocation { }

现在,您可以轻松找到这些资源:

Image clubs = new Image(ImageLocation.class.getResource("clubs.png").toExternalForm());

从类的子包中加载资源也相当简单。假设有以下布局:

enter image description here

我们可以按照以下方式在App类中加载资源:
package org.jamesd.examples.resourcedemo;
import java.net.URL;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class App extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
                
        URL fxmlResource = getClass().getResource("fxml/MainView.fxml");
        
        Parent root = FXMLLoader.load(fxmlResource);
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource("style/main-style.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        Application.launch(args);
    }
}

要加载不在同一包或子包中的类加载器中的资源,需要使用绝对路径:

    URL fxmlResource = getClass().getResource("/org/jamesd/examples/resourcedemo/fxml/MainView.fxml");

Maven(以及类似工具)的标准布局

Maven和其他依赖管理和构建工具推荐一种源代码文件夹布局,其中资源与Java源文件分开存放,按照Maven标准目录布局。前面示例的Maven布局版本如下:

enter image description here

重要的是要了解如何构建应用程序:
  • 位于src/main/java源文件夹中的*.java文件被编译为类文件,并部署到构建文件夹或jar文件中。
  • 位于src/main/resources资源文件夹中的资源被复制到构建文件夹或jar文件中。
在此示例中,由于资源位于与定义源代码的包的子包相对应的文件夹中,因此生成的构建(默认情况下使用Maven在target/classes中)由单个结构组成。
请注意,src/main/javasrc/main/resources都被视为相应结构在构建中的根目录,因此只有它们的内容而不是文件夹本身是构建的一部分。换句话说,在运行时没有可用的resources文件夹。构建结构在“故障排除”部分中显示。
请注意,在这种情况下(Eclipse),IDE以不同的方式显示src/main/java源文件夹和src/main/resources文件夹。对于第一种情况,它显示,但对于资源文件夹,它显示文件夹。确保您知道您的IDE是否正在创建包(其名称以分隔)或文件夹(其名称不得包含或任何其他Java标识符中无效的字符)。
如果您正在使用Maven,并决定为了便于维护,您更愿意将.fxml文件放在引用它们的.java文件旁边(而不是严格遵循Maven Standard Directory Layout),则可以这样做。只需在pom.xml文件中包含类似以下内容,告诉Maven将这些文件复制到与生成的类文件相同的文件夹中,该类文件是从这些源文件生成的:
    <build>
        ...
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.fxml</include>
                <include>**/*.css</include>
            </includes>
        </resource>
        ...
    </build>

如果您这样做,就可以使用类似于FXMLLoader.load(getClass().getResource("MyFile.fxml"))的方法,从包含它们自己的.class文件的目录中加载.fxml资源。
故障排除
如果出现意外错误,请首先检查以下内容:
- 确保您没有为资源使用无效的名称。这包括在资源路径中使用..。 - 确保您在预期的地方使用相对路径和绝对路径。对于Class.getResource(...),如果路径以/开头,则路径是绝对的,否则是相对的。对于ClassLoader.getResource(...),路径始终是绝对的,并且不能/开头。 - 请记住,绝对路径是相对于类路径定义的。通常,类路径的根是IDE中所有源文件和资源文件的联合体。
如果所有这些都正确,并且仍然看到错误,请检查构建或部署文件夹。此文件夹的确切位置将因IDE和构建工具而异。如果您使用Maven,默认情况下是target/classes。其他构建工具和IDE将部署到名为binclassesbuildout的文件夹中。
通常,您的IDE不会显示构建文件夹,因此您可能需要使用系统文件浏览器进行检查。
上述Maven示例的组合源和构建结构为:

enter image description here

如果你正在生成一个jar文件,一些IDE可能允许你以树形视图展开jar文件以检查其内容。你也可以通过命令行使用jar tf file.jar来检查内容:

$ jar -tf resource-demo-0.0.1-SNAPSHOT.jar 
META-INF/
META-INF/MANIFEST.MF
org/
org/jamesd/
org/jamesd/examples/
org/jamesd/examples/resourcedemo/
org/jamesd/examples/resourcedemo/images/
org/jamesd/examples/resourcedemo/style/
org/jamesd/examples/resourcedemo/fxml/
org/jamesd/examples/resourcedemo/images/so-logo.png
org/jamesd/examples/resourcedemo/style/main-style.css
org/jamesd/examples/resourcedemo/Controller.class
org/jamesd/examples/resourcedemo/fxml/MainView.fxml
org/jamesd/examples/resourcedemo/App.class
module-info.class
META-INF/maven/
META-INF/maven/org.jamesd.examples/
META-INF/maven/org.jamesd.examples/resource-demo/
META-INF/maven/org.jamesd.examples/resource-demo/pom.xml
META-INF/maven/org.jamesd.examples/resource-demo/pom.properties
$ 

如果资源没有被部署,或者被部署到了意外的位置,请检查您的构建工具或IDE的配置。

示例图像加载故障排除代码

这段代码故意比必要的更冗长,以便为图像加载过程添加额外的调试信息。 它还使用System.out而不是记录器,以便更易于移植。

String resourcePathString = "/img/wumpus.png";
Image image = loadImage(resourcePathString);

// ...

private Image loadImage(String resourcePathString) {
    System.out.println("Attempting to load an image from the resourcePath: " + resourcePathString);
    URL resource = HelloApplication.class.getResource(resourcePathString);
    if (resource == null) {
        System.out.println("Resource does not exist: " + resourcePathString);

        return null;
    }

    String path = resource.toExternalForm();
    System.out.println("Image path: " + path);

    Image image = new Image(path);
    System.out.println("Image load error?  " + image.isError());
    System.out.println("Image load exception? " + image.getException());

    if (!image.isError()) {
        System.out.println("Successfully loaded an image from " + resourcePathString);
    }

    return image;
}

外部教程参考

资源定位的有用外部教程是Eden编码的教程:

Eden编码教程的好处在于它是全面的。除了涵盖本问题中关于从Java代码进行查找的信息外,Eden教程还涵盖了其他主题,例如在CSS中作为url编码的资源的定位,或者使用@指定符或fx:include元素在FXML中引用资源(这些主题目前不在本回答中直接涵盖)。


2
提到大小写处理的潜在差异是一个很好的补充 - 这就是为什么我更喜欢对所有资源名称使用小写字母(虽然这不是一种命名约定)。 - kleopatra
3
一些框架(例如afterburner.fx和FXWeaver)要求控制器类名与FXML文件名相匹配,这会强制使FXML文件名为大写。 - James_D
1
关于FXML如何处理@符号的评论将会很有帮助。例如:<URL value="@/styles/root.css" /> - Will Hartung
1
你可能需要考虑使用ClassLoader API对查找进行一些小的更改:也许强调它 不能 有前导斜杠(正如jewelsea在你的另一个答案https://stackoverflow.com/a/68913233/203657中指出的那样)。 - kleopatra
谢谢编辑 :) 或许在故障排除部分提醒不要使用前导斜杠? - kleopatra
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接