Надеюсь среди Вас найдутся такие же любители делать формочки как и я.
Дело в том, что я всегда был приверженцем дружелюбных интерфейсов. Меня расстраивали приложения, которые мало ориентированны на пользователей, такое особенно бывает в корпоративной разработке. И зачастую клиентские приложения написанные на Java это черные окошки, а к приложениям c GUI относятся со скептицизмом.
Ранее, на Swing или AWT все было очень печально, да наверное и до появления JavaFX 8 написание анонимных классов превращалось в спаггети код. Но с появлением лямбда-выражений все изменилось, код стал проще, понятней, красивее. Использовать JavaFX в своих проектах стало одним удовольствием.
Вот и возникла у меня мысль связать лучший инструмент для Java Spring Framework и удобный в наше время инструмент для создания GUI JavaFX, это даст нам использовать все возможности Spring`а в клиентском десктопном приложении. Собрав всю информацию воеидно, которую я искал по просторам сети, я решил поделиться ей. Прежде всего хочу отметить, что статья предназначена больше для новичков, поэтому некоторые подробности для многих могут оказаться слишком банальными и простыми, но я не хочу их опускать, чтобы не терять целостность статьи.
Жду конструктивной критики, по свои решениям.
Кому интересно, прошу под кат.
Попробуем написать небольшое приложение. Предположим, что есть такое примитивное задание: необходимо написать приложение которое будет загружать из БД данные о продуктах в таблицу на форме, а при клике на каждую строку таблицы открывать дополнительное окно с более подробными данными о продукте. Для наполнения базы данных воспользуемся сервисом. Я сгенерировал фейковые данные для таблицы с продуктами и успешно заполнил ими БД.
Получается следующее.
Главная форма состоит из компонентов:
1. Button с текстом «Загрузить»
2. TableView c полями «ID», «Наименование», «Количество», «Цена»
Функционал
- При старте приложения в контексте будет создаваться bean DataSource и происходить подключение к БД. Данные для подключения находятся в файле конфигурации. Необходимо вывести 4 поля из таблицы Products.
- При нажатии на кнопку «Загрузить» TableView наполнится данными из таблице.
- При двойном клике на строку таблицы, откроется дополнительное окно со всеми полями Products.
Используемый стек:
JavaFX 8
Spring JDBC
SQLite 3
IntelliJ IDEA Community Edition 2017
Создаем JavaFX проект
Создаем новый проект в IDEA, используя архетип Maven. Первоначальную структуру которую мы видим вполне стандартную для maven проекта:
SpringFXExample
+--.idea
+--src
¦ +--main
¦ ¦ +--java
¦ ¦ L--resources
¦ L--test
+------pom.xml
L------SpringFXExample.iml
External Libraries
Выставляем необходимый Language Level для модуля и проекта и изменяем Target bytecode version для нашего модуля в настройках Build, Execution, Deployment -> Compiler -> Java Compiler. В завимисомти от версии вашего JDK.
Теперь необходимо превратить то что получилось, в приложение на JavaFX. Структуру проекта которую я хочу получить привожу ниже, она не претендует на идеал.
SpringFXExample
+--.idea
+--src
¦ +--main
¦ ¦ +--java
¦ ¦ ¦ L--org.name
¦ ¦ ¦ +--app
¦ ¦ ¦ ¦ +--controller
¦ ¦ ¦ ¦ ¦ +--MainController.java
¦ ¦ ¦ ¦ ¦ L--ProductTableController.java
¦ ¦ ¦ ¦ L--Launcher.java
¦ ¦ ¦ L--model
¦ ¦ ¦ +--dao
¦ ¦ ¦ ¦ L-ProductDao.java
¦ ¦ ¦ L--Product.java
¦ ¦ L--resources
¦ ¦ L--view
¦ ¦ +--fxml
¦ ¦ ¦ +--main.fxml
¦ ¦ ¦ L--productTable.fxml
¦ ¦ +--style
¦ ¦ L--image
¦ L--test
+------pom.xml
L------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.
Переходим к содержимому файлов:
<?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>
<?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>
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: и передать полученный данные в таблицу для отображения
}
}
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));
}
}
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();
}
}
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>
<?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
Теперь пришло время для 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>
<?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 в ресурсы со следующим содержанием, если вы хоть немного знакомы со спрингом, то думаю в вас не возникнет проблем в понимании написанного ниже.
<?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, чтобы мы могли получать контекст из любого контроллера.
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. Он будет больше похож на утилитный класс, в котором можно реализовать загрузку различных сцен и окон, поэтому он у меня сразу получился таким объемным.
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. А так же добавляем инициализацию контекста и его освобождение.
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
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));
}
}
Теперь осталось дописать пару строк в главном контроллере, чтобы при нажатии на кнопку у нас данные загружались в таблицу
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.
<?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>
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);
Шаблон с картинкой:
<?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 для загрузки приложения с заставкой
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();
}
}
Собираем, запускаем и получаем то что хотели:
Окончательная сборка JAR
Остался последний шаг. Это собрать JAR, но уже со Spring`ом. Для этого необходимо добавить в pom еще один плагин maven-shade-plugin:
<?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
¦ ¦ ¦ L--org.name
¦ ¦ ¦ +--app
¦ ¦ ¦ ¦ +--controller
¦ ¦ ¦ ¦ ¦ +--Controller.java
¦ ¦ ¦ ¦ ¦ +--MainController.java
¦ ¦ ¦ ¦ ¦ +--ProductTableController.java
¦ ¦ ¦ ¦ ¦ L--ProductDetailsModalStage.java
¦ ¦ ¦ ¦ +--Launcher.java
¦ ¦ ¦ ¦ L--SpringStageLoader.java
¦ ¦ ¦ L--model
¦ ¦ ¦ +--dao
¦ ¦ ¦ ¦ L-ProductDao.java
¦ ¦ ¦ L--Product.java
¦ ¦ L--resources
¦ ¦ +--view
¦ ¦ ¦ +--fxml
¦ ¦ ¦ ¦ +--main.fxml
¦ ¦ ¦ ¦ +--productDetails.fxml
¦ ¦ ¦ ¦ +--productTable.fxml
¦ ¦ ¦ ¦ L--splash.fxml
¦ ¦ ¦ +--style
¦ ¦ ¦ L--image
¦ ¦ ¦ L--splash.png
¦ ¦ L--application-context.xml
¦ L--test
+------config.properties.xml
+------pom.xml
+------SpringFXExample.iml
L------test-db.xml
External Libraries
Исходники на GitHub. Там же файл PRODUCTS.sql для таблицы в БД.
Комментарии (23)
PloadyFree
12.02.2018 06:28Может, я невнимательно смотрел, но где здесь Spring Boot?
ShinRa Автор
12.02.2018 06:29Спасибо, мой косяк, как говорится, ляпнул не подумав. Исправил дабы не вводить людей в заблуждение
tamsus
12.02.2018 06:59Ну для начала у Вас не Spring Boot, а обычный Spring с xml конфигурациями. Boot позволяет как раз таки избегать application-context.xml и так далее. Потом, SQL запросы в коде? Почему бы не использовать JPA и Hibernate для таких вещей?
AstarothAst
12.02.2018 11:41Да бросьте, если запрос не меняется, да к тому же достаточно элементарен, то почему бы не положить его в код ровно там, где он применяется, и где его будет ожидать увидеть тот, кто код читает? jpa, hibernate, гвоздь, микроскоп…
tamsus
12.02.2018 14:38Почему же с таким успехом не написать обычный метод, к примеру findOneByName, без SQL да в интерфейсе, наследуемом от JpaRepository и поместить его на сервисный слой? Что мешает понять этот код и найти его там, где ожидается?
AstarothAst
13.02.2018 09:56Ну, нашли вы этот код, а он читает данные из БД, а у вас проблема как раз в том, что возвращается что-то не то. В итоге все равно вы обречены спуститься на уровень базы, данных и селектов. JPA это абстракция, и как любая абстракция имеет положительные и отрицательные эффекты. В случае таких вот элементарных запросов, имхо, отрицательных эффектов гораздо больше, чем положительных. Запрос + rowmapper — почему бы нет?
Lure_of_Chaos
12.02.2018 08:42Слишком много кода для слишком малых возможностей и гибкости.
Я делал подобное, в зависимости от потребностей пришлось написать 2-5 классов, которые потом не нужно трогать.
Писать абстрактный класс контроллера с необходимостью его расширять только чтобы был доступен спринговый контекст?
До сих пор спринг конфигурируется через xml?
… вот счастье-то.
gotozero
12.02.2018 09:05Есть уже проекты которые делают такое из коробки.
github.com/roskenet/springboot-javafx-support
PqDn
12.02.2018 09:44+1Щас рекомендуется для спринг приложении использовать спринг бут. Во первых легче, во вторых некоторая гарантия, что не будет конфликтов версий, В третих не надо плагины самому прописывать на компилятор и на сингл джар
Вот дока на последнею версию docs.spring.io/spring-boot/docs/2.0.0.RC1/reference/htmlsingle
aol-nnov
12.02.2018 11:21Spring Boot это этакая сборная солянка для веб-сервера.
Нет.
"In Spring Boot, to create a non-web application, implement CommandLineRunner and override the run method"
Mogaba
12.02.2018 16:08<fx:include> позволяет делать инъекцию дочернего контроллера:
main.fxml
<fx:include source="productTable.fxml" fx:id="table" />
MainController.java
@FXML private ProductTableController tableController;
ShinRa Автор
12.02.2018 16:09Не совсем понял, либо тут ошибочка и fx:id должен быть равен tableController?
Mogaba
12.02.2018 17:22Имя поля для контроллера должно быть составлено из значения атрибута fx:id элемента <fx:include> (в данном случае «table») с добавлением слова «Controller». Когда лоадер видит такое поле (с аннотацией @FXML, естественно), он инжектит в него контроллер, который указан в include-файле.
fspare
12.02.2018 16:20Поправьте меня если я не прав, но насколько это хорошее решение делать толстого клиента?
Может быть было бы резоннее сделать слой который ходит в базу данных не частью JavaFX приложения, а частью веб сервиса который предоставлял бы эту функциональность? А для JavaFX приложения использовать нечто более легковесное, чем Spring? Например, Google Guice или что-то другое для Dependency Injection?
Просто я пытался использовать Spring для десктопных приложений и пришел к выводу, что он довольно-таки тяжелый(именно стадия инициализации занимает много времени да и куча библиотек за ним тянется из-за чего конечный разме jar файла получается довольно-таки большим, да и памяти отъедается прилично на ПК пользователя) и для десктопа именно я его бы не использовал, хотя на стороне сервера это решение себя хорошо зарекомендовало.
Все вышесказанное — мое ИМХО — было бы интересно услышать мнение других сторон.
MisterParser
13.02.2018 15:56Спасибо за статью! Как раз позавчера писал своё первое JavaFX приложение и использовал Spring для удобной работы с настройками пользователя. Почерпнул идеи из вашего примера и может быть даже напишу статью о своей разработке.
Moxa
за что нам такое счастье? неужели нет другого способа в базу ходить?
ShinRa Автор
Что вы имеете ввиду? Использование Spring, datasourse и конфига?