Пошаговое руководство по созданию JavaFX-приложения с Maven, логированием и сборкой в exe-файл. Разбираем три способа: с установленной JRE, с встроенной JRE (jpackage + jlink).
Обычно в книгах по десктопной разработке, статьях и учебных курсах основное внимание уделяют быстрому созданию и запуску из среды разработки. Но вот созданию дистрибутивов приложения и/или исполняемых exe файлов, которые можно просто запустить парой кликов без танцев с бубном, внимание не уделяют, а зря.
Создаем проект.
Для начала создаем болванку, используя возможности IntelliJ IDEA. Сборщиком будет Maven. ядро проекта будет на JavaFx.

Добавляем зависимости по вкусу. Я их добавлять не буду. Нажимаем на «Create».

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

Ну вот и все. С вас пять тыщ. Шутка. Дальше самое интересное.
Настройка логгирования.
Чтобы знать, что там пошло не так, когда оно пойдет не так, нужно чтобы приложение умело писать куда нибудь свои логи. Сделаем простую базовую настройку для логов. Для этого сначала добавим зависимости на sl4j и logback.
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
</dependency>
Настроим вывод логов в файл. Создаем logback.xml в src/main/resources:
<configuration>
<property name="LOG_PATH" value="${log.dir:-${LOCALAPPDATA}/myJavaFxApp/logs}"/>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${LOG_PATH}/app.log</file>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="FILE" />
</root>
</configuration>
В десктопных приложениях есть область с данными приложения, куда ОС разрешает писать файлы. Например, для Windows это «C:\users\username\AppData\Local\appname». Вышеуказанная настройка как раз и создает каталоги для временных файлов программы в разрешенном месте.
Также для ручного контроля существования нужных каталогов, добавим метод initLogDir() в главном классе программы и вызовем его в методе start():
public class HelloApplication extends Application {
private static final Logger logger = LoggerFactory.getLogger(HelloApplication.class);
@Override
public void start(Stage stage) throws IOException {
initLogDir();
FXMLLoader fxmlLoader = new FXMLLoader(
HelloApplication.class.getResource("hello-view.fxml")
);
Scene scene = new Scene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
logger.info("Application started.");
}
public static void main(String[] args) {
launch();
}
private void initLogDir(){
String logDir = System.getenv("LOCALAPPDATA") + "/myJavaFxApp/logs";
new File(logDir).mkdirs();
System.setProperty("log.dir", logDir);
System.out.println("Logs will be written to: " + logDir);
}
}
Далее дополним файл module-info.java. Это пригодится в дальнейшем для сборки с помощью Jlink.
module ru.makushimo.javafxexe {
requires javafx.controls;
requires javafx.fxml;
requires org.slf4j;
requires ch.qos.logback.classic;
opens ru.makushimo.javafxexe to javafx.fxml;
exports ru.makushimo.javafxexe;
}
Проверка: запускаем приложение и смотрим в появившийся файл логов:

Сборка exe файла.
Сборка своего образа JDK.
Для получения исполняемого файла exe с нашей программой и JDK внутри, для начала нужно собрать свой образ среды исполнения (JDK). Этот образ будет включать базовые модули Java и нужные нам модули приложения и зависимостей. Далее команды выполняем в терминале.
Сборка проекта:
mvn clean package
Эта команда создаст jar и остальные нужные файлы в target.
Чтобы появилась папка target/libs, содержащая все зависимости проекта, можно выполнить команду:
mvn dependency:copy-dependencies
Либо добавить в POM.xml этот плагин:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
<includeScope>runtime</includeScope> <!-- Только нужные для запуска -->
<stripVersion>true</stripVersion> <!-- Убирает версии из имен JAR-файлов -->
</configuration>
</execution>
</executions>
</plugin>
Также, чтобы jar прриложения содержал манифест и главный запускаемый класс, не забыть добавить плагин:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifest>
<mainClass>ru.makushimo.javafxexe.HelloApplication</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
Теперь после сборки проекта target будет содержать все что нужно для Jlink.
Собираем среду исполнения:
"%JAVA_HOME%/bin/jlink"
--module-path "%JAVA_HOME%\jmods;target\classes;target\libs"
--add-modules ru.makushimo.javafxexe,java.naming
--launcher javafxexe=ru.makushimo.javafxexe/ru.makushimo.javafxexe.HelloApplication
--output custom-runtime
Если все настроено правильно, в корне проекта появится папка custom-runtime с файлами среды исполнения для нашей программы.
На что обратить внимание:
- В проекте и в JAVA_HOME должен быть указан путь к той же версии Java, на которой написан проект.
- В module-path указываем все основные источники модулей, в том числе папку со скомпилированными классами приложения, и с файлами зависимостей проекта.
- В add-module указываем только модуль нашего приложения, т.к. jlink, глядя на ддескриптор модуля (module-info.java) сам найдет и подключит нужные модули из указанных источников. Модуль «java.naming» добавили для того, чтобы внутри программы правильно запускался логгер Sl4J и правильно разрешались имена классов.
- В launcher указываем, какой класс запускать. При этом в /bin создаваемого образа будет создан исполняемый файл с именем, которое указали слева от знака «равно». Этот файл будет выполнять команду «java —module <то что указали справа от ‘равно’>».
- В output указываем каталог относительно корня проекта.
Проверить правильность сборки custom-runtime можно следующей командой в терминале:
custom-runtime\bin\java --list-modules
Список модулей должен содержать те, которые мы добавляли:
ch.qos.logback.classic@1.5.18
ch.qos.logback.core@1.5.18
java.base@17.0.14
java.datatransfer@17.0.14
java.desktop@17.0.14
java.logging@17.0.14
java.naming@17.0.14
java.prefs@17.0.14
java.scripting@17.0.14
java.security.sasl@17.0.14
java.xml@17.0.14
javafx.base@17.0.14
javafx.controls@17.0.14
javafx.fxml@17.0.14
javafx.graphics@17.0.14
jdk.unsupported@17.0.14
org.slf4j@2.0.17
ru.makushimo.javafxexe@1.0-SNAPSHOT
Проверка работоспособности jar приложения.
Перед тем, как собирать установщик, нужно вручную убедиться что используемый jar файл в целом рабочий.
Запускаем приложение с помощью нашего custom-runtime:
"custom-runtime\bin\javafxexe"
или
"custom-runtime\bin\java"
-m ru.makushimo.javafxexe/ru.makushimo.javafxexe.HelloApplication
Сборка установщика приложения (Jpackage).
Далее используя образ, созданный с помощью Jlink создадим exe файл установщика приложения.
"%JAVA_HOME%/bin/jpackage"
--type exe
--name myJavaFxApp
--app-version 1.0
--module ru.makushimo.javafxexe/ru.makushimo.javafxexe.HelloApplication
--runtime-image custom-runtime
--dest out/installer
--verbose
Значения параметров интуитивно понятны. Можно уточнить пару момемнтов:
- module указываем модуль приложения
- runtime-image — это тот самый каталог, созданный Jlink на предыдущем шаге.
- verbose добавлять, если нужно видеть логи сборки, если вдруг что-то идет не так.
- dest — путь к папке относительно корня проекта, в которой будет лежать готовый инсталятор.
Установка и проверка
В папке out в корне проекта находим и запускаем файл myJavaFxApp.exe. К слову сказать название для него нужно придумать покрасивше, потому что так будет называться папка с установленным приложением.

Запускаем и радуемся.

Автоматизируем сборку всего.
Чтобы не печатать каждый день все эти команды с параметрами, не хочется. Можно, конечно, написать bat-файл запускать его. А почему бы не использовать Maven?
Итак, что нужно автоматизировать:
- Очистка — хочу удалять все создаваемые артефакты, такие как target, custom-runtime и out.
- Сборка рантайма и затем инсталятора.
- Отправка — по идее, в этом месте можно кидать все в репозиторий типа Nexus или еще куда, но сейчас такой задачи не стоит, поэтому пропущу.
Для расширения фазы clean использую плагин maven-antrun-plugin.
<!-- Удаление custom-runtime и out -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<phase>clean</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<delete dir="custom-runtime"/>
<delete dir="out"/>
<delete dir="target"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
Для расширения фазы install использую плагин exec-maven-plugin.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<!-- JLink: создание custom-runtime -->
<execution>
<id>jlink</id>
<phase>install</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${java.home}/bin/jlink</executable>
<arguments>
<argument>--module-path</argument>
<argument>${java.home}/jmods;target/classes;target/libs</argument>
<argument>--add-modules</argument>
<argument>ru.makushimo.javafxexe,java.naming</argument>
<argument>--launcher</argument>
<argument>
javafxexe=ru.makushimo.javafxexe/ru.makushimo.javafxexe.HelloApplication
</argument>
<argument>--output</argument>
<argument>custom-runtime</argument>
</arguments>
</configuration>
</execution>
<!-- JPackage: создание инсталлятора после JLink -->
<execution>
<id>jpackage</id>
<phase>install</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${java.home}/bin/jpackage</executable>
<arguments>
<argument>--type</argument>
<argument>exe</argument>
<argument>--name</argument>
<argument>myJavaFxApp</argument>
<argument>--app-version</argument>
<argument>1.0</argument>
<argument>--module</argument>
<argument>
ru.makushimo.javafxexe/ru.makushimo.javafxexe.HelloApplication
</argument>
<argument>--runtime-image</argument>
<argument>custom-runtime</argument>
<argument>--dest</argument>
<argument>out/installer</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
Ну вот теперь, намного лучше. Для получения заветного инсталятора нужно выполнить команду:
mvn clean install
Ее можно использовать в CI/CD pipeline.
Заключение.
Это уже неплохо. Это уже рабочий вариант. Но не решенными остались несколько моментов:
- Обновление программы будет происходить путем де-инсталяции/инсталяции приложения из нового установщика. Чтобы прилжение можно было обновлять запуская установщик снова и снова, нужно бубен побольше. Например, собирать установщик типа msi, а не exe, для этого будет нужен WIX Tools.
- Зависимости могут быть модульными и не модульными. не модульные зависимости могут быть проигнорированы jlink. Проект может не быть чисто модульным из-за тех же не модульных зависимостей и тогда jlink либо не поможет, либо настройка команды будет другой.
- Приложение нужно все время устанавливать, а как же portable-вариант?
Разберу все эти сложности в последующих статьях, если возникнет такая необходимость.
Код этого шаблонного проекта можно забрать тут.