图像不是从 JavaFX 项目中 gradle 构建的 jar 加载的

Image isn't loaded from jar builded by gradle in JavaFX project

我使用 JavaFX 制作了一个应用程序,当我使用 IntelliJ IDE 上的构建按钮构建项目时它可以正常工作。 但是,当我从 gradle JavaFX 项目生成的 jar 文件中 运行 并且控制台上显示以下错误时,它不起作用。

$java -jar sample-menu-all.jar

java.io.FileNotFoundException: file:/Users/myuser/MyProjectRoot/build/libs/sample-menu-all.jar!/images/main_menu/icon.svg (No such file or directory)
    at java.base/java.io.FileInputStream.open0(Native Method)
    at java.base/java.io.FileInputStream.open(FileInputStream.java:220)
    at java.base/java.io.FileInputStream.<init>(FileInputStream.java:158)
    at java.base/java.io.FileInputStream.<init>(FileInputStream.java:113)
    at java.base/java.io.FileReader.<init>(FileReader.java:58)
    at presentation.utils.ImageUtil.loadSvgImage(ImageUtil.java:23)
    at presentation.controller.MenuController.loadRegistrationButtonImage(MenuController.java:25)
    at presentation.controller.MenuController.initialize(MenuController.java:20)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2573)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2466)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3253)
    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.load(FXMLLoader.java:3144)
    at presentation.navigator.VitalBitMenuNavigator.launchNewScene(VitalBitMenuNavigator.java:22)
    at presentation.navigator.VitalBitMenuNavigator.launchMenuStage(VitalBitMenuNavigator.java:33)
    at presentation.MainApp.start(MainApp.java:20)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1(LauncherImpl.java:919)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait(PlatformImpl.java:449)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater(PlatformImpl.java:418)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater(PlatformImpl.java:417)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
javafx.fxml.LoadException:
file:/Users/myuser/MyProjectRoot/build/libs/sample-menu-all.jar!/fxml/menu.fxml

    at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2625)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2603)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2466)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3253)
    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.load(FXMLLoader.java:3144)
    at presentation.navigator.VitalBitMenuNavigator.launchNewScene(VitalBitMenuNavigator.java:22)
    at presentation.navigator.VitalBitMenuNavigator.launchMenuStage(VitalBitMenuNavigator.java:33)
    at presentation.MainApp.start(MainApp.java:20)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1(LauncherImpl.java:919)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait(PlatformImpl.java:449)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater(PlatformImpl.java:418)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater(PlatformImpl.java:417)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
Caused by: java.lang.NullPointerException
    at javafx.swing/javafx.embed.swing.SwingFXUtils.toFXImage(SwingFXUtils.java:77)
    at presentation.utils.ImageUtil.loadSvgImage(ImageUtil.java:30)
    at presentation.controller.MenuController.loadRegistrationButtonImage(MenuController.java:25)
    at presentation.controller.MenuController.initialize(MenuController.java:30)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2573)
    ... 15 more

我的目录结构是这样的

ProjectRootDir
|-- build
  |-- libs
    sample-menu-all.jar
|-- build.gradle
|-- src
  |-- main
    |-- java
      |-- presentation
        MainApp.java
        |-- controller
          |-- MenuController.java
        |-- navigator 
          |-- MenuNavigator.java
        |-- utils
          |-- ImageUtile.java
    |-- resources
      |-- fxml
        |-- menu.fxml
      |-- properties
        |-- string.properties

gradle 文件、main.java 和 class fxml 加载在 snnipet link.

build.gradle

    buildscript {
    dependencies {
        classpath group: 'de.dynamicfiles.projects.gradle.plugins', name: 'javafx-gradle-plugin', version: '8.8.2'
        classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
    }
    repositories {
        jcenter()
        mavenLocal()
        mavenCentral()
    }
}

plugins {
    id 'java'
}

version '1.0-SNAPSHOT'
apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'javafx-gradle-plugin'

mainClassName = 'presentation.MainApp'
sourceCompatibility = 1.8

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    // Camera lib
    compile 'com.github.sarxos:webcam-capture:0.3.12'
    compile 'org.slf4j:slf4j-log4j12:1.7.21'
    // RxJavaFX
    implementation "io.reactivex.rxjava2:rxjava:2.2.2"
    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
    // Batik (for loading AVG)
    compile group: 'org.apache.xmlgraphics', name: 'batik-transcoder', version: '1.10'
}


jfx {
    mainClass = 'MainApp'
    vendor = 'myVendor'
}

jar {
    manifest {
        attributes 'Main-Class': 'presentation.MainApp'
    }
    from {
        configurations.compile.collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }
}

