Главная » Хабрахабр » [Из песочницы] И на улицу JavaFX тоже придет Spring

[Из песочницы] И на улицу JavaFX тоже придет Spring

Доброе время суток, хабровчане!

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

Ранее, на Swing или AWT все было очень печально, да наверное и до появления JavaFX 8 написание анонимных классов превращалось в спаггети код. Но с появлением лямбда-выражений все изменилось, код стал проще, понятней, красивее. Использовать JavaFX в своих проектах стало одним удовольствием.

Вот и возникла у меня мысль связать лучший инструмент для Java Spring Framework и удобный в наше время инструмент для создания GUI JavaFX, это даст нам использовать все возможности Spring`а в клиентском десктопном приложении. Собрав всю информацию воеидно, которую я искал по просторам сети, я решил поделиться ей. Прежде всего хочу отметить, что статья предназначена больше для новичков, поэтому некоторые подробности для многих могут оказаться слишком банальными и простыми, но я не хочу их опускать, чтобы не терять целостность статьи.

Жду конструктивной критики, по свои решениям.

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

Получается следующее.

Главная форма состоит из компонентов:

1. Button с текстом «Загрузить»
2. TableView c полями «ID», «Наименование», «Количество», «Цена»

Функционал

  1. При старте приложения в контексте будет создаваться bean DataSource и происходить подключение к БД. Данные для подключения находятся в файле конфигурации. Необходимо вывести 4 поля из таблицы Products.
  2. При нажатии на кнопку «Загрузить» TableView наполнится данными из таблице.
  3. При двойном клике на строку таблицы, откроется дополнительное окно со всеми полями Products.

Используемый стек:

JavaFX 8
Spring Boot
Spring JDBC
SQLite 3
IntelliJ IDEA Community Edition 2017

Создаем JavaFX проект

Создаем новый проект в IDEA, используя архетип Maven. Первоначальную структуру которую мы видим вполне стандартную для maven проекта:

SpringFXExample
├──.idea
├──src
│ ├──main
│ │ ├──java
│ │ └──resources
│ └──test
├──────pom.xml
└──────SpringFXExample.iml
External Libraries

Выставляем необходимый Language Level для модуля и проекта и изменяем Target bytecode version для нашего модуля в настройках Build, Execution, Deployment -> Compiler -> Java Compiler. В завимисомти от версии вашего JDK.

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

SpringFXExample
├──.idea
├──src
│ ├──main
│ │ ├──java
│ │ │ └──org.name
│ │ │ ├──app
│ │ │ │ ├──controller
│ │ │ │ │ ├──MainController.java
│ │ │ │ │ └──ProductTableController.java
│ │ │ │ └──Launcher.java
│ │ │ └──model
│ │ │ ├──dao
│ │ │ │ └─ProductDao.java
│ │ │ └──Product.java
│ │ └──resources
│ │ └──view
│ │ ├──fxml
│ │ │ ├──main.fxml
│ │ │ └──productTable.fxml
│ │ ├──style
│ │ └──image
│ └──test
├──────pom.xml
└──────SpringFXExample.iml
External Libraries

Создаем пакет org.name (или просто используете тот же значение как и в groupId) в директории java. Точка входа приложения, контроллеры, кастомные элементы и утилиты для интерфейса будут расположены в пакете app. Все остальное что касается непосредственно сущностей используемых в приложении в пакете model. В resources я создаю директорию view и храню *.fxml в папке fxml, *.css в папке style и изображение в папке image.

В FXML шаблоне main задаем шаблон внешнего вида приложения. Он будет включать в себя шаблон productTable, в котором задан внешний вид таблицы. MainController это наш главный котроллер и он будет пока с одним методом обработки нажатия кнопки загрузки. ProductTableController котроллер для таблицы. Launcher расширяем от Application и загружаем в методе start наш main.fxml обычным способом. Класс ProductDao оставим на потом. А вот Product напишем по концепции JavaBean.

Переходим к содержимому файлов:

main.fxml

<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.control.Button?>
<AnchorPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="org.name.app.controller.MainController" prefHeight="400.0" prefWidth="400.0"> <Button fx:id="load" text="Загрузить" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="10" onMouseClicked="#onClickLoad"/> <!-- TableView будет подключаться из другого fxml шаблона --> <fx:include AnchorPane.topAnchor="40" AnchorPane.leftAnchor="10" AnchorPane.bottomAnchor="10" source="productTable.fxml"/>
</AnchorPane>

productTable.fxml

<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?> <TableView fx:id="productTable" prefWidth="350.0" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.name.app.controller.ProductTableController"> <columns> <TableColumn fx:id="id" prefWidth="30.0" text="ID"/> <TableColumn fx:id="name" prefWidth="200.0" text="Наименование"/> <TableColumn fx:id="quantity" prefWidth="50.0" text="Кол-во"/> <TableColumn fx:id="price" prefWidth="50.0" text="Цена"/> </columns>
</TableView>

MainController.java

package org.name.app.controller; import javafx.fxml.FXML;
import javafx.scene.control.Button; public class MainController { @FXML private Button load; /** * Обработка нажатия кнопки загрузки товаров */ @FXML public void onClickLoad() { System.out.println("Загружаем..."); // TODO: Реализовать получение данный из БД с помощью DAO класса // TODO: и передать полученный данные в таблицу для отображения }
}

ProductTableController.java

package org.name.app.controller; import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import org.name.model.Product; import java.util.List; public class ProductTableController { @FXML private TableColumn<Integer, Product> id; @FXML private TableColumn<String, Product> name; @FXML private TableColumn<Integer, Product> quantity; @FXML private TableColumn<String, Product> price; @FXML private TableView<Product> productTable; /** * Устанавливаем value factory для полей таблицы */ public void initialize() { id.setCellValueFactory(new PropertyValueFactory<>("id")); name.setCellValueFactory(new PropertyValueFactory<>("name")); quantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); price.setCellValueFactory(new PropertyValueFactory<>("price")); } /** * Заполняем таблицу данными из БД * @param products список продуктов */ public void fillTable(List<Product> products) { productTable.setItems(FXCollections.observableArrayList(products)); }
}

Launcher.java

package org.name.app; import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage; public class Launcher extends Application { public static void main(String[] args) { launch(args); } public void start(Stage stage) throws Exception { Parent root = FXMLLoader.load(getClass() .getResource("/view/fxml/main.fxml")); stage.setTitle("JavaFX Maven Spring"); stage.setScene(new Scene(root)); stage.show(); }
}

Product.java

package org.name.model; public class Product { private int id; private String name; private int quantity; private String price; private String guid; private int tax; public Product() { } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } public String getPrice() { return price; } public void setPrice(String price) { this.price = price; } public String getGuid() { return guid; } public void setGuid(String guid) { this.guid = guid; } public int getTax() { return tax; } public void setTax(int tax) { this.tax = tax; }
}

Запускаем, чтобы убедится, что все работает.

Первая сборка

Пробуем собрать JAR с помощью maven package. Добавив в наш pom.xml следующую конфигурацию (В проекте у меня Java 9, но это не значит что я использую все ее возможности, просто для новых проектов выбираю самые свежие инструменты):

<properties> <maven.compiler.source>9</maven.compiler.source> <maven.compiler.target>9</maven.compiler.target>
</properties>

и maven-jar-plugin:

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>org.name.app.Launcher</mainClass> </manifest> </archive> </configuration> </plugin> </plugins>
</build>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.name</groupId> <artifactId>SpringFXExample</artifactId> <version>1.0</version> <properties> <maven.compiler.source>9</maven.compiler.source> <maven.compiler.target>9</maven.compiler.target> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>org.name.app.Launcher</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build> </project>

Пробуем запустить получившийся jar-ник, если у вас должным образом настроены переменные среды:

start java -jar target\SpringFXExample-1.0.jar

Или с помощью run.bat со следующим содержанием:

set JAVA_HOME=PATH_TO_JDK\bin
set JAVA_CMD=%JAVA_HOME%\java start %JAVA_CMD% -jar target\SpringFXExample-1.0.jar

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

Кстати, чтобы скрыть терминал вызываем не java, а javaw просто для текущего случая нам необходимо было проверить вывод текста при нажатии на кнопку.

Добавляем Spring Boot

Теперь пришло время для Spring, а именно создадим application-context.xml в resources и напишем немного измененный загрузчик сцен. Сразу отмечу, что идея загрузчика Spring для JavaFX не моя, я уже встречал такое на просторах сети. Но я немного ее переосмыслил.

Редактируем для начала наш pom.xml. Добавляем версию Spring

<properties> <maven.compiler.source>9</maven.compiler.source> <maven.compiler.target>9</maven.compiler.target> <spring.version>5.0.3.RELEASE</spring.version>
</properties>

и зависимости spring-context, spring-jdbc и sqlite-jdbc.

<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.7.2</version> </dependency>
</dependencies>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.name</groupId> <artifactId>SpringFXExample</artifactId> <version>1.0</version> <properties> <maven.compiler.source>9</maven.compiler.source> <maven.compiler.target>9</maven.compiler.target> <spring.version>5.0.3.RELEASE</spring.version> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>org.name.app.Launcher</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.7.2</version> </dependency> </dependencies> </project>

Создаем файл конфигурации config.properties. Он содержит следующие данные:

#Заголовок главной сцены
title=JavaFX & Spring Boot!
#Конфигурация подключения к БД
db.url=jdbc:sqlite:PATH_TO_DB/test_db
db.user=user
db.password=password
db.driver=org.sqlite.JDBC

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

application-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="file:config.properties" ignore-unresolvable="true"/> <context:component-scan base-package="org.name"/> <bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="url" value="${db.url}"/> <property name="driverClassName" value="${db.driver}"/> <property name="username" value="${db.user}"/> <property name="password" value="${db.password}"/> </bean>
</beans>

Напишем абстрактный контроллер Controller который расширяет интерфейс ApplicationContextAware, чтобы мы могли получать контекст из любого контроллера.

Controller.java

package org.name.app.controller; import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; public abstract class Controller implements ApplicationContextAware { private ApplicationContext context; public ApplicationContext getContext() { return context; } @Override public void setApplicationContext(ApplicationContext context) throws BeansException { this.context = context; }
}

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

SpringStageLoader.java

package org.name.app; import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component; import java.io.IOException; @Component
public class SpringStageLoader implements ApplicationContextAware { private static ApplicationContext staticContext; //инъекция заголовка главного окна @Value("${title}") private String appTitle; private static String staticTitle; private static final String FXML_DIR = "/view/fxml/"; private static final String MAIN_STAGE = "main"; /** * Загрузка корневого узла и его дочерних элементов из fxml шаблона * @param fxmlName наименование *.fxml файла в ресурсах * @return объект типа Parent * @throws IOException бросает исключение ввода-вывода */ private static Parent load(String fxmlName) throws IOException { FXMLLoader loader = new FXMLLoader(); // setLocation необходим для корректной загрузки включенных шаблонов, таких как productTable.fxml, // без этого получим исключение javafx.fxml.LoadException: Base location is undefined. loader.setLocation(SpringStageLoader.class.getResource(FXML_DIR + fxmlName + ".fxml")); // setLocation необходим для корректной того чтобы loader видел наши кастомные котнролы loader.setClassLoader(SpringStageLoader.class.getClassLoader()); loader.setControllerFactory(staticContext::getBean); return loader.load(SpringStageLoader.class.getResourceAsStream(FXML_DIR + fxmlName + ".fxml")); } /** * Реализуем загрузку главной сцены. На закрытие сцены стоит обработчик, которых выходит из приложения * @return главную сцену * @throws IOException бросает исключение ввода-вывода */ public static Stage loadMain() throws IOException { Stage stage = new Stage(); stage.setScene(new Scene(load(MAIN_STAGE))); stage.setOnHidden(event -> Platform.exit()); stage.setTitle(staticTitle); return stage; } /** * Передаем данные в статические поля в реализации метода интерфейса ApplicationContextAware, т.к. методы их использующие тоже статические */ @Override public void setApplicationContext(ApplicationContext context) throws BeansException { SpringStageLoader.staticContext = context; SpringStageLoader.staticTitle = appTitle; }
}

Немного переписываем метод start в классе Launcher. А так же добавляем инициализацию контекста и его освобождение.

Launcher.java

package org.name.app; import javafx.application.Application;
import javafx.stage.Stage;
import org.springframework.context.support.ClassPathXmlApplicationContext; import java.io.IOException; public class Launcher extends Application { private static ClassPathXmlApplicationContext context; public static void main(String[] args) { launch(args); } /** * Инициализируем контекст */ @Override public void init() { context = new ClassPathXmlApplicationContext("application-context.xml"); } @Override public void start(Stage stage) throws IOException { SpringStageLoader.loadMain().show(); } /** * Освобождаем контекст */ @Override public void stop() throws IOException { context.close(); }
}

Не забываем унаследовать класс MainController от Controller и всем контроллерам добавить аннотацию Component, это позволит добавить их в контекст через component-scan и получать любые контроллеры из контекста, как бины, или инжектить их. Иначе получим исключение

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.name.app.controller.MainController' available

Запускаем и видим что текст заголовок окна стал таким который мы прописали в property:

Но загрузка данных у нас еще не реазилована как и отображение подробной информации о продукте.

Реализуем класс ProductDao

ProductDao.java

package org.name.model.dao; import org.name.model.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import javax.sql.DataSource;
import java.util.List; @Component
public class ProductDao { private JdbcTemplate template; /** * Инжектим dataSource и создаем объект JdbcTemplate */ @Autowired public ProductDao(DataSource dataSource) { this.template = new JdbcTemplate(dataSource); } /** * Получаем весь список продуктов из таблицы. Т.к. класс Product построен на концепции JavaBean * мы можем воспользоваться классом BeanPropertyRowMapper. */ public List<Product> getAllProducts(){ String sql = "SELECT * FROM product"; return template.query(sql, new BeanPropertyRowMapper<>(Product.class)); }
}

Теперь осталось дописать пару строк в главном контроллере, чтобы при нажатии на кнопку у нас данные загружались в таблицу

MainController.java

package org.name.app.controller; import javafx.fxml.FXML;
import javafx.scene.control.Button;
import org.name.model.dao.ProductDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; @Component
public class MainController extends Controller { @FXML private Button load; private ProductTableController tableController; private ProductDao productDao; @Autowired public MainController(ProductTableController tableController, ProductDao productDao) { this.tableController = tableController; this.productDao = productDao; } /** * Обработка нажатия кнопки загрузки товаров */ @FXML public void onClickLoad() { tableController.fillTable(productDao.getAllProducts()); load.setDisable(true); }
}

и реализовать открытие нового окна с деталями продукта. Для этого используем шаблон productDetails и сцену ProductDetailsModalStage.

productDetails.fxml

<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1"> <children> <GridPane> <columnConstraints> <ColumnConstraints prefWidth="150.0"/> <ColumnConstraints prefWidth="300.0"/> </columnConstraints> <rowConstraints> <RowConstraints prefHeight="30.0"/> <RowConstraints prefHeight="30.0"/> <RowConstraints prefHeight="30.0"/> <RowConstraints prefHeight="30.0"/> <RowConstraints prefHeight="30.0"/> <RowConstraints prefHeight="30.0"/> </rowConstraints> <Label fx:id="name" style="-fx-font-weight: bold;-fx-padding: 3px;" prefWidth="450" GridPane.columnSpan="2" alignment="CENTER"/> <Label style="-fx-font-weight: bold; -fx-padding: 3px;" GridPane.rowIndex="1" text="ГУИД:"/> <Label fx:id="guid" style="-fx-padding: 3px;" GridPane.rowIndex="1" GridPane.columnIndex="1"/> <Label style="-fx-font-weight: bold; -fx-padding: 3px;" GridPane.rowIndex="2" text="Количество на складе:"/> <Label fx:id="quantity" style="-fx-padding: 3px;" GridPane.rowIndex="2" GridPane.columnIndex="1"/> <Label style="-fx-font-weight: bold; -fx-padding: 3px;" GridPane.rowIndex="3" text="Цена:"/> <Label fx:id="price" style="-fx-padding: 3px;" GridPane.rowIndex="3" GridPane.columnIndex="1"/> <Label style="-fx-font-weight: bold; -fx-padding: 3px;" GridPane.rowIndex="4" text="Общая стоимость:"/> <Label fx:id="costOfAll" style="-fx-padding: 3px;" GridPane.rowIndex="4" GridPane.columnIndex="1"/> <Label style="-fx-font-weight: bold; -fx-padding: 3px;" GridPane.rowIndex="5" text="Налог:"/> <Label fx:id="tax" style="-fx-padding: 3px;" GridPane.rowIndex="5" GridPane.columnIndex="1"/> </GridPane> </children>
</AnchorPane> 

ProductDetailsModalStage.java

package org.name.app.controller; import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.name.app.SpringStageLoader;
import org.name.model.Product; import java.io.IOException; public class ProductDetailsModalStage extends Stage { private Label name; private Label guid; private Label quantity; private Label price; private Label costOfAll; private Label tax; public ProductDetailsModalStage() { this.initModality(Modality.WINDOW_MODAL); this.centerOnScreen(); try { Scene scene = SpringStageLoader.loadScene("productDetails"); this.setScene(scene); name = (Label) scene.lookup("#name"); guid = (Label) scene.lookup("#guid"); quantity = (Label) scene.lookup("#quantity"); price = (Label) scene.lookup("#price"); costOfAll = (Label) scene.lookup("#costOfAll"); tax = (Label) scene.lookup("#tax"); } catch (IOException e) { e.printStackTrace(); } } public void showDetails(Product product) { name.setText(product.getName()); guid.setText(product.getGuid()); quantity.setText(String.valueOf(product.getQuantity())); price.setText(product.getPrice()); costOfAll.setText("$" + getCostOfAll(product)); tax.setText(String.valueOf(product.getTax()) + " %"); setTitle("Детали продукта: " + product.getName()); show(); } private String getCostOfAll(Product product) { int quantity = product.getQuantity(); double priceOfOne = Double.parseDouble(product .getPrice() .replace("$", "")); return String.valueOf(quantity * priceOfOne); }
}

В SpringStageLoader допишем еще один метод:

public static Scene loadScene(String fxmlName) throws IOException { return new Scene(load(fxmlName));
}

а в метод инициализации ProductTableController добавить несколько строчек:

productTable.setRowFactory(rf -> { TableRow<Product> row = new TableRow<>(); row.setOnMouseClicked(event -> { if (event.getClickCount() == 2 && (!row.isEmpty())) { ProductDetailsModalStage stage = new ProductDetailsModalStage(); stage.showDetails(row.getItem()); } }); return row;
});

Запускаем и видим результат:

Проблема долгой инициализация контекста

А вот еще одна интересная тема. Предположим что ваш контекст долго инициализируется, в этом случае, пользователь не поймет идет ли запуск приложения или нет. Поэтому для наглядности необходимо добавить заставку, во время инициализации контекста.
Сцену с заставкой будем писать обычным способом через FXMLLoader. Т.к. контекст как раз в этом время будет инициализироваться. Инициализацию тяжелого контекста сымитируем вызовом Thread.sleep(10000);

Шаблон с картинкой:

splash.fxml

<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx" mouseTransparent="true"> <ImageView> <Image url="@/view/image/splash.png"/> </ImageView>
</AnchorPane>

Измененный Launcher для загрузки приложения с заставкой

Launcher.java

package org.name.app; import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.springframework.context.support.ClassPathXmlApplicationContext; import java.io.IOException; public class Launcher extends Application { private static ClassPathXmlApplicationContext context; private Stage splashScreen; public static void main(String[] args) { launch(args); } /** * Контекст инициализируется не в UI потоке. Поэтому в методе init() UI поток вызывается через Platform.runLater() * @throws Exception */ @Override public void init() throws Exception { Platform.runLater(this::showSplash); Thread.sleep(10000); context = new ClassPathXmlApplicationContext("application-context.xml"); Platform.runLater(this::closeSplash); } @Override public void start(Stage stage) throws IOException { SpringStageLoader.loadMain().show(); } /** * Освобождаем контекст */ @Override public void stop() { context.close(); } /** * Загружаем заставку обычным способом. Выставляем везде прозрачность */ private void showSplash() { try { splashScreen = new Stage(StageStyle.TRANSPARENT); splashScreen.setTitle("Splash"); Parent root = FXMLLoader.load(getClass().getResource("/view/fxml/splash.fxml")); Scene scene = new Scene(root, Color.TRANSPARENT); splashScreen.setScene(scene); splashScreen.show(); } catch (IOException e) { e.printStackTrace(); } } /** * Закрывает сцену с заставкой */ private void closeSplash() { splashScreen.close(); }
}

Собираем, запускаем и получаем то что хотели:

Launch App GIF

Окончательная сборка JAR

Остался последний шаг. Это собрать JAR, но уже со Spring`ом. Для этого необходимо добавить в pom еще один плагин maven-shade-plugin:

