Итак, сейчас приложение использует javafx, настроено логирование (logback, sl4j) и все это собирается в кастомный jre и затем упаковывается в инсталятор с помощью Jpackage.
Продолжаем усложнять проект и зависимости.
Lombok
Так или иначе препроцессор Lombok понадобится в проекте в любом случае, вопрос в том, как jlink его обработает, ведь jlink не любит автоматические модули, а Lombok не является классическим модульным jar. К тому же среда разработки просто не скомпилирует проект если не увидит классы Lombok.
Обо всем по подробнее.
Добавим простой класс Task, на будущее, т.к. проект будем развивать в Todo-list приложение.

Все красное, среда разработки не может найти эти классы. Причем даже если добавить зависимость в pom.xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>compile</scope>
</dependency>
scope = compile здесь не просто так, поскольку препроцессор работает до непосредственной сборки приложения и не нужен в режиме runtime.
Более того, среде разработки дополнительно нужно дать указание использовать эти препроцессоры.

А также нужно проверить, что плагин Lombok также установлен и включен.

Смотрим в Task, а он все равно красный.

Вспоминаем, что проект у нас модульный, а значит не хватает записи в файле module-info.java. Добавим туда то что нужно.

И, наконец-то, класс Task стал нормальным, среда разработки нашла все что нужно и теперь с радостью соберет нам наш проект.
mvn clean install
И как бы странно это не звучало, все прекрасно собралось. Шутка.
Все конечно же пошло не по плану. и сборка завершилась с ошибкой.

Странным образом не нашлись именно те методы, которые должен был (по идее) создать lombok, но он этого не сделал. То ли лыжи не едут…
Для решения этой проблемы, нужно напрямую сказать maven-compiler-plugin, чтобы он использовал lombok при компиляции.

Теперь то проект собирается и упаковывается как надо.
Однако среда разработки ненавязчиво что-то намекает.

Вроде как это означает, что в этом файле можно и не указывать про Lombok, что он очень уж нужен и можно удалить. Но что бывает, если его не указать в module-info, мы уже видели. И что делать?
Можно оставить как есть, собралось же и запускается.
Но лучше все таки сделать все правильно. Поскольку этот модуль не нужен для запуска, но должен быть во время компиляции, ему нужно добавить слово static.
requires static lombok;
Такое же пояснение среда разработки показывает и для модулей javafx.controls и javafx.fxml. Это происходит потому, что эти модули указаны в зависимостях в pom.xml а так же они присутствуют и в jdk 17,0,14, которую использует проект. В настройках jlink к папке jmods этой jdk мы указали путь, равно как и к папке target/libs, куда maven-compiler-plugin копирует все зависимости проекта, глядя на секцию dependencies. Получается, что этих модулей доступно по два штуки.

Можно смело удалить зависимости на эти модули из pom.xml
mvn clean install
Все! Выдыхаем, jlink сожрал Lombok и можно двигаться дальше.
Sqlite+Hibernate 6
Ни одно приложение хоть с какой-либо сложностью, хотя бы с минимальной полезной сложностью не сможет обойтись без слоя работы с базой данных.
Поэтому добавляем наипростейший вариант файловой базы данных и Hibernate 6 как способ работы с ней из кода приложения.
Сначала добавляем зависимости в pom.xml:
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.50.2.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
<version>6.6.20.Final</version>
</dependency>
И конфигурационный файл hibernate.cfg.xml для Hibernate:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- JDBC настройки -->
<property name="connection.driver_class">org.sqlite.JDBC</property>
<property name="connection.url">
jdbc:sqlite:${user.home}/AppData/Local/myJavaFxApp/db/myapp.db
</property>
<!-- Dialect -->
<property name="dialect">org.hibernate.community.dialect.SQLiteDialect</property>
<!-- Логирование SQL -->
<property name="show_sql">true</property>
<property name="format_sql">true</property>
<!-- Автоматическое создание таблицы при первом запуске -->
<property name="hbm2ddl.auto">update</property>
<!-- Entity-классы -->
<mapping class="ru.makushimo.javafxexe.Task"/>
</session-factory>
</hibernate-configuration>
Чтобы этот конфиг сработал как надо, нужно превратить класс Task в entity-класс:

