jlink-ing后如何获取资源路径?

问题描述 投票:0回答:2

我正在尝试使用 Maven 和 Visual Studio Code 使我的 JavaFx 应用程序可执行。

在这个主题上花了一些时间后,我发现了一些提到 jlink 的帖子。

我是打包 Java/JavaFX 应用程序的新手,所以我尝试了一下。

目前,我至少可以执行包的启动器。

但是启动应用程序后,立即抛出 NullPointerException: 无法调用“Object.toString()”,因为“java.lang.Class.getResource(String)”的返回值为 null。

为了设置视图组件的样式,我创建了一些 .css 文件并将它们放入 /style 目录中。根据示例 JavaFx 应用程序,我将此目录放置在 Maven 创建的 /resources 目录中。以类似的方式,我继续处理我的声音和图像文件。

在这里您可以看到我的目录结构的摘录。

|
|--src/main
|  |
|  |-- java
|  |   | ...
|  |
|  |-- resources
|      |
|      |-- img
|      |   | ...    
|      |
|      |-- style
|      |   | ...
|      |
|      |-- sound
|          | ...
|
|-- target
    |
    |-- classes
    |   | ...
    |   |
    |   |-- img
    |   |   | ...
    |   |
    |   |-- style
    |   |   | ...
    |   |
    |   |-- sound
    |   |   | ...
    |
    |-- ...
    |
    |-- app
        |
        |-- bin
        |-- ...

现在我正在尝试从我的应用程序中访问我的资源。

这是我的第一个方法。从 VSCode 运行时效果很好。

    public static final String PATH_TO_STYLESHEET = App.class.getResource("/style").toString();
    public static final String PATH_TO_IMG = App.class.getResource("/img").toString();
    public static final String PATH_TO_SOUNDS = App.class.getResource("/sounds").toString();

但是运行 jlink 后,我的应用程序崩溃了,显示前面提到的 NullPointerException。

这是我的 pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.openjfx</groupId>
    <artifactId>App</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>19</maven.compiler.release>
        <javafx.version>19</javafx.version>
        <javafx.maven.plugin.version>0.0.8</javafx.maven.plugin.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-media</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>${javafx.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <release>${maven.compiler.release}</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>${javafx.maven.plugin.version}</version>
                <configuration>
                    <release>${maven.compiler.release}</release>
                    <jlinkImageName>App</jlinkImageName>
                    <launcher>launcher</launcher>
                    <mainClass>com.test.App</mainClass>
                </configuration>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
    </build>
    
</project>

这是我一直用于创建包的命令。

mvn javafx:jlink -f pom.xml

有谁知道运行 jlink 后如何获取样式表、图像和声音的路径?这条路绝对够用了。我不需要文件本身。

是否可以选择将资源复制到特定位置?

java maven visual-studio-code javafx jlink
2个回答
3
投票

问题

您有如下代码:

public static final String PATH_TO_IMG = App.class.getResource("/img").toString();

这是试图获取资源

"/img"
。但根据你的问题,这本身并不是一个资源,而是一个目录(即一个包)。问题似乎是当 Class#getResource(String) 参数表示目录时
String
的行为不一致。当您的代码
not
在JRT图像中时,对#getResource(String)的调用将返回
URL
;当您的代码
is
打包在JRT映像中时,相同的调用将返回null,尽管该目录存在。
我不知道这种行为是一个错误还是只是未定义。一件有趣的事情是 

ModuleReader#find(String)

 显然能够
查找目录: 查找资源,返回模块中资源的 URI。

如果模块读取器可以确定该名称定位目录,则生成的 URI 将以斜杠结尾 (

'/'

)。

至少对我来说,这表明您尝试做的事情应该是可能的。但当模块打包在 JRT 映像中时,即使该方法也会失败(通过返回空的 

Optional
)。请注意,如果您查询

ModuleReader#list()
方法,当模块
not
位于 JRT 映像中时,它将包含目录,但当模块
is 位于 JRT 映像中时,不会包含相同的目录。 示例

我在这个答案的末尾放了一个最小的例子来演示这个问题。

解决方案


我假设您正在使用这些常量(例如,

PATH_TO_IMG

)来执行以下操作:

Image image = new Image(PATH_TO_IMG + "foo.png");

这避免了到处打电话给
SomeClass.class.getResource("...").toString()
。如果这是你的目标,那么我至少可以想到一个解决方案。更改常量以简单地引用

资源根

。例如:
public static final String IMG_ROOT = "/img";

然后创建一个实用方法来解析资源:
public static String getImagePath(String name) {
    var resource = IMG_ROOT + "/" + name;
    var url = App.class.getResource(resource);
    if (url == null) {
        throw new RuntimeException("could not find resource: " + resource);
    }
    return url.toString();
}

然后您可以像这样使用该实用方法:
Image image = new Image(getImagePath("foo.png"));

可能的替代方案
另一种选择可能是使用 JRT 

FileSystem

实现。类似于以下内容:

FileSystem jrtFs = FileSystems.getFileSystem(URI.create("jrt:/"));
Path path = jrtFs.getPath("modules", "<module-name>", "img");
// Note: Doesn't seem to include the trailing '/'
String pathToImg = path.toUri().toString();

尽管您必须检测您的代码是否在 JRT 映像中。

最小示例


鉴于这与 JavaFX 无关,我创建了一个最小的示例来演示此问题。

Maven 3.8.6

    OpenJDK 19.0.1 2022-10-18
  • 在 Windows 11 上测试
  • 源代码
项目结构:

| pom.xml | \---src \---main +---java | | module-info.java | | | \---sample | Main.java | \---resources \---data file.txt

pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    <groupId>sample</groupId>
    <artifactId>app</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>19</maven.compiler.release>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
            </plugin>
            
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.0</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jlink-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <launcher>app=app/sample.Main</launcher>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

模块信息.java:
module app {}

Main.java:
package sample;

public class Main {

    public static void main(String[] args) {
        var modRef = Main.class.getModule()
            .getLayer()
            .configuration()
            .findModule(Main.class.getModule().getName())
            .orElseThrow()
            .reference();
        System.out.printf("Module Location = %s%n%n", modRef.location().orElseThrow());

        var dataUrl = Main.class.getResource("/data");
        var fileUrl = Main.class.getResource("/data/file.txt");
        System.out.printf("Data URL = %s%nFile URL = %s%n%n", dataUrl, fileUrl);
    }
}

建筑
我运行了这两个命令来构建项目:

mvn compile jar:jar
  1. mvn jlink:jlink
  2. 无论出于何种原因,执行 
  3. mvn compile jar:jar jlink:jlink
都会导致

jlink

 任务失败。
输出

这是不同包装的不同输出:

模块分解:

...> java -p target\classes -m app/sample.Main Module Location = file:///C:/Users/***/Desktop/jlink-tests/target/classes/ Data URL = file:/C:/Users/***/Desktop/jlink-tests/target/classes/data/ File URL = file:/C:/Users/***/Desktop/jlink-tests/target/classes/data/file.txt

模块化 JAR:
...> java -p target\app-1.0-SNAPSHOT.jar -m app/sample.Main
Module Location = file:///C:/Users/***/Desktop/jlink-tests/target/app-1.0-SNAPSHOT.jar

Data URL = jar:file:///C:/Users/***/Desktop/jlink-tests/target/app-1.0-SNAPSHOT.jar!/data/
File URL = jar:file:///C:/Users/***/Desktop/jlink-tests/target/app-1.0-SNAPSHOT.jar!/data/file.txt

JRT 图片:
...> .\target\maven-jlink\default\bin\app
Module Location = jrt:/app

Data URL = null
File URL = jrt:/app/data/file.txt

结果
如您所见,对 

getResource("/data/file.txt")

的调用每次都有效,但对

getResource("/data")

 的调用对于 JRT 打包版本不起作用。

您需要将 JRT 注册为有效的 url 模式,那么无论如何它都可以全局使用

0
投票
简单-

public class JrtUrlHandler extends URLStreamHandlerProvider { @Override public URLStreamHandler createURLStreamHandler(String protocol) { if ("jrt".equals(protocol)) { return new URLStreamHandler() { @Override protected URLConnection openConnection(URL u) throws IOException { return new JrtUrlConnection(u); } }; } return null; } }

然后连接

public class JrtUrlConnection
        extends URLConnection
{
    private Path path;
    
    /**
     * Cleans up incorrectly found jar:jrt urls and returns a viable URI
     * @param url
     */
    protected JrtUrlConnection(URL url)
    {
        super(url);
        URI uri = URI.create(url.toString()
                                .replace("jar:jrt:/", "jrt:/")
                                .replace("!", ""));
        path = Path.of(uri);
    }

    @Override
    public void connect() throws IOException
    {
    }

    @Override
    public int getContentLength()
    {
        return (int) path.toFile()
                         .length();
    }

    @Override
    public long getContentLengthLong()
    {
        return path.toFile()
                   .length();
    }

    @Override
    public long getDate()
    {
        return path.toFile()
                   .lastModified();
    }

    @Override
    public long getLastModified()
    {
        return path.toFile()
                   .lastModified();
    }

    @Override
    public InputStream getInputStream() throws IOException
    {
        return Files.newInputStream(path);
    }

    @Override
    public OutputStream getOutputStream() throws IOException
    {
        return Files.newOutputStream(path);
    }
}

在模块信息中为 java.net.spi.URLStreamHandlerProvider 注册服务加载器
provides java.net.spi.URLStreamHandlerProvider with JrtUrlHandler;

对于测试类支持,还要在 META-INF/services 中创建服务加载器文件
所有标准操作都可以干净地工作,class.getReosurceStream 等,

适用于 JDK 9 到 23,尚未损坏

© www.soinside.com 2019 - 2024. All rights reserved.