我需要包含一个包含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打包?
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
。