Pull to refresh

Java: есть ли жизнь на десктопе?

Reading time15 min
Views26K

Привет! Я Виктор Барсуков, Java-разработчик в Lamoda. В этой статье я хочу рассказать о разработке десктопного Java-приложения, что из этого получилось и что можно было бы сделать иначе.

В 2019 году в России ввели обязательную маркировку ряда товаров. В связи с этим малому бизнесу пришлось покупать и настраивать дорогие по меркам МСП (то есть, малых и средних предпринимателей) программы и оборудование. 

У меня есть такие знакомые, и из общения с ними я знаю, что в России малому бизнесу и без маркировки хватает забот. Поэтому решил внести посильный вклад в развитие МСП. Но какой вклад может сделать программист? Написать программу. Программу с одной-единственной функцией — печатью кодов маркировки, которая должна помочь соблюсти требования новых законов и избежать непомерных штрафов.

Предыстория проекта

С 2019 года в РФ введена обязательная маркировка ряда товаров. Более подробно об этом можно узнать на сайте Честного Знака. На момент старта маркировки было много нерешенных вопросов, которые озвучивались в профильных чатах. Малым предприятиям требовалось приобретать и настраивать достаточно дорогие программные решения. В результате многие покупали 1С со всем его богатым набором функций исключительно ради печати DataMatrix-кодов.

Мне показалось, что ситуацию вполне можно исправить, написав программу с единственной функцией — печать кодов маркировки. Собственно, с этого все и началось.

Дисклеймер: сразу оговорюсь, что проект делается в свободное от основной работы время, поэтому глубоко исследовать разные технологии было некогда. Часто приходилось выбирать не самое оптимальное решение, а первое, которое показалось приемлемым, и с хорошо описанной документацией.

Используемые технологии

Так как я Java-разработчик, то неудивительно, что в первую очередь рассматривался знакомый язык. Конечно, Java чаще всего ассоциируется с разработкой бэкенда, но я решил попробовать его для десктопа.

При разработке десктопного приложения на Java выбор в принципе стоит между JavaFx и Swing. JavaFx показался дружелюбным в разработке, поэтому выбор пал на него. Swing все-таки считается устаревшей технологией и даже Oracle говорит о замене Swing на JavaFx.

Изначально проект был на Java 8, потому что у него еще есть поддержка JavaFx. В более старших версиях эту функцию убрали, поэтому потребуются определенные танцы с бубном для запуска приложения. Ниже опишу, как удалось обойтись без них и продолжить поддерживать актуальный стек. 

Сборка дистрибутива

Первые версии приложения поставлялись как fat-jar со всеми зависимостями, но это оказалось неудобно. Целевая аудитория — малые предприниматели, у которых уровень владения компьютером чаще всего на уровне пользователя, изредка — продвинутого пользователя. Для них оказалось весьма проблематично устанавливать Java правильной версии, а потом запускать приложение через bat-файл. 

Сначала я рекомендовал сборку Java от Liberica: после инсталляции она связывает зависимости, и приложение запускается просто по двойному клику мыши. Но даже после этого при вводе запроса «barCodesFX» Google продолжал автоматом подсказывать «как запустить». К счастью, сейчас ситуация поменялась.

Эта подсказка натолкнула меня на мысль, что приложение должно запускаться через exe-файл по двойному клику без установки дополнительного ПО. Или же это ПО должно ставиться автоматом.

В поисках решения нашелся проект Launch4j. Он позволяет создавать exe-файл, выступающий в роли «стартера» для основного jar-приложения. При этом JRE «правильной» версии помещается в состав дистрибутива и не требует отдельной установки. Это увеличило размер дистрибутива, но в век безлимитного интернета лишние 70 Мб для удобства пользователя не кажутся критичными.

Отдельная благодарность команде Liberica, которая поставляет сборки JRE с подключенными модулями JavaFx — это сильно облегчает запуск приложения.

Сам exe-файл создается при сборке после добавления плагина в maven-проект.

