繁体   English   中英

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

[英]Jlink - including a directory that contains a custom python script in a JavaFX application

我需要包含一个目录,其中包含 python 脚本和需要由脚本基于 JavaFX 应用程序中已解析的 arguments 执行的二进制文件。

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

使用 maven 运行配置构建时,应用程序可以正常工作,但为了创建运行时映像,我偶然发现在“目标”的“bin”文件夹中运行生成的 launcher.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>

*注意:为 jfoenix package 兼容性添加了 javafx-maven-plugin 的附加选项

还有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;
}

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

问题

src/main/resources目录只存在于您的项目源代码中。 它在构建 output 中不存在,并且在您的部署位置中绝对不存在。 换句话说,使用:

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

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

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

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

执行脚本

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


潜在的解决方案

我可以想到至少三种可以解决上述问题的方法。 因为我只有 Windows,所以我不能 promise 下面的例子可以在其他平台上工作,或者很容易翻译成其他平台。

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

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

模块信息.java:

module com.example {}

主要.py:

print("Hello from Python!")

1. 在运行时提取脚本

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

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

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

项目结构:

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

主要.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!

2.将脚本放在“应用程序目录”中

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

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

应用程序目录是本文档中显示的app目录。

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

鉴于我编写Main.java的方式,此演示必须在使用jpackage打包后执行。 我怀疑有一种更强大的方法来实现这个解决方案。

项目结构:

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

主要.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:

(这只是org.panteleyev:jpackage-maven-plugin插件的<configuration> ,因为 POM 中的所有其他内容与第一个解决方案相比没有变化)

<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!

3.添加脚本作为附加的“应用程序内容”

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM