jpackage от А до Я: как правильно упаковать Java‑приложение в десктопный дистрибутив

Начну с небольшой предыстории. Я начинал свой путь в IT с базового курса «Разработка программного обеспечения» в одной из профильных академий. Этого фундамента хватило для уверенного старта, но реальные коммерческие задачи часто выходят за рамки учебных программ.
Одной из таких «болей» стала дистрибуция Java‑приложений конечному пользователю. Сколько раз мы слышали от клиентов: «А у меня не запускается, наверное, Java не та»? Чтобы раз и навсегда избавиться от необходимости писать в README фразу «сначала установите JRE», я решил глубоко погрузиться в тему нативной упаковки.
В этой статье я систематизировал весь свой опыт работы с jpackage: от создания портативных версий до сборки полноценных инсталляторов для Windows, Linux и macOS, а также разобрал подводные камни при работе с JavaFX и Spring Boot.
jpackage— это мощный инструмент, встроенный в JDK (начиная с версии 14, JEP 343/392), который предназначен для упаковки Java-приложений в нативные образы для конкретных платформ. Он автоматически создает нативный лаунчер (запускатор) и включает в себя урезанную версию JRE (созданную черезjlink`), чтобы пользователю не нужно было устанавливать Java.
Далее так как я не знал изначально что умеет jpackage я для себя выделил два приоритета:
1. Создание Portable (портативных) приложений
2. Создание установочных пакетов (Installers)
Далее я расскажу о каждом из приоритетов и не только чтобы у каждого получилось поделиться своим приложением на Java без последующих лишних вопросов.
ПРИОРИТЕТ 1: Создание Portable (портативных) приложений
Портативное приложение (app‑image) — это папка, которую можно скопировать на любой компьютер (флешку, другой ПК) и запустить без установки. Она содержит ваш код, нативный лаунчер и скрытую JRE.
Для ОС Windows
Результат: Папка с
.exeфайлом и подпапкойruntime.Команда:
jpackage --type app-image --name "MyApp" --input ./target --main-jar myapp.jar --main-class com.example.Main --dest ./distКак сделать по‑настоящему portable: Просто заархивируйте полученную папку в ZIP. Пользователь распаковывает её и запускает
.exe.Важно: Если вы хотите, чтобы приложение работало на старых версиях Windows (например, Win 7), убедитесь, что вы собираете его на JDK, скомпилированной для нужной архитектуры, и учитывайте зависимости C++ runtime (обычно
jpackageподтягивает нужные DLL, но иногда требуется Visual C++ Redistributable).
<ol start=«2»>
<li>Для ОС Linux</li>
</ol>
Результат: Папка с исполняемым скриптом/бинарником и подпапкой
runtime.Команда:
jpackage --type app-image --name "MyApp" --input ./target --main-jar myapp.jar --main-class com.example.Main --dest ./distОграничения «портативности» в Linux: В отличие от Windows, Linux‑портативность условна. Скомпилированная JRE будет зависеть от версии
glibc(GNU C Library) и графических библиотек (X11/Wayland, GTK) той системы, где происходила сборка.Решение:* Собирайте portable‑версию для Linux на самой старой целевой версии дистрибутива (например, на Debian 10 или Ubuntu 18.04), чтобы обеспечить обратную совместимость
glibc.
ПРИОРИТЕТ 2: Создание установочных пакетов (Installers)
jpackage умеет создавать нативные инсталляторы. Внимание: для их создания на машине сборщика должны быть установлены сторонние утилиты.
Windows (MSI и EXE)
Формат:**
.msi(нативно через WiX Toolset).Требования: Установлен WiX Toolset 3.0 или новее и добавлен в
PATH.Команда:
jpackage --type msi --name "MyApp" --app-version 1.0.0 --input ./target --main-jar myapp.jar --main-class com.example.Main --win-menu --win-dir-chooser --dest ./distПримечание:
jpackageне создает.exeинсталляторы (вроде InnoSetup) напрямую. Он создает MSI. Если нужен EXE, MSI можно обернуть в EXE через WiX Burn Bundle или InnoSetup пост‑фактум.
<ol start=«2»>
<li>Linux (DEB и RPM) </li>
</ol>
Форматы:
.deb(для Debian/Ubuntu),.rpm(для RHEL/Fedora).Требования: Установлены
dpkg-deb(для deb) иrpmbuild(для rpm).Команда (для DEB):
jpackage --type deb --name "myapp" --app-version 1.0.0 --input ./target --main-jar myapp.jar --main-class com.example.Main --linux-shortcut --linux-menu-group "Utility" --dest ./dist<ol start=«3»>
<li>macOS (DMG и PKG)</li>
</ol>
Форматы:**
.dmg(образ диска),.pkg(инсталлятор).Требования: Сборка должна происходить только на macOS.
Команда:
jpackage --type dmg --name "MyApp" --app-version 1.0.0 --input ./target --main-jar myapp.jar --main-class com.example.Main --mac-package-identifier com.example.myapp --dest ./distПРИОРИТЕТ 3: Запуск разных типов Java‑приложений
Главное правило jpackage: он запускает то, что запускает стандартная java -jar, но через нативный лаунчер. Однако есть нюансы для разных UI.
Консольные приложения (CLI)
Проблема: В Windows по умолчанию лаунчер
jpackageскрывает консольное окно, так как считает приложение GUI‑ным.Решение:** Используйте флаг
--win-console.Команда:
jpackage --type app-image --name "MyCLI" --input ./target --main-jar mycli.jar --main-class com.example.CLI --win-console --dest ./distДля Linux/macOS:* Консоль и так будет видна при запуске из терминала. Если нужен запуск по клику, создайте
.shскрипт‑обертку.
<ol start=«2»>
<li>Swing / AWT приложения</li>
</ol>
Особенности:** Работают «из коробки». Но в Windows могут быть проблемы с масштабированием (DPI) и рендерингом.
Решение: Передайте JVM‑аргументы для улучшения рендеринга.
Команда:
jpackage --type app-image --name "MySwingApp" --input ./target --main-jar myswing.jar --main-class com.example.SwingUI --java-options "-Dsun.java2d.d3d=true" --java-options "-Dsun.java2d.ddscale=true" --dest ./dist<ol start=«3»>
<li>3. JavaFX приложения (Самый сложный случай!)</li>
</ol>
С версии JDK 11 JavaFX отделена от JDK. jpackage не умеет автоматически подтягивать JavaFX, если вы используете «fat jar» (JAR со всеми зависимостями внутри).
Вариант А (Модульное приложение — РЕКОМЕНДУЕТСЯ): Если ваше приложение модульное (есть
module-info.java),jpackageсам всё поймет.
jpackage --type app-image --name "MyFXApp" --input ./target --main-jar myfx.jar --main-class com.example.FXApp --dest ./distВариант Б (Немодульное приложение / Fat JAR): Вам нужно явно указать
module-pathи добавить модули JavaFX, иначе вы получите ошибкуGraphicsDevice initialization failed.
jpackage --type app-image --name "MyFXApp" \
--input ./target \
--main-jar myfx.jar \
--main-class com.example.FXApp \
--module-path /path/to/javafx-sdk/lib \
--add-modules javafx.controls,javafx.fxml \
--dest ./distЛайфхак: Если не хотите возиться с --module-path, создайте класс‑пустышку, наследующий javafx.application.Application, и укажите его как --main-class.
<ol start=«4»>
<li>Spring Boot приложения (Ограничение!)</li>
</ol>
Проблема:** Spring Boot создает «fat jar» (JAR внутри JAR).
jpackageНЕ ПОДДЕРЖИВАЕТ вложенные JAR‑файлы. Он не сможет найти вашmain-class.Решение 1 (Правильное):** Используйте
maven-shade-pluginилиspring-boot-maven-pluginс конфигурациейlayout=ZIPиPropertiesLauncher, чтобы распаковать зависимости.Решение 2 (Обходное): Извлеките все зависимости из fat‑jar в папку
libперед запускомjpackage, и передайте все JAR‑ы в--input.
# Сначала распакуем Spring Boot fat jar
# Затем упакуем через jpackage, указав главный класс Spring Boot
jpackage --type app-image --name "MySpringApp" --input ./extracted-libs --main-jar my-spring-app.jar --main-class org.springframework.boot.loader.PropertiesLauncher --dest ./distВСЁ, ЧТО МОЖНО И НЕЛЬЗЯ ДЕЛАТЬ С jpackage
ЧТО МОЖНО (Возможности)
Бандлинг JRE:** Автоматически создает минимальную JRE (через
jlink), уменьшая размер дистрибутива с ~300 МБ до ~30-50 МБ.Кастомизация JVM:** Передача любых аргументов виртуальной машине (
--java-options), например, выделение памяти (-Xmx1024m), настройка кодировки (-Dfile.encoding=UTF-8).Иконки и ресурсы:** Назначение нативных иконок (
.icoдля Win,.icnsдля Mac,.pngдля Linux) через--icon.Использование готовой JRE: Если вы заранее собрали идеальную JRE через
jlink, вы можете скормить еёjpackageчерез флаг--runtime-image, чтобы он вообще не трогал модули.Добавление файлов: Копирование конфигов, README, баз данных в папку приложения через
--resource-dirили--file-associations.Ассоциация файлов:** Привязка расширений файлов к вашему приложению (чтобы双击 по файлу
.myextоткрывал его в вашей программе) через--file-associations.Подпись кода (Code Signing):
jpackageпозволяет подписывать приложение.Windows: Через
--win-upgrade-uuidи пост‑обработкуsigntool.macOS: Через
--mac-sign,--mac-signing-key-user-name,--mac-signing-keychain.
ЧТО НЕЛЬЗЯ (Ограничения)
Создание ОДНОГО файла (.exe /.appimage):**
jpackageникогда не создает один исполняемый файл. Он всегда создает папку (дляapp-image) или архив/инсталлятор, внутри которого лежит папка с JRE. Сделать «портативный exe на 50 МБ» штатными средствами нельзя (нужны сторонние упаковщики вроде Enigma Virtual Box, но они ломают JRE).Компиляция кода:**
jpackageне компилирует.javaв.class. Ему нужны готовые.jarили.classфайлы.Кросс‑компиляция:** Вы не можете собрать
.msiили.dmgна Linux. Вы не можете собрать.debна Windows. Сборка инсталляторов возможна только на той ОС, для которой она предназначена. (Исключение:app-imageдля Linux можно собрать на Windows, но он не будет работать без WSL/Cygwin, так что на практике собирайте на целевой ОС).Нативное шифрование кода:
jpackageне защищает ваш код от декомпиляции. Ваш.jarвнутри пакета лежит в открытом виде. Для защиты используйте обфускаторы (ProGuard, Zelix KlassMaster) до упаковки.Поддержка Nested JARs:** Как указано выше, Spring Boot fat‑jar «из коробки» не поддерживается.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.