<plugin>
   <groupId>com.akathist.maven.plugins.launch4j</groupId>
   <artifactId>launch4j-maven-plugin</artifactId>
   <version>2.1.2</version>
   <executions>
       <execution>
           <id>l4j-clui</id>
           <phase>compile</phase>
           <goals><goal>launch4j</goal></goals>
           <configuration>
               <headerType>gui</headerType>
               <outfile>target/BarCodesFX.exe</outfile>
               <jar>barCodesFX.jar</jar>
               <icon>src/main/resources/microQR.ico</icon>
               <downloadUrl>https://download.oracle.com/java/17/archive/jdk-17.0.1_windows-x64_bin.msi</downloadUrl>
               <errTitle>Launching error</errTitle>
               <dontWrapJar>true</dontWrapJar>
               <jre>
                   <path>%PWD%/jre-17.0.1</path>
                   <jdkPreference>preferJdk</jdkPreference>
                   <initialHeapPercent>20</initialHeapPercent>
                   <maxHeapPercent>80</maxHeapPercent>
               </jre>
               <versionInfo>
                   <fileVersion>0.0.0.0</fileVersion>
                   <txtFileVersion>${version}</txtFileVersion>
                   <fileDescription>BarCodesFX-${version}</fileDescription>
                   <copyright>Barsukov Viktor</copyright>
                   <productVersion>0.0.0.0</productVersion>
                   <txtProductVersion>${version}</txtProductVersion>
                   <productName>BarCodesFX</productName>
                   <internalName>BarCodesFX</internalName>
                   <originalFilename>BarCodesFX.exe</originalFilename>
               </versionInfo>
               <messages>
                   <startupErr>Startup error</startupErr>
                   <bundledJreErr>Bundled Jre Error</bundledJreErr>
                   <jreVersionErr>Jre Version Error</jreVersionErr>
                   <launcherErr>Launcher Error</launcherErr>
               </messages>
           </configuration>
       </execution>
   </executions>
</plugin>

Через несколько недель после компоновки дистрибутива был обнаружен задокументированный баг: если в пути к exe-файлу есть названия папок на национальных языках (то есть на любом, кроме английского), то приложение не запускается. К счастью, проект Launch4j в целом живой и этот баг пофиксили.

Далее zip-архив вручную упаковывается в SFX-архив и выкладывается в SourceForge. Теперь пользователь может спокойно скачать единственный установочный exe-файл, несколько раз нажать ОК и Далее и получить работающую программу. 

Функциональность расширяется: подключаю Spring и СУБД

Как скрестить ужа с ежом?
Как скрестить ужа с ежом?

Проект развивался по классическому Agile: вначале скейт, потом самокат, потом велосипед. На старте основная функциональность заключалась в печати датаматриксов. Потом добавилось генерирование XML-файлов, а в самой программе появились настройки. 

Изначально настройки хранились в файле и подтягивались с запуском приложения, однако для пользователей это было неудобно и неочевидно. Затем все переехало в графический интерфейс, но тут начались проблемы с общим доступом к файлу настроек и стало понятно, что не хватает базы данных. Для создания БД мне захотелось использовать знакомый фреймворк, а не писать все с нуля, поэтому я подключил к проекту Spring.

К счастью, есть проект JavaFX Weaver, который решает проблемы получения колючей проволоки путем скрещивания ежа и ужа: контроллеры превращаются в бины, dependency injection становятся доступными и становятся доступными прочие плюшки Spring. Но главное — не требуется полностью переписывать все приложение.

В качестве базы данных была выбрана H2. Она легковесная и не требует установки. Пользователь просто запускает приложение и все работает без предварительной установки СУБД.

Добавление шаблонизатора

Проект изначально был опенсорсный, поэтому довольно быстро набрал популярность. Но конкуренты не дремлют: для увеличения трафика коммерческие компании создавали свои бесплатные решения. И у этих решений было одно ключевое преимущество —  возможность создавать в удобном графическом редакторе свой шаблон этикетки.

Для меня разработка такой функциональности была непозволительной роскошью, потому что заняла бы много времени без гарантии положительного результата. Однако мне все же хотелось добавить шаблонизатор. По умолчанию в моем приложении было только два варианта печати: либо А4, либо термопринтер. На самой этикетке выводился только датаматрикс и чуть-чуть пользовательского текста.

Первый вариант наклеек для листа А4, а второй — для термопринтера
Первый вариант наклеек для листа А4, а второй — для термопринтера

В поисках решения я подробно изучил продукты компании iText. Их библиотека iТext PDF используется в BarCodesFX для генерации PDF. У нее есть много разных функций и при этом она полностью бесплатная при использовании в опенсорс-проектах. В коде можно полностью собрать pdf-файл с форматированием, дополнительными элементами и картинками. Через нее же происходит преобразование строки в датаматрикс.

У них же обнаружил pdfHTML — библиотеку, которая преобразует HTML-страницу в PDF. Внутри нее нашлась функциональность для заполнения HTML-шаблона. Так у пользователя появилась возможность самостоятельно сверстать практически любую этикетку, а не ограничиваться предустановленными шаблонами.

В шаблоне часть тегов нужно было заменять на пользовательские значения. Например, вместо <datamatrix/> выводить картинку кода, а вместо <page_number/> — номер страницы. Для этого разработчику достаточно в свойствах передать свою CustomTagWorkerFactory, которая наследует от DefaultTagWorkerFactory. Таким образом, все изменения гармонично встраиваются в поток обработки HTML-кода, при необходимости заменяя дефолтные обработчики тегов на собственные, с помощью имплементации интерфейса ITagWorker . 

properties.setTagWorkerFactory(new CustomTagWorkerFactory());
List<IElement> elements =
   HtmlConverter.convertToElements(new FileInputStream(htmlSource), properties);

for (IElement element : elements) {
   document.add((IBlockElement) element);
}

Можно сказать, что в целом шаблонизатор удался и им активно пользуются. Из 194 млн распечатанных кодов 25 млн было сгенерированно с использованием шаблонизатора. В качестве источника данных для заполнения полей типа артикула, цвета и размера используется эксель-файл.

Итоговая кастомизированная наклейка
Итоговая кастомизированная наклейка

Хотя не обошлось и без нюансов при верстке. Например, если ваш кастомный тег <datamatrix/> обернуть в <div>, то замена не произойдет. Такая вот особенность библиотеки.

Попытки интеграций с API

За время существования проекта было две попытки интеграций с API операторов маркировки.

Первая интеграция была с СУЗ (станцией управления заказами) оператора ЦРПТ (Центра развития перспективных технологий). Она прошла успешно, все работало, но в какой-то момент оператор без предупреждения изменил поведение API. Это привело к массовым обращениям от пользователей, которые сообщали об ошибках в работе приложения.

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

Вообще работа с криптографией — это весьма интересная задача. Но использовать отечественного криптопровайдера типа КриптоПро в десктопном Java-приложении — это задача «со звездочкой».

Вторая попытка интеграции была с API оператора Республики Беларусь. Авторизация у оператора проходила по логину и паролю и не требовала наличия крипто-провайдера. Функциональность была готова к релизу, но в последний момент пришлось отказаться.

  • Во-первых, была плохая коммуникация с поддержкой оператора РБ, нежелание разбираться и что-то решать. Их позиция: «У нас все работает, проблема на вашей стороне».

  • Во-вторых, нарушение стандарта RFC 8259 при реализации API. В чате поддержка оператора сказала, что символы GS не нужно экранировать при использовании их API. Это дважды нарушает 7-ой пункт стандарта.

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

Генерация XML

Чтобы при работе на сайте не отмечать каждый код в длинном списке, Честный Знак добавил возможность загружать коды файлами. Например, вам нужно списать 2000 кодов и вы просто загружаете один файл, а не выискиваете все эти коды и не прокликиваете их по одному. 

Однако файл должен быть в xml-формате с правильно заполненными тегами. Вручную его создавать еще сложнее, чем прокликивать коды по одному. И именно с созданием таких файлов и помогает программа.

Задача создания xml-файлов достаточно типовая, поэтому здесь особо нечего рассказывать, кроме трех интересных моментов:

Для генерации XML используется библиотека Jackson FasterXML. При сериализации списков пришлось добавить кастомный StdSerializer, иначе вместо этого…

<products_list>
    <product>
      <ki>0104600840362250215&lt;QCkpilDMTLs</ki>
    </product>
    <product>
      <ki>0104600840362250215*uJEIn4EH&gt;Rd</ki>
    </product>
</products_list>

… получали лишние теги product вокруг списка.

<products_list>
<product>
    <product>
        <ki>0104600840362250215&lt;QCkpilDMTLs</ki>
    </product>
    <product>
        <ki>0104600840362250215*uJEIn4EH&gt;Rd</ki>
    </product>
</product>
</products_list>

Разрулить ситуацию через аннотации не представлялось возможным, потому что классы генерируются автоматически на базе XSD-схем. 

ЦРПТ ввели дополнительные правила экранирования символов в кодах маркировки в XML. По этой причине нужно было экранировать одинарные и двойные кавычки, хотя этого не требуется по стандарту (пункт 2.4). 

К счастью, в библиотеке FasterXML это удалось решить добавлением кастомной EscapingWriterFactory.

