提问者:小点点

Jlink-包括JavaFX应用程序中包含自定义python脚本的目录


我需要包含一个包含python脚本和二进制文件的目录,这些脚本需要根据JavaFX应用程序中解析的参数由脚本执行。

该项目是模块化的,使用Maven构建(尽管模块化部分不是那么重要的信息)。

当使用maven运行配置构建时,应用程序正常工作,但为了创建运行时映像,我偶然发现了当我在“目标”的“bin”文件夹中运行生成的启动器. bat脚本时没有执行脚本的问题。

出于生成运行时的目的,我将脚本目录放在项目“资源”文件夹中。脚本使用Java运行时从Java代码执行。

假设代码如下所示:

pyPath = Paths.get("src/main/resources/script/main.py").toAbsolutePath().toString();
command = "python"+pyPath+args;
runtime = Runtime.getRuntime();
process = runtime.exec(command);

pom. xml文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>gui</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>gui</name>
  <packaging>jar</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <junit.version>5.8.2</junit.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.openjfx</groupId>
      <artifactId>javafx-controls</artifactId>
      <version>18</version>
    </dependency>
    <dependency>
      <groupId>org.openjfx</groupId>
      <artifactId>javafx-fxml</artifactId>
      <version>18</version>
    </dependency>
    <dependency>
      <groupId>org.controlsfx</groupId>
      <artifactId>controlsfx</artifactId>
      <version>11.1.1</version>
    </dependency>
    <dependency>
      <groupId>com.dlsc.formsfx</groupId>
      <artifactId>formsfx-core</artifactId>
      <version>11.3.2</version>
      <exclusions>
        <exclusion>
          <groupId>org.openjfx</groupId>
          <artifactId>*</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.kordamp.ikonli</groupId>
      <artifactId>ikonli-javafx</artifactId>
      <version>12.3.0</version>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.jfoenix</groupId>
      <artifactId>jfoenix</artifactId>
      <version>9.0.10</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.panteleyev</groupId>
        <artifactId>jpackage-maven-plugin</artifactId>
        <version>1.5.2</version>
        <configuration>
          <name>gui</name>
          <appVersion>1.0.0</appVersion>
          <vendor>1234</vendor>
          <destination>target/dist</destination>
          <module>com.example.gui/com.example.gui.Application</module>
          <runtimeImage>target/example-gui</runtimeImage>
          <winDirChooser>true</winDirChooser>
          <winPerUserInstall>true</winPerUserInstall>
          <winShortcut>true</winShortcut>
          <winMenuGroup>Applications</winMenuGroup>
          <icon>${project.basedir}/main/resources/img/icon.ico</icon>
          <javaOptions>
            <option>-Dfile.encoding=UTF-8</option>
          </javaOptions>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.10.1</version>
        <configuration>
          <source>18</source>
          <target>18</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.openjfx</groupId>
        <artifactId>javafx-maven-plugin</artifactId>
        <version>0.0.8</version>
        <executions>
          <execution>
            <id>default-cli</id>
            <configuration>
              <mainClass>com.example.gui/com.example.gui.Application</mainClass>
              <launcher>gui-launcher</launcher>
              <jlinkZipName>gui</jlinkZipName>
              <jlinkImageName>gui</jlinkImageName>
              <jlinkVerbose>true</jlinkVerbose>
              <noManPages>true</noManPages>
              <stripDebug>true</stripDebug>
              <noHeaderFiles>true</noHeaderFiles>
              <options>
                <option>--add-opens</option><option>javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED</option>
                <option>--add-opens</option><option>javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED</option>
                <option>--add-opens</option><option>javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED</option>
                <option>--add-opens</option><option>javafx.base/com.sun.javafx.binding=ALL-UNNAMED</option>
                <option>--add-opens</option><option>javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED</option>
                <option>--add-opens</option><option>javafx.base/com.sun.javafx.event=ALL-UNNAMED</option>
                <option>--add-exports</option><option>javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED</option>
                <option>--add-exports</option><option>javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED</option>
                <option>--add-exports</option><option>javafx.base/com.sun.javafx.binding=ALL-UNNAMED</option>
                <option>--add-exports</option><option>javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED</option>
                <option>--add-exports</option><option>javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED</option>
                <option>--add-exports</option><option>javafx.base/com.sun.javafx.event=ALL-UNNAMED</option>
              </options>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