sourceSets {
    main {
        java {
            srcDirs 'src/main/java'
        }
        resources {
            srcDirs 'src/main/resources'
        }
    }
}

MainApp.java

package presentation;

import javafx.application.Application;
import javafx.stage.Stage;
import presentation.navigator.MenuNavigator;

public class MainApp extends Application {

    public static Stage primaryStage;

    private MenuNavigator navigator;

    public MainApp() {
        navigator = new MenuNavigator();
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        MainApp.primaryStage = primaryStage;
        navigator.launchMenuStage(primaryStage);
    }

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

MenuNavigator.java

    import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import presentation.utils.ResourceBundleUtf8Control;
import presentation.utils.config.Config;
import java.io.File;
import java.net.URL;
import java.io.IOException;
import java.util.Locale;
import java.util.ResourceBundle;

public class MenuNavigator {

    private void launchNewScene(Stage stage, String $fxmlName) {
        try {
            URL location = getClass().getResource("/fxml/" + $fxmlName);
            ResourceBundle resources = ResourceBundle.getBundle("properties.string", Locale.getDefault(), new ResourceBundleUtf8Control());
            Parent root = FXMLLoader.load(location, resources);
            Scene scene = new Scene(root, Config.loadDimen("dimension.app_screen_size.width"), Config.loadDimen("dimension.app_screen_size.height"));
            stage.setScene(scene);
            stage.setTitle(Config.loadString("string.title"));
            stage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void launchMenuStage(Stage stage) {
        launchNewScene(stage, "menu.fxml");
    }
}

MenuContoroller.java

package presentation.controller;

import javafx.event.ActionEvent;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import presentation.MainApp;
import presentation.utils.FileUtil;
import presentation.utils.ImageUtil;
import java.net.URL;
import java.util.ResourceBundle;

public class MenuController extends BaseController implements Initializable {

    public Button userRegistrationButton;

    @Override
    public void initialize(URL arg0, ResourceBundle arg1) {
        this.loadRegistrationButtonImage();
    }

    private void loadRegistrationButtonImage() {
        ImageView buttonImage = new ImageView();
        ImageUtil.loadSvgImage(buttonImage, "/images/main_menu/icon.svg", 71, 94);
        userRegistrationButton.setGraphic(buttonImage);
        userRegistrationButton.setGraphicTextGap(37.5);
    }

}

ImageUtile.java

package presentation.utils;

import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.image.PNGTranscoder;
import java.awt.image.BufferedImage;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.net.URL;

public class ImageUtil {
    public static void loadSvgImage(ImageView target, String resourcePath, int withd, int height) {
        SvgTranscoder imageTranscoder = new SvgTranscoder();

        imageTranscoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, (float) withd);
        imageTranscoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, (float) height);

        try {
            URL imagePath = ImageUtil.class.getResource(resourcePath);
            TranscoderInput input = new TranscoderInput(new FileReader(imagePath.getFile()));
            imageTranscoder.transcode(input, null);
        } catch (FileNotFoundException | TranscoderException e) {
            e.printStackTrace();
        }

        BufferedImage bimage =  imageTranscoder.getImage();
        WritableImage wimage = SwingFXUtils.toFXImage(bimage, null);
        target.setImage(wimage);
    }
}

SvgTranscoder.java

package presentation.utils;

import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.ImageTranscoder;

import java.awt.image.BufferedImage;

public class SvgTranscoder extends ImageTranscoder {

    private BufferedImage image = null;

    @Override
    public BufferedImage createImage(int w, int h) {
        image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        return image;
    }

    @Override
    public void writeImage(BufferedImage img, TranscoderOutput out) {
    }

    public BufferedImage getImage() {
        return image;
    }
}

这里也有相同的代码片段。 https://snippets.cacher.io/snippet/3dab5b901e6aa5e861e3

我解压了 jar 文件并检查了 fxml 目录和文件是否包含在内。 这是屏幕截图。 Unpacked jar files

如果有人就此问题向我提出建议,我将不胜感激。

问题是这样的:

new FileReader(imagePath.getFile())

尽管名称如此,URL class 的 getFile() 方法不是 return 有效的文件名,也不是将 URL 转换为文件。 (该方法是在二十多年前的 Java 1.0 中引入的,当时大多数 URL 实际上确实表示同一台计算机或不同计算机上的物理文件。)

即使是这样,.jar 文件也是一个单一的归档文件——其中的条目不是文件本身,只是代表压缩数据的字节子序列。

必须将 .jar 条目中的资源引用为资源 URL 或其等效流。您不得尝试将其转换为文件。

幸运的是,您不需要文件。您可以直接将 URL 作为字符串传递:

URL imagePath = ImageUtil.class.getResource(resourcePath);
TranscoderInput input = new TranscoderInput(imagePath.toString());