public Writer createEscapingWriterFor(final Writer out, String enc) {

   return new Writer(){
       @Override
       public void write(char[] cbuf, int off, int len) throws IOException {

           String val = "";
           for (int i = off; i < len; i++) {
               val += cbuf[i];
           }
           String escapedStr =  StringEscapeUtils.escapeXml(val);
           out.write(escapedStr);
       }

       @Override
       public void flush() throws IOException {
           out.flush();
       }

       @Override
       public void close() throws IOException {
           out.close();
       }
   };
}

ЦРПТ предоставляет некорректные xsd-схемы. Схемы разделены по документам: есть документ на ввод в оборот, вывод из оборота, списание. Для каждого документа есть свой xsd-файл и при этом есть общая для всех схема — «Определенные пользовательские типы». 

Общая схема добавляется через импорт в схемы документов. Например, в схеме документа списания есть такой элемент:

 <xs:element name="cis" type="cis_type">
   <xs:annotation>
     <xs:documentation>Код идентификации</xs:documentation>
   </xs:annotation>

Однако тип cis_type не отражен в общей схеме «Определенные пользовательские типы». Такие типы приходилось допридумывать самостоятельно, потому что даже техподдержка не смогла предоставить полностью валидные схемы.

Тесты

Боль. Их просто нет. А для уважающего себя проекта они должны быть написаны — хотя бы юнит-тесты. Но на все не хватает времени.

И к сожалению, нет ни малейшего представления, как можно тестировать сам интерфейс. Вся надежда на проект TestFX, но пока у меня еще не дошли руки посмотреть его подробнее.

Что стоило бы изначально сделать иначе

Контроллер. Сейчас при запуске приложения открывается форма со множеством вкладок, которые программно управляются из одного класса-контроллера. Из-за этого класс достигает размера под 1000 строк. Можно было бы подключать каждую вкладку как отдельный FXML-файл со своим контроллером — это сделало бы программу намного удобней в разработке и поддержке.

Подключить DI (dependency injection) на более ранних этапах. Это повлияло бы в целом на архитектуру и упростило как разработку, так и дальнейшую поддержку. До сих пор в коде видны следы разработки до DI с ручным управлением жизненным циклом объекта. Изначально при написании пришлось потратить время на кодирование жизненного цикла и при дальнейшей разработке приходится помнить об этой особенности.

Немного статистики

Поделюсь статистикой скачиваний за все время существования проекта. Пик в 7000 скачиваний — это выброс из-за сбоя в SourceForge, когда вместо загрузки файла заново запускался таймер, и так до бесконечности. При этом прокручивался счётчик скачиваний. Остальные пики совпадают с этапами внедрения маркировки.

Пик скачивания из РБ (Республика Беларусь) в последние месяцы обусловлен тем, что программа была официально рекомендована оператором РБ для формирования pdf файлов с этикетками.

Согласно статистике самой программы за все время существования проекта с ее помощью распечатали приблизительно 184 000 000 этикеток и ввели в оборот около 210 000 000 единиц товара, а отгрузили почти 81 000 000 единиц товаров.

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

Цифры приблизительные, потому что статистику начали собирать уже после старта обязательной маркировки обуви. Кроме того, сгенерировать PDF — не значит его распечатать. К тому же у ряда пользователей программа находится за корпоративным фаерволом, поэтому статистика просто не доходит до сервера.

Выводы

Разрабатывать десктопные приложения на JavaFx можно: технология живая и достаточно удобная за счет разделения view и controller. В графическом редакторе можно набросать FXML-форму, потом прописать ей поведение в контроллере. Есть подключаемые библиотеки с дополнительными элементами форм. А еще можно написать свои элементы при необходимости. Есть возможность задействовать сторонние фреймворки типа Spring — он работает с современными версиями Java.

Подводя итог можно сказать следующее: если вы Java-разработчик и перед вами поставлена задача сделать утилитное десктопное приложение для внутреннего использования, то JavaFx — хороший выбор. Для написания небольших прикладных программ его более, чем достаточно.

Если же вам нужно сделать коммерческое решение с красивой визуальной картинкой, использующее особенности конкретной ОС (например, сворачивание в трей или показ всплывающих уведомлений), то скорее всего вам потребуется другое решение.

Спасибо, что дочитали, буду рад ответить на вопросы в комментариях.

Ссылка на проект, ссылка на исходники

Tags:
Hubs:
Total votes 48: ↑46 and ↓2+44
Comments48

Articles

Information

Website
tech.lamoda.ru
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия