Всем привет. Меня зовут Тетка Андрей, и я работаю в качестве Java Backend Developer в компании CodeValue в Израиле. Моя основная обязанность - разработка BackEnd на Java.

Время от времени я делаю свои "Pet Projects", которые позволяют мне проверить свои навыки и испытать себя в новых технологиях. Недавно я решил попробовать свои силы с последней версией Spring Boot v3 и некоторыми другими технологиями. В итоге получилось кое что интересное, и я решил попробовать сделать отдельное приложение. Однако, я столкнулся с некоторыми проблемами при создании и дистрибьюции Java приложений.

В этой статье я расскажу о том, как я разрабатывать Desktop-приложения на Java с Spring Boot. 

До начала необходимо оговориться о некоторых деталях. Я не являюсь профессиональным разработчиком Desktop-приложений и первый раз решил попробовать себя в этой области. Следовательно, могут возникнуть идеологические или практические неточности. Моя цель - дать понятное руководство для тех, кто сталкнётся с аналогичными трудностями. Лично мне его не хватало. Вы можете выбрать другой путь решения проблемы. Создать инсталлятор который установит все зависимости или просто создать скрипт который установит все зависимости. Всё зависит от потребностей.

Давайте начнем с основ. 

Важно знать, что Java не является лучшим языком для разработки интерфейсов пользовательского интерфейса. Хотя концепция "написано один раз, работает везде" привлекательна, есть некоторые ограничения, которые следует учитывать. JVM долго запускается и при запуске код работает в режиме интерпретатора, что конечно влияет на производительность. По этому Джаву чаще используют именно на backend, что бы пользоваться быстрой Джавой, которую скомпилирует JIT compiler. Так же использовать Spring в приложении которое запускается как Desktop Application тоже не лучшее решение, Spring хоть и удобен, но может долго подниматься при запуске. В пустом приложении это занимает секунды, а если мы добавим работу с базой данных(H2), различные сервисы, то не удивительно если приложение будет подниматься уже минуты. Так же стоит помнить что у пользователей нашего приложения может быть различные компьютеры, на одном приложение может подниматься за 10 секунд, а на другом за 50 и это будет вполне нормальная ситуация. Но раз уж изначально это был эксперимент, то давайте начнём создавать наше приложение.

Для начала давайте создадим Spring Boot проект. Для этого идём на https://start.spring.io/ и выбираем то что нам нужно.

После генерации проекта мы получаем стандартный файл для запуска Spring boot в приложении.

Spring может запускаться от нескольких секунд до нескольких минут, всё конечно же зависит от размера проекта. Чтобы улучшить пользовательский опыт, необходимо изменить последовательность и в первую очередь отобразить интерфейс (GUI), который покажет, что приложение загружается.

Давайте перейдём к выбору GUI фреймворка для нашего приложения. 

Первый вариант который я начал рассматривать, это написать отдельное приложение для GUI интерфейса на более подходящем для этого языке. Это приложение уже скачало бы JVM при необходимости и управляло бы моим приложение. Я рассмотрел 4 варианта:

  1. Flatter - относительно новая разработка от Google, применяется для создания мобильных и десктопных кросплатформенных приложений. Но минимальное приложение, с пустой формой весило 100Мб, даже при отключении всех дебаг функций не удалось сильно сократить размер приложения

  2. GoLang - не самый очевидный вариант для GUI интерфейса и не самый правильных, но в целом довольно интересный. Правда скомпилированный пустой аппликейшн опять весил 50+ Мегабайт и даже не запустился

  3. C++ QT - QT наверное является одним из самых основных фреймворков для создания приложения и он всем хорош

    1. Кроссплатформенность - есть

    2. Возможность создать красивый интерфейс - есть

    3. Выходной файл весил всего 80Кб, а туда я даже добавил кнопку чего я не позволял себе во Flatter и GoLang

    4. Есть Scene Builder где мы можем просто перетаскивать кнопочки и тем самым создавать красивый интерфейс

      Но был и один недостаток, а именно C++. Всё таки плюсы это сложнее и не так приятны для разработки. А так как был чисто не коммерческий проект, то не хотелось сильно запариваться с этим всем

  4. Python QT - Так как мне понравился QT, изначально не хотел его пробовать так как этот фреймворк уже довольно старый, но не понравился именно C++, то решил попробовать его с Python. И тут было всё замечательно, но у пользователя может не оказаться Python и тогда прийдётся его просить устанавливать и его, что не очень приятно

В итоге попробовав способ с полным разделением визуала и функционала меня не устроило и пришлось выбирать Java UI Framework. А выбирать действительно есть из чего. Я при разработке выбрал JavaFX, как наиболее подходящий и функциональный GUI фреймворк для Java, но меня ждало несколько неприятностей. 

JavaFX — платформа на основе Java для создания приложений с насыщенным графическим интерфейсом. Она может использоваться для создания настольных приложений, запускаемых непосредственно из под операционных систем, интернет-приложений, работающих в браузерах, а также приложений для мобильных устройств. JavaFX предназначена для замены ранее используемой библиотеки Swing.

JavaFX очень быстро ворвался в JDK 8, но так же быстро оттуда и выпилился.

Начиная с версии Java 11 больше не входит в Java SE и не разрабатывается компанией Oracle (как отдельный модуль поддерживается компанией Gluon).

В целом JavaFX во всём хорош, но то что его нет больше в составе JRE, это печально. Но временами без танцев с бубнами не обойтись и не всегда всё работает гладко. Например, могут быть различные проблемы на процессорах Apple M1, а на Windows x64 всё будет работать без проблем. Так что, если вы планируете создать совсем простой GUI "с одной кнопкой", то лучше выбрать Swing.

И так приступим. Давайте переделаем наше приложение, чтобы при запуске отображалось окно загрузки пока инициализируется Spring.

Для начала подключим зависимости. Я сгенерировал Gradle проект, по этому буду использовать код для Gradle в примерах ниже. Также я пользуюсь новым интерфейсом IntelliJ Idea и скриншоты будут с новым интерфейсом, но думаю разобраться не составит труда.

  1. Подключаем плагин gradle

plugins {
	id 'org.openjfx.javafxplugin' version '0.0.13'
}
  1. Так же в build.gradle добавляем конфигурацию для нашего приложения

javafx {
	version = "17"
	modules = ['javafx.controls']
	configuration = "compileOnly"
}
  1. Скачиваем JavaFX SDK: https://gluonhq.com/products/javafx/ 

  2. После распаковки нужно подключить SDK к нашей IDE. Для этого идём в настройки проекта и подключаем библиотеку

Далее выбираем что хотим добавить библиотеку Java и указываем путь к папке куда вы распаковали SDK

  1. Ну и остался последний момент. Нужно подключить нашу библиотеку к проекту. Для этого идём в модули, выбираем наш проект и добавляем зависимость

  2. После этого остаётся ещё одно действие. Нам нужно добавить несколько параметров при запуске нашего приложения.

    Нам нужно добавить параметры для виртуальной машины. По умолчанию они не отображаются, найти их можно в меню More Options -> Add VM Options. 

    Добавляем туда следующие значения (заменяя путь до вашего JavaFX SDK)

    --module-path {JAVAFX_SDK}\lib --add-modules javafx.controls

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

Теперь нам нужно переписать приложение таким образом чтобы сразу отобразилось окно загрузки и параллельно грузилось наше приложение.

Для этого я нашёл GIF анимацию “загрузки” которую, я просто отображу, а затем уже нарисую весь остальной дизайн. Вы конечно можете сразу отрисовать дизайн, но это уже зависит от ваших задач. Мне рисовать ничего не надо, пока не поднимется база данных, по этому я отобразил загрузку приложения. У нас получился вот такой код, где в начале показывается окно загрузки, а затем приложение.

Теперь, всё работает как надо у нас и можно писать дальше наше приложение с полноценным Spring Boot. Но что дальше, как распространять наше приложение?

Что делать пользователям у которых нету JRE на компьютере?

В целом уже давно есть множество решений данной проблемы, даже в составе JDK. В различных версиях Java начали появлятся различные тулы, такие, как JLink или JPackege. Эти инструменты позволяют вам создать приложение, которое можно будет запустить одним кликом. Но к сожалению у меня эти тулы так просто не завелись. По этому я предлагаю другое решение, а именно Launch4J. С его помощью можно будет создать EXE файл, который будет запускаться двойным кликом мышки. В целом там всё крайне просто.

  1. Собираем наше Spring приложение с помощью команды bootJar

  2. Скачиваем и устанавливаем Launch4J https://launch4j.sourceforge.net/ 

  3. Запускаем его и настраиваем. Важны 3 основных поля (Output file, Jar, Icon)

  4. Идём в вкладку JRE и добавляем наши параметры для виртуальной машины(путь указываем именно такой, чуть позже станет понятно почему так) --module-path lib --add-modules javafx.controls

  5. Готово, собираем нажав на шестеренку (он предложит сохранить файл конфигураций, сохраняем его) и готово.

  6. Мы получили наш Exe файл, но мы его не сможем запустить. Всё потому что у него нету библиотек для запуска JavaFX. Для этого вам нужно содержимое JavaFx SDK положить в туже папку, где и ваш EXE файл и распространять в месте с ним. 

  7. Наша программа запустилась. Теперь она работает независимо и красиво. Из рекомендаций могу дать совет запустить вашу программу и удалить всё, что можно из папок JavaFX SDK, Windows не даст удалить то, что используется, а всё лишнее удалится(но будьте осторожны, так можно удалить что то нужное, проверьте все внимательно). Таким образом я из 85 МБ сделал 11,5 МБ. Хотя конечно это не так критично, потому что само приложение со Spring boot и библиотеками весит 150 мегабайт, но уменьшил объем требуемых файлов на треть.

    Ссылка на Github: https://github.com/kecven/JavaFX-with-Spring-Boot

Комментарии (23)


  1. jreznot
    00.00.0000 00:00
    -1

    Настольным приложениям намного легче с DI на базе PicoContainer или накрайняк Guice. Я бы советовал не брать туда Spring ни в коем случае.


  1. jreznot
    00.00.0000 00:00
    +2

    А для сборки бинарников JavaFX приложений рекомендуется использовать jlink, IntelliJ IDEA создает готовый проект JavaFX со всем необходимым сама в File - New Project - JavaFX. И никакой Launch4j не требуется


    1. Kecven Автор
      00.00.0000 00:00
      +1

      Спасибо. Обязательно гляну на DI на базе PicoContainer, если честно не знаю что это такое. По поводу создания javaFx приложения в Idea, видел такую возможность, но если честно так и не опробовал, так как пришлось бы весь проект заново пересоздавать и в него уже засовывать свои классы. Рассматривал другие варианты, но может руки и дойдут до этого варианта.

      Спасибо за комментарий!


      1. olku
        00.00.0000 00:00
        +1

        Спринг, вероятно, вам был нужен на десктопе только из за DI и Hibernate. На что намекнули выше. А готовое приложение можно поставлять вместе с рантаймом, сейчас 300 мб архива роли не играют, зато избавитесь от проблем с зоопарком версий на машинах клиентов.


        1. Kecven Автор
          00.00.0000 00:00

          Изначально спринг заюзался потому что я в целом хотел поиграться с новой версией Spring Boot 3 и с Playwright, цели создавать десктопное приложение не было. в целом я писал об этом в самом начале, что это далеко не лучшее решение. Боялся что вообще захейтят за это. Но вижу тут полезные комментарии с которыми я полностью согласен и солидарен.

          Спасибо, поставил всем лайки на коменты)))


          1. olku
            00.00.0000 00:00

            Докину лайфхак. Можно скачать архивы под каждую платформу с https://elastic-kaizen.com/download заменить джарник и запаковать назад. Поставка готова.


    1. oldd
      00.00.0000 00:00

      А лучше сразу подключить com.github.johnrengelman.shadow, и настроить его так, как нужно. Можно сразу нужную версию FX запаковать


      1. BitLord
        00.00.0000 00:00

        Добавлю в эту ветку. Для таких же целей использовал плагин для Gradle Badass JLink <org.beryx.jlink>. Показалось тоже достаточно удобным.


  1. jreznot
    00.00.0000 00:00
    +2

    А если говорить совсем начистоту про DI и настольные приложения, то он там вообще вреден. Показателен пример IntellIJ IDEA, которая раньше использовала какой-никакой DI с PicoContainer, но полностью от него отказалась в пользу простейшего паттерна Service Locator, который позволил загружать сервисы лениво по надобности и не грузить все классы сервисов заранее в память на стартапе.

    Для настольных приложений внезапно оказывается стартап и отзывчивость важнее, чем красивая архитектура зависимостей.

    IntelliJ IDEA - Light Services.

    Do not acquire service instances eagerly or store them in fields, but obtain them in the place(s) where they will be used.


    1. vkoshkin
      00.00.0000 00:00

      Обычно DI полезен в случаях, когда ты хочешь отделять слой отображения от слоя контролирующей логики, как делают в том же Android. Неужто и здесь лучше обходиться без DI?


  1. nickD
    00.00.0000 00:00

    graalvm Native Image для бинариков


    1. jreznot
      00.00.0000 00:00

      Там до сих пор нет отладчика для Windows и с JavaFX будет гора проблем. Это пока работает только в бэкенде и на Linux


    1. Kecven Автор
      00.00.0000 00:00

      Согласен с предыдущим коментарием что нейтивы до сих пор работают не идеально. Однако в качеству JVM уже пересел GraalVM и этот проект так же работает по верх него без проблем



  1. Bakuard
    00.00.0000 00:00

    Чтобы jlink и jpackager могли собрать нативный образ - все завивисмости вашего приложения должн быть модульными, т.е. моддерживать JPMS. Поэтому, вместо H2 рекомендую взять HSQLDB, в качестве бибилиотеки логирования - logback версии 1.3 и выше.

    P.s. по сравнению с прожорливостью jlink и jpackager - Flatter и Golang выдают очень даже компактные сборки. Когда я написал небольшой таймер на JavafX + Gson + Logback, то получившейся бинарник весил целых 500 мб (500 МБ, КАРЛ!). Может я неправильно использовал эти инструменты, но это прям дохрена.


    1. oldd
      00.00.0000 00:00

      100% вы неправильно используете эти инструменты. Мои fatjar с javafx, usb, rabbitmq, postgresql, блекджеком и девушками не выходят за 50мб.
      Хотя делфийское приложение с тем же функционалом - 4мб ((


      1. Bakuard
        00.00.0000 00:00

        Собирал свое приложение под Ubuntu 20.04 используя OpenJDK. Попробовал добавить для jlink опции --strip-debug, --no-header-files, --no-man-pages. В итоге получилось ужать образ, но только до 100 МБ. Если вам не сложно, опишите кратко как вы смогли получить ваши 50мб?
        P.s. я продолжаю надеяться, что когда-нибудь, с помощью этих инструментов, можно будет собрать образ весящий не больше чем делфийские приложения. Эх, мечты)


        1. oldd
          00.00.0000 00:00

          Пишу на котлине, система сборки gradle. Для сборки использую shadowJar.
          Это почти рабочий пример, за исключение некоторых nba-кусочков

          plugins {
              application
              id("java")
              kotlin("jvm") version "1.7.20"
              kotlin("plugin.serialization") version "1.7.20"
              id("com.github.johnrengelman.shadow") version "5.2.0"
          }
          
          dependencies {
              implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
              implementation("org.jetbrains.exposed:exposed:$exposed_version")
              implementation("org.postgresql:postgresql:$postgresql_version")
              implementation("com.rabbitmq:amqp-client:latest.release")
              implementation("org.codehaus.groovy:groovy-all:3.0.9")
              implementation("ch.qos.logback:logback-classic:$logback_version")
              implementation("no.tornado:tornadofx:1.7.19")
              implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_version")
              implementation("org.usb4java:usb4java:1.3.0")
              implementation(fileTree("../libs"))  // тут лежат библиотеки javafx
          }
          
          sourceSets.main {
              java.srcDirs(
                // тут пути к исходникам
              )
              resources.srcDirs(
                // тут пути к ресурсам
              )
          }
          
          application {
              mainClassName = "ru.AAA.AAA.MainAbdKt"
          }
          
          tasks {
          
              shadowJar {
                  manifest {
                      attributes["mainClassName"] = "ru.AAA.AAA.MainAbdKt"
                      attributes["Company"] = "AAA"
                      attributes["Implementation-Version"] = "6.0 build $build от ${formatter.format(LocalDateTime.now())}"
                  }
              }
              compileKotlin {
                  kotlinOptions.jvmTarget = "1.8"
              }
              compileTestKotlin {
                  kotlinOptions.jvmTarget = "1.8"
              }
          }


          1. DrCroaker
            00.00.0000 00:00

            jvmTarget = "1.8"

            Ну какие модули в 8ой жабе!?

            Не удивлён 500 мегам. Вам натолкали весь JDK + всё jar-ы библиотек и все jar-ы их зависимостей. Удивлён, что оно вообще собралось.

            Вот прям сейчас небольшой проект сдал. JavaFX + ControlsFX + JNA(native к железкам) + Jackson + пара моих либ + ресурсы. Итого: экзешник 120MB. Причем ОЗУ это всё жрет раз в пять меньше, чем если на Electron-е делать хех

            А если только голый JavaFX c JRE то меньше 50MB должен выходить.


            1. oldd
              00.00.0000 00:00

              Читайте внимательнее, плиз. Это сборка моей программы, которая с "javafx, usb, rabbitmq, postgresql, блекджеком и девушками". Кстати, забыл, что там ещё и groovy в качестве скриптов работает .

              Итого jar в данный момент 51мб . Ну плюс ещё нужна голая jre, без javafx


              Если я правильно понял ваше "Итого: экзешник 120MB.", то это размер инсталяшки с jre ?


  1. Max_Pershin
    00.00.0000 00:00

    Сайт конечно https://openjfx.io/ выглядит как антипропаганда. Из интересных приложений - только расчет траекторий для космоса, но красиво. Обидно слегка.


  1. Cheregorka
    00.00.0000 00:00

    Как раз сейчас балуюсь таким же стеком.

    Только пару нюансов отличается:

    • Javafx библиотеки подтягиваю прямо в градле из блока dependencies. Причем подтягиваю сразу 3 ОС. (Проблему с М1 это все равно пока что не решает, даже если добавить 4ый тип -mac-aarch64)

    • javafx { } Gradlew Task - хранит в себе версию и модули JavaFx, без строки configuration..

    • Сборку fat jar делаю с помощью плагина shadowJar. Обычный bootJar с задачей справился не так хорошо.

    • Баг с потерянными javafx зависимостями решил советами из интернета через доп класс Launcher с main() методом.

    • На выходе имею кросс платформенный jar ~85мб , который стартует только на системах с установленной Java.

      След шаги :

    • Попробовать зашить Java либу в Jar, Все так же не сделав проект модульным, если это возможно.

    • Затестить native билды с GraalVM, опять-таки, если это в моих условиях возможно.

    • Попробовать повырезать лишние Spring модули для уменьшения размера.

    • Ну и не связанное с конфигом: сильнее разделить UI слой от реальной бизнес логики и добавить команды вызываемые из командной строки..(знал бы раньше - раньше бы так изначально и делал)

      Есть какие-то весомые причины не использовать зависимости из градла, а подтягивать локальные javafx либы?


    1. oldd
      00.00.0000 00:00

      Есть. Почему-то градл не любит линковать виндовую javafx при сборке под линуксом.