Скопируйте файлы внешних ресурсов после создания приложения JavaFX с помощью Maven

Наше приложение 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, его фазами и разными этапами.

Вот как это должно выглядеть:

Красный путь — это то, что должно быть скопировано в целевую папку после сборки.

Что вы подразумеваете под «внешними ресурсами»? Для меня это что-то вроде оксюморона. Ресурс является частью пакета приложения, поэтому он не является «внешним». По умолчанию файлы в src/main/resources копируются в сборку. Чтобы настроить другой каталог ресурсов, см. maven.apache.org/plugins/maven-resources-plugin/examples/…. Обратите внимание, что это часть сборки (поскольку вы собираете приложение), а не часть выполнения.

James_D 02.10.2022 17:44

Под внешними я подразумеваю файлы, которые не упакованы в само приложение, а находятся в свободном доступе как обычные файлы в структуре папок. Поэтому я хочу иметь возможность редактировать файлы и/или добавлять/удалять файлы из этой структуры папок, что было бы невозможно, если бы эти ресурсы находились в комплекте с приложением. Возможно, ресурс в данном случае неправильный термин. Рассматриваемые файлы представляют собой файлы конфигурации или содержимого в формате JSON.

GenerationLost 02.10.2022 17:58

Может я что-то упускаю, но мне это непонятно. Вы хотите, чтобы файлы копировались в сборку, а не как часть приложения? Итак, когда приложение собрано и развернуто, скажем, на другом компьютере, что должно произойти с этими файлами? Будут ли они просто отсутствовать (поскольку они не являются частью самого приложения)? Я не думаю, что это имеет смысл, но я мог просто не понять, что вы имеете в виду.

James_D 02.10.2022 19:00

Предполагается, что приложение частично управляется контентом из внешних JSON-файлов. Поэтому некоторый контент должен быть загружен в приложение через те файлы, которые должны быть доступны пользователю. Представьте себе конфиг или ini-файлы, которые поставляются со многими приложениями. Они не упакованы в сам исполняемый файл, а «снаружи» в «папке программы». Я обновил свой первоначальный пост изображением, надеюсь, теперь это будет иметь больше смысла.

GenerationLost 02.10.2022 19:19

Типичная стратегия для этого — хранить конфигурацию в «известном месте», например, в каком-то конкретном месте в домашнем каталоге пользователя. Включите конфигурацию по умолчанию в качестве обычного ресурса в пакет приложения (т.е. в файл jar). При запуске проверьте, существует ли файл конфигурации в ожидаемом месте, и если нет, скопируйте конфигурацию по умолчанию из приложения в это место. Это решает проблему развертывания, а также обработку «случайного удаления» файла конфигурации после развертывания и т. д.

James_D 02.10.2022 19:27

Хорошая точка зрения! Я сделаю это, спасибо.

GenerationLost 02.10.2022 19:30
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
6
201
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Чтобы решить как проблему развертывания «внешних» файлов (например, конфигураций), так и случайного удаления таких файлов после развертывания, рекомендуется следующая стратегия (взято из комментария 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

Вы можете использовать сборку для создания zip вывода jlink, а также дополнительных «свободных файлов».

Плагин сборки для Maven позволяет разработчикам объединять выходные данные проекта в единый распространяемый архив, который также содержит зависимости, модули, документацию сайта и другие файлы.

Использование jpackage (и, возможно, JPackageScriptFX ) будет альтернативой этому подходу, если вам также нужны установщик и деинсталлятор для конкретной платформы, или решение Джеймса также проще, если вы соответствуете своим требованиям. Этот альтернативный подход к использованию jlink+assembly включает в себя больше функций и представлен на случай, если он будет вам полезен. Если дополнительная сложность и функциональность не требуются, используйте решение Джеймса.

Вот неполный пример.

Пример делает немного больше, чем вы просите, просто отбросьте биты, которые вам не нужны.

  • Он предполагает модульное приложение (хотя при необходимости может использовать некоторые виды автоматических модулей, например, драйвер postgres).
  • Он использует jlink для связывания приложения с целевой архитектурой машины сборки.
  • Он использует сборку для создания zip-дистрибутива приложения.
    • Сборка включает в себя «свободные файлы», образ jlink, лаунчер, конфигурацию по умолчанию и немодульные библиотеки.
    • Образ jlink включает в себя среду выполнения Java, среду выполнения JavaFX, все зависимости модуля для вашего приложения и модуль вашего приложения.
    • «Свободные файлы» — это любые файлы, которые вы хотите видеть в файловой системе, такие как редактируемые файлы конфигурации, сценарии запуска, немодульные библиотеки для загрузки из пути к классам и т. д.
  • ZIP-файл предназначен только для одной операционной архитектуры (например, Mac, Linux или Windows, но не для всех). Используемая архитектура — это та, на которой построен проект.
  • При создании zip он копирует все файлы из каталога наложения изображений в zip, добавляя и заменяя, где это необходимо, файлы, сгенерированные процессом jlink.
  • Один из файлов наложения — это пользовательский файл запуска, который заменяет созданный jlink модуль запуска.
  • Немодульные библиотеки копируются в процессе сборки в каталог lib, и пользовательская программа запуска помещает их в путь к классам.
    • Это позволяет вам использовать jlink для связывания вашего модульного приложения, а также включать автоматические модули, которые jlink не обрабатывает, в путь к классам, поэтому приложение все равно будет работать. Однако он будет работать только для служебных библиотек, таких как драйвер postgres. Если у вас есть библиотека, требующая, чтобы вы вызывали API непосредственно из своего кода (например, HTTP-клиент apache, который в настоящее время является автоматическим модулем), вам нужно будет использовать другое решение для упаковки (например, jpackage), а не jlink.
  • Дополнительные экспорты используются на этапе компиляции и выполнения, чтобы нарушить модульность и разрешить доступ к частному API в JDK/JavaFX (при необходимости).
  • Профили используются для обеспечения возможности переключения настраиваемых файлов конфигурации на основе селектора среды.
  • Аргументы командной строки обрабатываются для обработки настройки конфигурации и настройки профиля.
  • Пользовательские переключатели JVM используются для включения функций предварительного просмотра Java.
  • Свойства конфигурации по умолчанию для приложения происходят из ${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

Чтобы пользователь мог развернуть ваше приложение, он

  1. Загрузите zip-файл, затем разархивируйте его в каталог развертывания.
  2. При необходимости измените файл <deployment directory>/conf/application.properties.
  3. Выполните скрипт запуска <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)

Другие вопросы по теме