Наше приложение JavaFX создано с помощью mvn clean javafx:jlink
для создания отдельного пакета для распространения. Теперь мне нужно включить внешние ресурсы (под этим я подразумеваю файлы конфигурации/контента в JSON, которые не упакованы в приложение, а находятся снаружи в свободно доступной структуре папок) в этот пакет, предпочтительно в процессе сборки с помощью maven.
Итак, я хотел бы добиться следующего:
Скопируйте MyProject/res/*
в MyProject/target/MyProject/res
Многие решения, которые я нашел, используют плагин ресурсов maven, и я безрезультатно пробовал следующее:
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>copy-external-resources</id>
<phase>generate-sources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/res</outputDirectory>
<resources>
<resource>
<directory>res</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
Я знаю, что сам путь (/target/res
) не обязательно правильный, так как я хочу, чтобы он был в папке MyProject, но в любом случае папка вообще не копируется. Что я здесь делаю неправильно?
Обратите внимание, что я не слишком хорошо знаком с Maven, его фазами и разными этапами.
Вот как это должно выглядеть:
Красный путь — это то, что должно быть скопировано в целевую папку после сборки.
Под внешними я подразумеваю файлы, которые не упакованы в само приложение, а находятся в свободном доступе как обычные файлы в структуре папок. Поэтому я хочу иметь возможность редактировать файлы и/или добавлять/удалять файлы из этой структуры папок, что было бы невозможно, если бы эти ресурсы находились в комплекте с приложением. Возможно, ресурс в данном случае неправильный термин. Рассматриваемые файлы представляют собой файлы конфигурации или содержимого в формате JSON.
Может я что-то упускаю, но мне это непонятно. Вы хотите, чтобы файлы копировались в сборку, а не как часть приложения? Итак, когда приложение собрано и развернуто, скажем, на другом компьютере, что должно произойти с этими файлами? Будут ли они просто отсутствовать (поскольку они не являются частью самого приложения)? Я не думаю, что это имеет смысл, но я мог просто не понять, что вы имеете в виду.
Предполагается, что приложение частично управляется контентом из внешних JSON-файлов. Поэтому некоторый контент должен быть загружен в приложение через те файлы, которые должны быть доступны пользователю. Представьте себе конфиг или ini-файлы, которые поставляются со многими приложениями. Они не упакованы в сам исполняемый файл, а «снаружи» в «папке программы». Я обновил свой первоначальный пост изображением, надеюсь, теперь это будет иметь больше смысла.
Типичная стратегия для этого — хранить конфигурацию в «известном месте», например, в каком-то конкретном месте в домашнем каталоге пользователя. Включите конфигурацию по умолчанию в качестве обычного ресурса в пакет приложения (т.е. в файл jar). При запуске проверьте, существует ли файл конфигурации в ожидаемом месте, и если нет, скопируйте конфигурацию по умолчанию из приложения в это место. Это решает проблему развертывания, а также обработку «случайного удаления» файла конфигурации после развертывания и т. д.
Хорошая точка зрения! Я сделаю это, спасибо.
Чтобы решить как проблему развертывания «внешних» файлов (например, конфигураций), так и случайного удаления таких файлов после развертывания, рекомендуется следующая стратегия (взято из комментария James_D):
Пример метода может выглядеть так:
public static String loadData(String file) throws IOException {
Path filePath = Path.of(file);
if (!Files.exists(filePath)) {
InputStream is = App.class.getResourceAsStream("/" + filePath);
Files.copy(is, file);
is.close();
}
return Files.readString(filePath);
}
Как предлагается в комментариях, одной из стратегий для этого является использование «известного местоположения» (обычно где-то в иерархии в домашнем каталоге пользователя) для файла. Сохраните версию файла по умолчанию в качестве ресурса в комплекте приложения. При запуске проверьте, существует ли файл в ожидаемом месте, и если нет, скопируйте содержимое из ресурса.
Вот полный пример. Обратите внимание, что это создаст папку (.configApp
) в вашем домашнем каталоге и файл (config.properties
) внутри этой папки. Для удобства нажатие «ОК» в диалоговом окне при выходе удалит эти артефакты. Вы, вероятно, не хотите этого в рабочей среде, поэтому нажмите «Отмена», чтобы увидеть, как это работает, и запустите его снова и нажмите «ОК», чтобы сохранить вашу файловую систему в чистоте.
package org.jamesd.examples.config;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
public class ConfigExample extends Application {
private static final Path CONFIG_LOCATION
= Paths.get(System.getProperty("user.home"), ".configApp", "config.properties");
private Properties readConfig() throws IOException {
if (!Files.exists(CONFIG_LOCATION)) {
Files.createDirectories(CONFIG_LOCATION.getParent());
Files.copy(getClass().getResourceAsStream("config.properties"), CONFIG_LOCATION);
}
Properties config = new Properties();
config.load(Files.newBufferedReader(CONFIG_LOCATION));
return config ;
}
@Override
public void start(Stage stage) throws IOException {
Properties config = readConfig();
Label greeting = new Label(config.getProperty("greeting"));
Button exit = new Button("Exit");
exit.setOnAction(e -> Platform.exit());
VBox root = new VBox(10, greeting, exit);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(20));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
@Override
// Provide option for cleanup. Probably don't want this for production.
public void stop() {
Alert confirmation = new Alert(Alert.AlertType.CONFIRMATION, "Delete configuration after exit?");
confirmation.showAndWait().ifPresent(response -> {
if (response == ButtonType.OK) {
try {
Files.delete(CONFIG_LOCATION);
Files.delete(CONFIG_LOCATION.getParent());
} catch (IOException exc) {
exc.printStackTrace();
}
}
});
}
public static void main(String[] args) {
launch();
}
}
с config.properties
под src/main/resources
и в том же пакете, что и класс приложения:
greeting=Hello
Если вам нужно улучшить это решение, чтобы получить список файлов для копирования из ресурсов, вы увидите: Получить список файлов из папки ресурсов пути к классам? или, если вы хотите разрешить файлы, выбранные пользователем, то см.: Как сохранить файл, загруженный FileChooser, в каталог вашего проекта?
Вы можете использовать сборку для создания zip вывода jlink, а также дополнительных «свободных файлов».
Плагин сборки для Maven позволяет разработчикам объединять выходные данные проекта в единый распространяемый архив, который также содержит зависимости, модули, документацию сайта и другие файлы.
Использование jpackage
(и, возможно, JPackageScriptFX ) будет альтернативой этому подходу, если вам также нужны установщик и деинсталлятор для конкретной платформы, или решение Джеймса также проще, если вы соответствуете своим требованиям. Этот альтернативный подход к использованию jlink+assembly включает в себя больше функций и представлен на случай, если он будет вам полезен. Если дополнительная сложность и функциональность не требуются, используйте решение Джеймса.
Вот неполный пример.
Пример делает немного больше, чем вы просите, просто отбросьте биты, которые вам не нужны.
${project.basedir}/src/main/resources/application.properties
.
application-<profile>.properties
, также используются, если они предоставлены.<deployment directory>/conf/application.properties
, и при необходимости их можно будет отредактировать и изменить там.
application-<profile>.properties
, также используются, если они предоставлены.Применение
Замените myapp
на имя вашего приложения и имя целевого каталога сборки, а com.example
на ваш пакет, сохраняя регистр там, где это необходимо.
Запустите mvn:package
, чтобы сгенерировать сборку.
Выход сборки заканчивается в:
${project.basedir}/target/myapp-<version>.zip
Запустите mvn:install
, чтобы опубликовать сборку в репозитории maven. Сборка окажется в том же месте репозитория maven, что и другие выходные артефакты сборки (например, файл pom.xml и jar), и будет классифицирована с использованием типа сборки (zip). Например, следующие координаты maven (groupId:artifactId:classifier:version):
com.example:myapp:zip:1.0-SNAPSHOT
Чтобы пользователь мог развернуть ваше приложение, он
<deployment directory>/conf/application.properties
.<deployment directory>/bin/myapp
.Если они хотят выбрать определенный профиль конфигурации при запуске, они могут указать аргумент --profile <profile-name>
в сценарии запуска.
${project.basedir}/src/main/assembly/zip.xml
Если у вас нет немодульных библиотек, вы можете опустить этот раздел, в противном случае замените файлы в этом разделе вашими немодульными (автоматически модульными) библиотеками.
<assembly xmlns = "http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>zip</id>
<includeBaseDirectory>true</includeBaseDirectory>
<formats>
<format>zip</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.basedir}/image-overlay</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<directory>${project.basedir}/target/myapp</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
<fileSet>
<directory>${project.basedir}/src/main/resources</directory>
<includes>
<include>*.properties</include>
</includes>
<outputDirectory>/conf</outputDirectory>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>non-modular-libs</outputDirectory>
<includes>
<include>org.postgresql:postgresql:jar:*</include>
</includes>
</dependencySet>
</dependencySets>
</assembly>
конфиг плагина в pom.xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>18</source>
<target>18</target>
<compilerArgs>
<arg>--enable-preview</arg>
<arg>--add-exports</arg>
<arg>javafx.web/com.sun.javafx.webkit=com.example</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<executions>
<execution>
<id>default-cli</id>
<phase>package</phase>
<goals>
<goal>jlink</goal>
</goals>
<configuration>
<mainClass>
com.example/com.example.MyApp
</mainClass>
<launcher>myapp</launcher>
<jlinkImageName>myapp</jlinkImageName>
<noManPages>true</noManPages>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
<jlinkExecutable>${jlinkExecutable}</jlinkExecutable>
<!--<jlinkVerbose>true</jlinkVerbose>-->
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/main/assembly/zip.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
Индивидуальный лаунчер image-overlay/bin/myapp
Перезаписывает пусковую установку по умолчанию, созданную jlink.
Поместите другие «свободные файлы», которые вы хотите включить в свой дистрибутив, в каталог image-overlay или в соответствующие подкаталоги.
#!/bin/sh
# disable glob (* wildcard) expansion
set -f
DIR=`dirname $0`
CONFIG_DIR=$DIR/../conf
JLINK_VM_OPTIONS = "--enable-preview -cp $DIR/../non-modular-libs/* --add-exports javafx.web/com.sun.javafx.webkit=com.example"
$DIR/java $JLINK_VM_OPTIONS -m com.example/com.example.MyApp --configdir "$CONFIG_DIR" "$@"
Config.java
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
public class Config {
private static final Logger log = LoggerFactory.getLogger(Config.class);
private String configPropertiesResource =
"/application"
+ (configProfile != null
? "-" + configProfile
: "")
+ ".properties";
private static final Properties properties = new Properties();
private static Config instance;
private static String configDir;
private static String configProfile;
public static String getConfigProfile() {
return configProfile;
}
public static Config getInstance() {
if (instance == null) {
instance = new Config();
}
return instance;
}
private Config() {
try {
// use the config override directory if defined, otherwise use the module resource path.
InputStream configInputStream;
if (configDir != null) {
Path configFilePath = Paths.get(configDir, configPropertiesResource);
if (!Files.exists(configFilePath)) {
log.error("Config file does not exist: {}", configFilePath.toAbsolutePath());
System.exit(-1);
}
configInputStream = Files.newInputStream(configFilePath);
log.info("Loading config properties from config file: {}", configFilePath.toAbsolutePath());
} else {
configInputStream = Config.class.getResourceAsStream(configPropertiesResource);
log.info("Loading config properties from config resource: {}", Config.class.getResource(configPropertiesResource));
}
properties.load(configInputStream);
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
properties.list(printWriter);
String sortedProperties =
stringWriter.toString()
.lines()
.sorted()
.collect(
Collectors.joining(
"\n"
)
);
log.info("Config properties: {}", sortedProperties);
} catch (IOException e) {
log.error("Unable to read configuration", e);
Platform.exit();
}
}
public static void initConfigDir(String newConfigDir) {
configDir = newConfigDir;
}
public static void initConfigProfile(String profileName) {
configProfile = profileName;
}
public boolean isTheSkyBlue() {
return Boolean.parseBoolean(
properties.getProperty(
"sky.blue",
"true"
)
);
}
}
Затем вы можете получить доступ к свойству конфигурации из любого места вашего приложения:
boolean skyBlue = Config.instance().isTheSkyBlue();
/src/main/resources/application.properties
sky.blue=true
Чтобы создать настройки по умолчанию для разных сред, определите отдельные файлы свойств, такие как application-dev.properties
, а затем при выполнении приложения передайте имя профиля, например. --profile dev
, чтобы выбрать настройку конфигурации для этой среды.
Имена по умолчанию для файла конфигурации application-properties
и выбора профиля соответствуют тем же соглашениям об именах, которые используются в конфигурации SpringBoot, поэтому, если вы хотите преобразовать этот метод для использования SpringBoot в будущем, преобразование будет проще. Это также означает, что конфигурация вашего приложения должна быть проста для понимания для любого, кто ранее конфигурировал приложение SpringBoot.
Метод main
в вашем приложении JavaFX. Поместите это в свой класс с расширением класса JavaFX Application
.
Пользовательский класс конфигурации загрузит конфигурацию из файлов свойств для соответствующего профиля среды в инициализированном каталоге конфигурации.
В качестве альтернативы вы можете использовать SpringBoot для загрузки конфигурации, поскольку она имеет много поддержки для таких вещей, но это выходит за рамки этого ответа, и SpringBoot (в настоящее время) трудно адаптировать и использовать с системой модулей платформы Java, используемой приложениями JavaFX. .
public static void main(String[] args) {
processArguments(args);
launch();
}
private static void processArguments(String[] args) {
for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case "--configdir" -> {
ensureValueArgAfter(args, i);
Config.initConfigDir(args[++i]);
log.info("Initialized config directory as: {}", args[i]);
}
case "--profile" -> {
ensureValueArgAfter(args, i);
Config.initConfigProfile(args[++i]);
log.info("Initialized config profile as: {}", args[i]);
}
default -> incorrectUsage();
}
}
}
private static void ensureValueArgAfter(String[] args, int idx) {
if (idx == args.length - 1) {
incorrectUsage();
}
}
private static void incorrectUsage() {
System.err.println(
"""
Usage: java com.example.MyApp --configdir <dirname> --profile <local|dev|qa|prod>
Adjust the command line for the "java" command based on your usage, see the "java" command man page for more info.
"""
);
System.exit(-1);
}
Пример распакованного выходного каталога
В этом примере не отображается большинство путей и файлов, добавленных для JRE процессом jlink
, так как большинство этих файлов не имеют значения для демонстрационных целей. Однако дополнительные файлы, сгенерированные jlink, будут включены в результирующий zip-файл, поэтому созданный zip-файл будет содержать больше файлов, чем показано здесь.
myapp-1.0-SNAPSHOT
├── bin (overlaid app launcher script)
│ └── myapp
├── conf (overlaid app config files)
│ ├── application-dev.properties
│ ├── application-local.properties
│ ├── application-qa.properties
│ ├── application.properties
├── legal (jre legal documents)
├── lib (jre libraries and jlink created modular image)
├── non-modular-libs (overlaid non modular libraries)
│ └── postgresql-42.5.0.jar
└── release (defines jre and base modules in the jlink image)
Что вы подразумеваете под «внешними ресурсами»? Для меня это что-то вроде оксюморона. Ресурс является частью пакета приложения, поэтому он не является «внешним». По умолчанию файлы в
src/main/resources
копируются в сборку. Чтобы настроить другой каталог ресурсов, см. maven.apache.org/plugins/maven-resources-plugin/examples/…. Обратите внимание, что это часть сборки (поскольку вы собираете приложение), а не часть выполнения.