Pull to refresh

И на улицу JavaFX тоже придет Spring

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

Надеюсь среди Вас найдутся такие же любители делать формочки как и я.
Дело в том, что я всегда был приверженцем дружелюбных интерфейсов. Меня расстраивали приложения, которые мало ориентированны на пользователей, такое особенно бывает в корпоративной разработке. И зачастую клиентские приложения написанные на 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 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


Теперь пришло время для 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 для таблицы в БД.
Tags:
Hubs:
+25
Comments23

Articles