*注意:为javafx-maven-plugin添加了其他选项以实现jfoison包的兼容性

也module-info.java

module com.example.gui {
    requires javafx.controls;
    requires javafx.fxml;

    requires org.controlsfx.controls;
    requires com.dlsc.formsfx;
    requires org.kordamp.ikonli.javafx;
    requires com.jfoenix;

    opens com.example.gui to javafx.fxml;
    exports com.example.gui;
}

现在的问题是如何将脚本包含在应用程序运行时映像中,在为应用程序调用生成的. bat时执行它,并最终使用jpack打包?


共1个答案

匿名用户

src/main/Resources目录仅存在于您的项目源中。它不存在于构建输出中,也绝对不存在于您的部署位置。换句话说,使用:

var pyPath = Paths.get("src/main/resources/script/main.py").toAbsolutePath().toString();

只有当您的工作目录是您的项目目录时才有效。它也在读取“错误”的main.py资源,因为“正确”的资源将在您的目标目录中。此外,资源不是文件。您必须使用资源查找API访问资源。例如:

var pyPath = getClass().getResource("/script/main.py").toString();

注意src/main/Resources不包含在资源名称中。

但是即使在你正确地访问了资源之后,你仍然有一个问题。你的脚本是一个资源,这意味着它在部署时会嵌入到JAR文件或自定义运行时映像中。我强烈怀疑Python会知道如何读取和执行这样的资源。这意味着你需要找到一种方法来使Python脚本成为主机上的常规文件。

我能想到至少三种方法可以解决上述问题。由于我只有视窗系统,我不能保证下面的例子能在其他平台上工作,或者很容易被翻译成其他平台。

我的示例不包括JavaFX,因为我认为没有必要演示如何包含在运行时执行的Python脚本。

以下是所有解决方案之间的一些共同方面。

module-info.java:

module com.example {}

main.py:

print("Hello from Python!")

一种方法是在运行时将Python脚本提取到主机上的已知位置。这可能是最通用的解决方案,因为它不太依赖于您部署应用程序的方式(jar、jlink、jpack等)。

此示例将脚本提取到临时文件中,但您可以使用其他位置,例如用户主目录下的应用程序特定目录。您也可以对其进行编码以仅在尚未提取时进行提取,或者每个应用程序实例仅提取一次。

我想这是我会使用的解决方案,至少一开始是这样。

项目结构:

|   pom.xml
|
\---src
    \---main
        +---java
        |   |   module-info.java
        |   |
        |   \---com
        |       \---example
        |           \---app
        |                   Main.java
        |
        \---resources
            \---scripts
                    main.py

Main.java:

package com.example.app;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class Main {

  public static void main(String[] args) throws Exception {
    Path target = Files.createTempDirectory("sample-1.0").resolve("main.py");
    try {
      // extract resource to temp file
      try (InputStream in = Main.class.getResourceAsStream("/scripts/main.py")) {
        Files.copy(in, target);
      }

      String executable = "python";
      String script = target.toString();

      System.out.printf("COMMAND: %s %s%n", executable, script); // log command
      new ProcessBuilder(executable, script).inheritIO().start().waitFor();
    } finally {
      // cleanup for demo
      Files.deleteIfExists(target);
      Files.deleteIfExists(target.getParent());
    }
  }
}

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>com.example</groupId>
  <artifactId>sample</artifactId>
  <version>1.0</version>

  <name>sample</name>
  <packaging>jar</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>18</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-jar-plugin</artifactId>
        <version>3.2.2</version>
        <configuration>
          <archive>
            <manifest>
              <mainClass>com.example.app.Main</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jlink-plugin</artifactId>
        <version>3.1.0</version>
        <executions>
          <execution>
            <id>default-cli</id>
            <phase>package</phase>
            <goals>
              <goal>jlink</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <classifier>win</classifier>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.panteleyev</groupId>
        <artifactId>jpackage-maven-plugin</artifactId>
        <version>1.5.2</version>
        <executions>
          <execution>
            <id>default-cli</id>
            <phase>package</phase>
            <goals>
              <goal>jpackage</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <type>APP_IMAGE</type>
          <runtimeImage>${project.build.directory}/maven-jlink/classifiers/win</runtimeImage>
          <module>com.example/com.example.app.Main</module>
          <destination>${project.build.directory}/image</destination>
          <winConsole>true</winConsole>
        </configuration>
      </plugin>

    </plugins>
  </build>

</project>

使用以下方式构建项目:

mvn package

然后执行构建的应用程序映像:

> .\target\image\sample\sample.exe

COMMAND: python C:\Users\***\AppData\Local\Temp\sample-1.015076039373849618085\main.py
Hello from Python!

免责声明:我不知道这样做是否是一种明智的甚至是受支持的方法。

该解决方案利用--input将脚本放置在应用程序映像的“应用程序目录”中。然后,您可以通过--java-option$APPDIR设置系统属性来获取对该目录的引用。注意,我试图让它与类路径一起工作,以便不需要$APPDIR系统属性,但我所尝试的一切都导致Class#getResource(String)返回null

应用程序目录是此留档中显示的app目录。

由于此解决方案将Python脚本与应用程序映像的其余部分一起放置,这意味着它被放置在安装位置,因此您可能更有可能遇到文件权限问题。

鉴于我编写Main.java的方式,这个演示只能在使用jpack打包后执行。我怀疑有更健壮的方法来实现这个解决方案。

项目结构:

|   pom.xml
|
+---lib
|   \---scripts
|           main.py
|
\---src
    \---main
        \---java
            |   module-info.java
            |
            \---com
                \---example
                    \---app
                            Main.java

Main.java:

package com.example.app;

import java.nio.file.Path;

public class Main {

  public static void main(String[] args) throws Exception {
    String executable = "python";
    String script = Path.of(System.getProperty("app.dir"), "scripts", "main.py").toString();

    System.out.printf("COMMAND: %s %s%n", executable, script); // log command
    new ProcessBuilder(executable, script).inheritIO().start().waitFor();
  }
}

pom. xml:

(这只是

<configuration>
  <type>APP_IMAGE</type>
  <runtimeImage>${project.build.directory}/maven-jlink/classifiers/win</runtimeImage>
  <input>lib</input>
  <javaOptions>
    <javaOption>-Dapp.dir=$APPDIR</javaOption>
  </javaOptions>
  <module>com.example/com.example.app.Main</module>
  <destination>${project.build.directory}/image</destination>
  <winConsole>true</winConsole>
</configuration>

使用以下方式构建项目:

mvn package

然后执行构建的应用程序映像:

> .\target\image\sample\sample.exe

COMMAND: python C:\Users\***\Desktop\sample\target\image\sample\app\scripts\main.py
Hello from Python!

免责声明:与第二种解决方案的免责声明相同。

这将在调用jpack时使用--app-content参数。不幸的是,我无法弄清楚如何使用Maven配置它,至少不能使用org. panteleyev:jpackage-maven-plugin插件。但本质上这个解决方案与上面的第二个解决方案相同,但删除了--input并添加了--app-content lib/脚本。并且对脚本Path在代码中的解析方式进行了轻微更改。

--app-content参数似乎将指定的任何目录/文件放在生成的应用程序映像的根目录中。我不确定获取此目录的方便方法,因为应用程序映像结构根据平台略有不同。据我所知,映像的根目录没有等效的$APPDIR