pom.xml - окончательная версия

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.name</groupId> <artifactId>SpringFXExample</artifactId> <version>1.0</version> <properties> <maven.compiler.source>9</maven.compiler.source> <maven.compiler.target>9</maven.compiler.target> <spring.version>5.0.3.RELEASE</spring.version> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>org.name.app.Launcher</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.handlers</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.schemas</resource> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.7.2</version> </dependency> </dependencies> </project>

Вот таким вот простым способом можно подружить Spring и JavaFX. Окончательная структура проекта:

SpringFXExample
├──.idea
├──src
│ ├──main
│ │ ├──java
│ │ │ └──org.name
│ │ │ ├──app
│ │ │ │ ├──controller
│ │ │ │ │ ├──Controller.java
│ │ │ │ │ ├──MainController.java
│ │ │ │ │ ├──ProductTableController.java
│ │ │ │ │ └──ProductDetailsModalStage.java
│ │ │ │ ├──Launcher.java
│ │ │ │ └──SpringStageLoader.java
│ │ │ └──model
│ │ │ ├──dao
│ │ │ │ └─ProductDao.java
│ │ │ └──Product.java
│ │ └──resources
│ │ ├──view
│ │ │ ├──fxml
│ │ │ │ ├──main.fxml
│ │ │ │ ├──productDetails.fxml
│ │ │ │ ├──productTable.fxml
│ │ │ │ └──splash.fxml
│ │ │ ├──style
│ │ │ └──image
│ │ │ └──splash.png
│ │ └──application-context.xml
│ └──test
├──────config.properties.xml
├──────pom.xml
├──────SpringFXExample.iml
└──────test-db.xml
External Libraries

Исходники на GitHub. Там же файл PRODUCTS.sql для таблицы в БД.


x

Ещё Hi-Tech Интересное!

Как Apple выходит из бесконечного цикла. Истории старого кампуса

Фото: Michelle Groskopf, Wired Все становится лучше, больше, быстрее, сверкающее. Я странно чувствую себя, смотря каждый год презентацию новых айфонов. Но в то же время будто бы укачивает от слишком разогнанного обмена веществ. Просто время от времени старые фанаты роняют ...

Чистота в мастерской гика. Часть 2

Или пылесос с управлением по Bluetooth В первой части я рассказал про пылесосы и циклоны для уборки стружки в мастерской. В комментах поделились опытом использования разных пылесосов хорошим и не очень. Теперь расскажу про то, как я делаю прототипы плат ...