Не так давно появился такой замечательный фреймворк как Spring Boot, без которого я уже не представляю себе разработку на Java. Освещая неосвещенное, хочу рассмотреть интеграцию Spring Boot и всех его «плюшек» с JavaFX 2.
Всех заинтересованных приглашаю под кат.
Преабмула
Spring Boot — прекрасный фреймворк, без которого невозможно обойтись попробовав лишь раз (рекомендую сделать это каждому!). Я хочу затронуть тему не совсем тривиальную для него, а именно — интеграцию с JavaFX. Ну и чтобы не было скучно, напишу простой справочник с блэкджеком и… подключением к БД.
Приступим
Конфигурация Maven проекта ничем не отличается от самого обычного приложения Spring Boot.
В файле настроек приложения также ничего особенного.
А вот с точкой входа в приложение все гораздо интересней!
Нам необходимо инициализировать Spring контекст и сделать это можно в двух разных местах:
- Если Вам потребуется создать экземпляры типов Scene, Stage, открыть popup, то делать это нужно в методе start(), т.к. он вызывается в UI потоке.
- В противном случае можете воспользоваться методом init() (как в примере ниже), который вызывается не в UI потоке перед вызовом метода start().
Напишем абстрактный класс следующего содержания:
Хочу обратить внимание на переопределенный метод init().
Именно на момент инициализации JavaFX мы запускаем инициализацию Spring контекста:
context = SpringApplication.run(getClass(), savedArgs);
Ну и следующей строкой заполняем текущий объект бинами:
context.getAutowireCapableBeanFactory().autowireBean(this);
Наследуя абстрактный класс описанный выше, укажем поведение нашего JavaFX приложения. На этом этапе мы уже можем использовать DI и все остальные «плюшки» спринга:
Application.java
@Lazy
@SpringBootApplication
public class Application extends AbstractJavaFxApplicationSupport{
@Value("${ui.title:JavaFX приложение}")//
private String windowTitle;
@Autowired
private ControllersConfig.View view;
@Override
public void start(Stage stage) throws Exception {
stage.setTitle(windowTitle);
stage.setScene(new Scene(view.getParent()));
stage.setResizable(true);
stage.centerOnScreen();
stage.show();
}
public static void main(String[] args){
launchApp(Application.class, args);
}
}
Ну и теперь к самому интересному.
JavaFX предоставляет возможность разделять код (controller) и представление (view), причем представление хранится в XML формате, в файле с расширением *.fxml. Для самой вьюхи есть прекрасный UI редактор — Scene Builder.
У меня получился примерно такой файл представления (view):
main.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="284.0" prefWidth="405.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ru.habrahabr.ui.MainController">
<children>
<TableView fx:id="table" editable="true" prefHeight="200.0" prefWidth="405.0" tableMenuButtonVisible="true" AnchorPane.bottomAnchor="50.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY" /></columnResizePolicy>
</TableView>
<HBox alignment="CENTER" layoutX="21.0" layoutY="207.0" prefHeight="50.0" prefWidth="300.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0">
<children>
<TextField fx:id="txtName" promptText="Имя">
<HBox.margin>
<Insets right="3.0" />
</HBox.margin>
</TextField>
<TextField fx:id="txtPhone" promptText="Телефон">
<HBox.margin>
<Insets right="3.0" />
</HBox.margin>
</TextField>
<TextField fx:id="txtEmail" promptText="E-mail">
<HBox.margin>
<Insets right="3.0" />
</HBox.margin>
</TextField>
<Button minWidth="-Infinity" mnemonicParsing="false" onAction="#addContact" text="Добавить" />
</children>
</HBox>
</children>
</AnchorPane>
Листинг этого файла трудночитаемый, но обратите внимание, что у корневого элемента указан атрибут fx:controller=«ru.habrahabr.ui.MainController». Он указывает на то, какой класс-контроллер использовать для этого компонента представления. А у вложенных элементов атрибут fx:id=«txtEmail» указывает на то, к какому полю контроллера делать инъекцию. Проблема как раз-таки в том, чтобы подружить инъекции контроллера от JavaFX (которые определяются аннотацией @FXML) и инъекции от спринга. Потому что, если использовать стандартный FXML загрузчик, то спринг не узнает о новом объекте-контроллере, и, соответственно, не сделает своих инъекций.
Напишем сам контроллер:
MainController.java
package ru.habrahabr.ui;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import ru.habrahabr.entity.Contact;
import ru.habrahabr.service.ContactService;
import javax.annotation.PostConstruct;
import java.util.List;
public class MainController{
// Инъекции Spring
@Autowired private ContactService contactService;
// Инъекции JavaFX
@FXML private TableView<Contact> table;
@FXML private TextField txtName;
@FXML private TextField txtPhone;
@FXML private TextField txtEmail;
// Переменные
private ObservableList<Contact> data;
/**
* Инициализация контроллера от JavaFX.
* Метод вызывается после того как FXML загрузчик произвел инъекции полей.
*
* Обратите внимание, что имя метода <b>обязательно</b> должно быть "initialize",
* в противном случае, метод не вызовется.
*
* Также на этом этапе еще отсутствуют бины спринга
* и для инициализации лучше использовать метод,
* описанный аннотацией @PostConstruct.
* Который вызовется спрингом, после того,
* как им будут произведены все оставшиеся инъекции.
* {@link MainController#init()}
*/
@FXML
public void initialize(){
}
/**
* На этом этапе уже произведены все возможные инъекции.
*/
@PostConstruct
public void init(){
List<Contact> contacts = contactService.findAll();
data = FXCollections.observableArrayList(contacts);
// Добавляем столбцы к таблице
TableColumn<Contact, String> idColumn = new TableColumn<>("ID");
idColumn.setCellValueFactory(new PropertyValueFactory<>("id"));
TableColumn<Contact, String> nameColumn = new TableColumn<>("Имя");
nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
TableColumn<Contact, String> phoneColumn = new TableColumn<>("Телефон");
phoneColumn.setCellValueFactory(new PropertyValueFactory<>("phone"));
TableColumn<Contact, String> emailColumn = new TableColumn<>("E-mail");
emailColumn.setCellValueFactory(new PropertyValueFactory<>("email"));
table.getColumns().setAll(idColumn, nameColumn, phoneColumn, emailColumn);
// Добавляем данные в таблицу
table.setItems(data);
}
/**
* Метод, вызываемый при нажатии на кнопку "Добавить".
* Привязан к кнопке в FXML файле представления.
*/
@FXML
public void addContact(){
Contact contact = new Contact(txtName.getText(), txtPhone.getText(), txtEmail.getText());
contactService.save(contact);
data.add(contact);
// чистим поля
txtName.setText("");
txtPhone.setText("");
txtEmail.setText("");
}
}
Осталось разобраться как у нас получилось заставить Spring произвести свои инъекции в незнакомом ему объекте. А секрет кроется в еще одном классе конфигурации Spring Boot:
ConfigurationControllers.java
@Configuration
public class ConfigurationControllers{
@Bean(name = "mainView")
public View getMainView() throws IOException {
return loadView("fxml/main.fxml");
}
/**
* Именно благодаря этому методу мы добавили контроллер в контекст спринга,
* и заставили его произвести все необходимые инъекции.
*/
@Bean
public MainController getMainController() throws IOException {
return (MainController) getMainView().getController();
}
/**
* Самый обыкновенный способ использовать FXML загрузчик.
* Как раз-таки на этом этапе будет создан объект-контроллер,
* произведены все FXML инъекции и вызван метод инициализации контроллера.
*/
protected View loadView(String url) throws IOException {
InputStream fxmlStream = null;
try {
fxmlStream = getClass().getClassLoader().getResourceAsStream(url);
FXMLLoader loader = new FXMLLoader();
loader.load(fxmlStream);
return new View(loader.getRoot(), loader.getController());
} finally {
if (fxmlStream != null) {
fxmlStream.close();
}
}
}
/**
* Класс - оболочка: контроллер мы обязаны указать в качестве бина,
* а view - представление, нам предстоит использовать в точке входа {@link Application}.
*/
public class View{
private Parent view;
private Object controller;
public View(Parent view, Object controller){
this.view = view;
this.controller = controller;
}
public Parent getView(){
return view;
}
public void setView(Parent view){
this.view = view;
}
public Object getController(){
return controller;
}
public void setController(Object controller){
this.controller = controller;
}
}
}
Вот и все, мы получили JavaFX приложение, интегрированное со Spring Boot, и открывающее все его колоссальные возможности.