Не так давно появился такой замечательный фреймворк как Spring Boot, без которого я уже не представляю себе разработку на Java. Освещая неосвещенное, хочу рассмотреть интеграцию Spring Boot и всех его «плюшек» с JavaFX 2.
Всех заинтересованных приглашаю под кат 😉
Преабмула
Spring Boot — прекрасный фреймворк, без которого невозможно обойтись попробовав лишь раз (рекомендую сделать это каждому!). Я хочу затронуть тему не совсем тривиальную для него, а именно — интеграцию с JavaFX. Ну и чтобы не было скучно, напишу простой справочник с блэкджеком и… подключением к БД.
Приступим
Конфигурация Maven проекта ничем не отличается от самого обычного приложения Spring Boot.
<dependencies> <!-- Spring Boot starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- Spring Boot JPA - для работы с БД, очевидно --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- H2 БД --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- Тесты --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
В файле настроек приложения также ничего особенного.
# Параметры UI ui.title = Spring Boot - JavaFX # JMX нам не нужен, а его отключение позволит ускорить запуск spring.jmx.enabled=false # Настройки подключения к БД и JPA spring.datasource.test-on-borrow=true spring.datasource.validation-query=SELECT 1 spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=create
А вот с точкой входа в приложение все гораздо интересней!
Нам обязательно необходимо инициализировать Spring контекст в UI потоке (в том же потоке, что и JavaFX). Для этого инициализировать Spring контекст мы будем из точки входа JavaFX класса. Напишем абстрактный класс следующего содержания:
package ru.habrahabr; import javafx.application.Application; import org.springframework.boot.SpringApplication; import org.springframework.context.ConfigurableApplicationContext; public abstract class AbstractJavaFxApplicationSupport extends Application { private static String[] savedArgs; protected ConfigurableApplicationContext context; @Override public void init() throws Exception { context = SpringApplication.run(getClass(), savedArgs); context.getAutowireCapableBeanFactory().autowireBean(this); } @Override public void stop() throws Exception { super.stop(); context.close(); } protected static void launchApp(Class<? extends AbstractJavaFxApplicationSupport> clazz, String[] args) { AbstractJavaFxApplicationSupport.savedArgs = args; Application.launch(clazz, args); } }
Хочу обратить внимание на переопределенный метод init().
Именно на момент инициализации JavaFX мы запускаем инициализацию Spring контекста:
context = SpringApplication.run(getClass(), savedArgs);
Ну и следующей строкой заполняем текущий объект бинами:
context.getAutowireCapableBeanFactory().autowireBean(this);
Наследуя абстрактный класс описанный выше, укажем поведение нашего JavaFX приложения. На этом этапе мы уже можем использовать DI и все остальные «плюшки» спринга:
@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):
<?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 загрузчик, то спринг не узнает о новом объекте-контроллере, и, соответственно, не сделает своих инъекций.
Напишем сам контроллер:
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:
@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 и открывающее все его колоссальные возможности.
Ссылка на исходники: https://github.com/ruslanys/fish-springboot-javafx
P.S. Буду счастлив, если кому-нибудь пригодится этот пост. Буду благодарен за подсказки и исправления.
ссылка на оригинал статьи http://habrahabr.ru/post/265511/
Добавить комментарий