Как обычно, ничего сразу не идет как надо. По опыту предыдущих аналогичных ситуаций догадываемся, что надо добавить что-то в module-info.java:
requires jakarta.persistence;
Хорошо, теперь все импорты были найдены и класс Task выглядит живым.

Теперь нужно добавить код инициализации файла базы данных там, где его будет искать Hibernate. Для этого в main класс добавим код аналогичный тому, что был сделан для файла логов:

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

И по привычке добавляем в module-info.java:
requires org.hibernate.orm.core;
Импорты все перестали быть красными. Дальше нужно вызвать метод создания сессии Hibernate.
Пока ради теста делаем это прямо в main классе:

Пробуем запустить приложение из среды разработки, чтобы проверить, что в логах появилась нужная запись, файл базы данных был создан, а в нем и первая таблица ‘tasks’. Однако получаем ошибку
java.lang.reflect.InaccessibleObjectException: Unable to make field java.lang.Integer ru.makushimo.javafxexe.Task.id accessible: module ru.makushimo.javafxexe does not "opens ru.makushimo.javafxexe" to module org.hibernate.orm.core
Тут уже сама ошибка говорит, что нужно сделать для исправления. Нужно открыть модуль приложения для модуля hibernate:
opens ru.makushimo.javafxexe to javafx.fxml, org.hibernate.orm.core;
Запускаем еще раз и в файле логов в AppData/local/…/app.log появилась запись
INFO r.m.javafxexe.HelloApplication - Hibernate session opened
А рядом был создан файл базы данных. Если к нему подключиться, то там должна быть таблица ‘tasks’.

Раз все работает, проверим как работает сборка приложения и установщика.
mvn clean install
И операция exec jlink свалилась с ошибкой
Error: automatic module cannot be used with jlink: org.jboss.jandex from file:///C:javafx-exe/target/libs/jandex.jar
Эта зависимость прилетела к нам транзитивно из hibernate-community-dialects и была скачана в target/libs согласно настройкам maven-dependency-plugin

Jlink не может работать с не модульными зависимостями. Даже автоматические модули не работают. Это такие зависимости, которые в манифесте содержат поле Authomatic-module-name. Этого достаточно для программирования, но с ними невозможно собрать кастомный jre для поставки приложения без установки JDK/JRE нужной версии.
Из этой точки имеется два выхода:
- Ручками создавать module-info.java для каждой не модульной зависимости (а это не легко, как показала практика) и затем патчить jar зависимости, вставляя в него этот скомпилированный файл. Тут конечно можно немного автоматизировать, написав скрипт и запускать его например через maven-exec-plugin, но дескрипторы модуля все равно писать руками. Этот путь также ставит крест на использовании spring в приложении на javafx.
- Сделать приложение не модульным (удалить module-info.java) и собирать образ приложения с помощью jpackage только. Например, с помощью jpackage-maven-plugin. Jpackage может сам собрать jre с помощью jlink при этом использовать он будет только те модули из JDK, которые нужны для работы приложения. Зависимости же будут складываться кучей в папку app, а запускатель эти зависимости будет подключать в classpath с главным классом приложения. В итоге получим, что хотели, приложение без необходимости устанавливать Java нужной версии.
Хотя путь 1 и более модерновый (типа), объем гемора с ним связанный отпугивает. Поэтому я буду пробовать путь 2.
Шаг назад: снова lombok.
Для лучшего понимания развития проекта вернусь ка я к шагу где настраивал Lombok и про Hibernate еще и мысли нет.
- Удаляю module-info.java совсем, теперь приложение не модульное.
- Возвращаю зависимости javafx
- Удаляю из pom.xml полностью плагин exec-maven-plugin, который двумя goals последовательно выполнял jlink и jpackage.
- Добавляю плагин org.panteleyev: jpackage-maven-plugin последней на текущий момент версии 1.66 и настраиваю его. Причем использую режим сборки не модульного приложения.

Использование параметров mainClass и mainJar переводит плагин на режим сборки не-модульного приложения. В этом режиме он соберет jre только на основе modulePath, в который я указал ему путь к модулям нужной java, а файлы зависимостей и файл приложения он соберет отдельно в папку app и пропишет добавление их в classpath при запуске приложения в exe файл. В этом различие от предыдущего подхода где jlink добавлял в jre также и модули зависимостей.
Отладка
Для удобства разработки я создам два профиля сборки:
- DEV — приложение будет собираться как уже установленное (app-image) и с включенными настройками отладки. Так мне не придется постоянно сносить и заново устанавливать новую сборку на компьютере. А jpwd позволяет подключаться к процессу запущенного приложения для отладки.
- PROD — приложение будет собираться в инсталятор без режима отладки.


С такими настройками dev профиля запуск приложения из myJavaFxApp.exe будет ожидать подключения отладочного процесса (suspend=y).

Добавляем конфигурацию удаленной отладки в IntelliJ IDEA

Теперь для для отладки собранного и запущенного образа приложения порядок действий такой:
- Расставляем точки останова в коде
- Запускаем myJavaFxApp.exe
- Запускаем процесс отладки в IDEA
Отлаживаем как обычно.
При этом в процессе именно разработки приложения обычный запуск main класса в режиме debug также остается.
Hibernate + sqlite
А вот теперь попытка №2 добавить в приложение поддержку работы с базой данных.
Делаем все изменения как и в первый раз за исключением разве что module-info.java, его теперь нет в проекте.
Проверяем — приложение собирается и затем еще и запускается. Фантастика. Оно живое.
To-do list
Усложняем приложение. Добавим на форму таблицу со списком дел и обычным набором CRUD операций.
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.*;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import org.hibernate.Session;
import org.hibernate.Transaction;
public class TaskController {
@FXML private TableView<Task> taskTable;
@FXML private TableColumn<Task, String> nameColumn;
@FXML private TableColumn<Task, Boolean> doneColumn;
@FXML private TextField taskInput;
private ObservableList<Task> tasks = FXCollections.observableArrayList();
@FXML
public void initialize() {
nameColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
doneColumn.setCellValueFactory(cellData -> new SimpleBooleanProperty(cellData.getValue().isDone()));
taskTable.setItems(tasks);
loadTasks();
}
private void loadTasks() {
tasks.clear();
try (Session session = HibernateUtil.getSessionFactory().openSession()) {
tasks.addAll(session.createQuery("from Task", Task.class).list());
}
}
@FXML
private void handleAdd() {
String name = taskInput.getText();
if (name.isEmpty()) {
return;
};
Task task = new Task();
task.setName(name);
task.setDone(false);
try (Session session = HibernateUtil.getSessionFactory().openSession()) {
Transaction tx = session.beginTransaction();
session.persist(task);
tx.commit();
}
taskInput.clear();
loadTasks();
}
@FXML
private void handleDelete() {
Task selected = taskTable.getSelectionModel().getSelectedItem();
if (selected == null) {
return;
};
try (Session session = HibernateUtil.getSessionFactory().openSession()) {
Transaction tx = session.beginTransaction();
session.remove(selected);
tx.commit();
}
loadTasks();
}
@FXML
private void handleToggle() {
Task selected = taskTable.getSelectionModel().getSelectedItem();
if (selected == null) {
return;
};
selected.setDone(!selected.isDone());
try (Session session = HibernateUtil.getSessionFactory().openSession()) {
Transaction tx = session.beginTransaction();
session.merge(selected);
tx.commit();
}
loadTasks();
}
}
FXML файл для формы списка заданий
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx/17"
xmlns:fx="http://javafx.com/fxml"
fx:controller="ru.makushimo.javafxexe.TaskController">
<center>
<TableView fx:id="taskTable">
<columns>
<TableColumn text="Name" fx:id="nameColumn"/>
<TableColumn text="Done" fx:id="doneColumn"/>
</columns>
</TableView>
</center>
<bottom>
<HBox spacing="5" alignment="CENTER">
<TextField fx:id="taskInput" promptText="New Task"/>
<Button text="Add" onAction="#handleAdd"/>
<Button text="Delete" onAction="#handleDelete"/>
<Button text="Toggle Done" onAction="#handleToggle"/>
</HBox>
</bottom>
</BorderPane>
Запускаем и приложение выглядит вот так

На этом все. Исследование этого варианта сборки exe приложения закончено.
Код можно найти тут.