【问题标题】:Strange JavaFX NPE "Location is required"奇怪的 JavaFX NPE“需要位置”
【发布时间】:2018-04-20 09:20:30
【问题描述】:

我正在构建一个包含大量场景的大型 JavaFX 应用程序,现在正尝试使用 Maven 部署该应用程序。

Maven 需要资源,例如FXML 文件,放在src/main/resources 下。这目前与我将新场景加载到主场景容器中的方式发生冲突。

我正在使用jewelsea 的一个小框架来加载新内容:Small JavaFX framework for swapping in and out child panes in a main FXML container.

到目前为止,这种方法效果很好。但是,现在我正在尝试从 src/main/resources 获取资源(FXML 文件),我遇到了 NullPointerExceptions。

我可以使用下面的方法很好地加载我的MAIN 视图,但所有其他视图都会抛出 NPE。

MAIN 场景是这样加载的:

FXMLLoader loader = new FXMLLoader();
Pane mainPane = loader.load(getClass().getResource(ViewNavigator.MAIN));
MainController mainController = loader.getController();
ViewNavigator.setMainController(mainController);
ViewNavigator.loadVista(ViewNavigator.CONFIGURE);

注意最后一行。我使用函数loadVista 来注入CONFIGURE 视图。该函数在ViewNavigator 类中声明:

static void loadVista(String fxml) {
    try {
        mainController.setVista(
                FXMLLoader.load(
                        ViewNavigator.class.getClass().getResource(
                                fxml
                        )
                )
        );
    } catch (IOException e) {
        e.printStackTrace();
    }
}

如您所见,exact 相同的方法用于获取 CONFIGURE 场景,但这次我遇到了 NPE:

Caused by: java.lang.NullPointerException: Location is required.
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3246)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3210)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3179)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3152)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3129)
at javafx.fxml/javafx.fxml.FXMLLoader.load(FXMLLoader.java:3122)
at com.demant.controller.ViewNavigator.loadVista(ViewNavigator.java:33)
at com.demant.controller.MainController.configureView(MainController.java:99)
... 58 more

ViewNavigator 内部,MAINCONFIGURE 视图的 URL 声明如下:

static final String MAIN      = "MainControllerView.fxml";
static final String CONFIGURE = "ConfigureView.fxml";

显然,我希望将我的资源文件放在src/main/resources 下,并将包标记为resources root

希望有人能指出正确的方向。

谢谢。

【问题讨论】:

  • 打开文件浏览器并查看您的构建文件夹(target/classes,我认为,是使用 Maven 时的默认设置)。考虑到源层次结构中的设置,这应该有助于您了解所有内容在运行时的放置位置。

标签: java maven intellij-idea javafx


【解决方案1】:

FXML 文件是在运行时加载的,因此从某种意义上说,您的源代码层次结构是完全不相关的;唯一重要的是类路径的内容。当然,您的 IDE(在这种情况下通过 Maven)决定如何编译源层次结构中的源代码和资源并将其部署到将成为类路径层次结构的位置。

所以要做到这一点,你需要了解两件事:第一,你的不同源文件夹中定义的各种资源在类路径中的最终位置是什么,第二,Class.getResource(resourceName) 如何解析指定的resourceName 寻找资源。

对于第一个问题,Maven 喜欢将需要编译为类文件的 Java 源文件与其他“静态”资源分开。 (这在 Web 应用程序中很有意义,这些应用程序通常部署到不同的位置;恕我直言,它在桌面应用程序中意义不大;但这有点离题了。)

默认情况下,Java 源代码位于文件夹 src/main/java 中。编译该文件夹中的源代码并将生成的类文件部署到构建文件夹(默认为target/classes)。关于 Java 包的通常规则适用,因此如果您在名为 org.example 的包中有一个类 Main,则源代码将在 src/main/java/org/example/Main.java 中,编译后的代码将在 target/classes/org/example/Main.class 中。

同样默认情况下,非 Java 资源放置在 src/main/resources 中。此文件夹中的文件被部署(即复制)到同一个构建文件夹。因此,如果您有一个 FXML 文件,例如 Sample.fxml,并且您希望它最终出现在包 org.example 中,您可以将它放在 src/main/resources/org/example/Sample.fxml 中。项目建好后会复制到target/classes/org/example/Sample.fxml

对于第二个问题,Class 实例有一个 getResource(resourceName) 方法,该方法将返回一个表示资源的 URL。 (因此,如果您从文件系统中的类运行,它将返回带有 file: 方案的 URL;如果您从 jar 文件运行,它将返回带有 jar: 方案的 URL,等等。)我喜欢将Class.getResource(...) 视为“在与当前类相同的位置查找资源”的意思。如果您的资源名称是absolute,这意味着它以/ 开头,那么将从类路径的根目录中搜索该资源。如果资源名称是relative,即它不以/ 开头,则从包中搜索当前类的资源。请注意,资源名称的组成部分(/ 之间的部分)必须是有效的 Java 标识符。特别是,这意味着您不能假设您正在从文件系统加载资源(通常不是),并且包含... 的名称是无效的。因此只能在当前类的包、该包的子包或使用绝对资源名称中进行搜索。

这是一个简单的例子。假设我们正在制作带有联系信息的常用演示 JavaFX 程序。我们需要(至少)一个主类,我称之为ContactApp,一个FXML 文件,Contacts.fxml,和一个控制器类,ContactController。我会将主类ContactApp 放在一个名为org.jamesd.fxtest 的包中,将控制器类放在一个子包org.jamesd.fxtest.contacts 中。

我喜欢将 FXML 文件与其对应的控制器类放在同一个包中(这样做的好处很快就会显现出来),所以 Contacts.fxml 也将放在 org.jamesd.fxtest.contacts 中。

这是项目布局的屏幕截图:

要确认这些正在按预期部署,请使用系统文件浏览器(Finder、Windows 资源管理器等)查看“build”文件夹,默认为 target/classes

如您所见,正如预期的那样,FXML文件和控制器类部署到包org.jamesd.fxtest.contacts,主类部署到org.jamesd.fxtest

要从 ContactApp 类加载 FXML 文件,我可以指定一个绝对资源名称(同样,这意味着相对于类路径):

URL location = getClass().getResource("/org/jamesd/fxtest/contacts/Contacts.fxml");

或者,我可以使用相对资源名称。由于当前课程在org.jamesd.fxtest 中,而我想要的资源在org.jamesd.fxtest.contacts 中,因此以下将起作用:

URL location = getClass().getResource("contacts/Contacts.fxml");

我更喜欢的方法是将 FXML 文件与其对应的控制器放在同一个包中。这样做的好处是我可以使用控制器类来加载资源。这里的一个优点是重构更容易,因为代码不太依赖 FMXL 文件的实际位置(任何 IDE 都可以轻松处理重构导入)。这是使用这种方法的整个ContactApp 类。

package org.jamesd.fxtest;

import java.net.URL;

import org.jamesd.fxtest.contacts.ContactController;

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

public class ContactApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        URL location = ContactController.class.getResource("Contacts.fxml");
        FXMLLoader loader = new FXMLLoader(location);
        Parent root = loader.load() ;
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

}

因此,在您的示例中,我建议将您的 FXML 文件放在与 ViewNavigator 相同的包中:这意味着如果 ViewNavigator 在包 com.demant.controller 中,那么 FXML 文件将放在 src/main/resources/com/demant/controller 中。然后只需使用ViewNavigator.class.getResource("ConfigureView.fxml") 就可以了。如果还是不行,可以在系统文件浏览器中打开target/classes来诊断问题。

最后,注意你想要的

ViewNavigator.class.getResource(...)

不是

ViewNavigator.class.getClass().getResource(...)

在后一个代码中,ViewNavigator.class 解析为Class<ViewNavigator> 的实例,因此ViewNavigator.class.getClass() 解析为Class<Class<?>> 的实例。由于Classjava.lang 中,您将在类路径中搜索java.lang.ConfigureView.fxml,这显然不(也不应该)存在。

【讨论】:

  • 感谢您非常详尽的回答!我遵循了您的建议,现在程序可以正常编译和运行。感谢详细介绍所带来的额外好处,这让我对类路径和类层次结构有了更深入的了解。
【解决方案2】:

您在Class.class 上使用getResource。 (ViewNavigator.class 计算为包含ViewNavigator 类信息的Class 对象,getClass() 返回此对象的类型。)因此getResource 正在路径/java/lang/ResourceName 中寻找资源。

您需要使用ViewNavigator 类来解析资源:

mainController.setVista(
        FXMLLoader.load(
                ViewNavigator.class.getResource(
                        fxml
                )
        )
);

或使用绝对路径:

static final String MAIN      = "/com/demant/controller/MainControllerView.fxml";
static final String CONFIGURE = "/com/demant/controller/ConfigureView.fxml";

【讨论】:

  • 感谢您的回复。将 ViewNavigator.class.getClass() 更改为 ViewNavigator.class.getResource() 并没有什么不同。它仍然抛出相同的 NPE。另外,我不能使用您提到的路径,因为 Maven 期望 FXML 文件位于 src/main/resources 文件夹中。
  • @kdhansen 您确实在资源根目录下使用了相同的包结构。 (至少这是工作代码 sn-p 所暗示的。或者包含加载主 fxml 的代码的类是什么?)如果没有更多信息,我唯一能想到的是资源名称中的拼写错误。
  • 我理解你想要表达的意思。在实施 Maven 以部署应用程序之前,我正在为我的 FXML 文件使用这些确切的路径,就像在 "/com/demant/controller/MainControllerView.fxml" 中一样。但是,现在我使用 Maven 进行部署,我将我的 FXML 文件放在 src/main/resources 下,因为 Maven 在编译和打包时希望这些文件驻留在此处。让我感到困惑的是MainControllerView.fxml 文件可以很好地位于src/main/resources 文件夹中,而其他文件则不能。我在任何地方都找不到任何错别字。我被难住了!
  • 加载MainViewController.fxml 的类文件位于src/main/java/com/demant/controller/MainController 下。我将 java 标记为 sources root.
  • @kdhansen 您的 FXML 文件应位于 src/main/resources/com/demant/controller/MainControllerView.fxml 等文件夹中。这假设 ViewNavigatorcom.demant.controller 包中。将src/main/resources 设置为“资源根”意味着内容将部署到类路径的根。在运行时,ViewNavigator.class.getResource(...) 将相对于 ViewNavigator 在类路径中的位置进行搜索,即它将在定义 ViewNavigator 的包中进行搜索。 (对于绝对路径,它相对于类路径进行搜索。)
【解决方案3】:

在您的 fxml 文件中,您需要指定要使用的控制器

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

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import java.util.ArrayList?>

<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.MyController">
    <Label  fx:id="label1"/>
    <Label  fx:id="label2"/>
</VBox>

【讨论】:

  • 感谢您的快速回复,但是,在我所有的 FXML 文件中,我已经声明了要使用的控制器。这是我的 MainControllerView.fxml 的摘录:[...] fx:controller="com.demant.controller.MainController">
猜你喜欢
  • 2014-03-26
  • 2019-03-26
  • 2019-10-10
  • 1970-01-01
  • 2014-02-03
  • 2015-02-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多