Как подружить JavaFX и Spring Boot

от автора


Не так давно появился такой замечательный фреймворк как Spring Boot, без которого я уже не представляю себе разработку на Java. Освещая неосвещенное, хочу рассмотреть интеграцию Spring Boot и всех его «плюшек» с JavaFX 2.

Всех заинтересованных приглашаю под кат 😉

Преабмула

Spring Boot — прекрасный фреймворк, без которого невозможно обойтись попробовав лишь раз (рекомендую сделать это каждому!). Я хочу затронуть тему не совсем тривиальную для него, а именно — интеграцию с JavaFX. Ну и чтобы не было скучно, напишу простой справочник с блэкджеком и… подключением к БД.

Приступим

Конфигурация Maven проекта ничем не отличается от самого обычного приложения Spring Boot.

pom.xml

<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> 

В файле настроек приложения также ничего особенного.

application.properties

# Параметры 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 класса. Напишем абстрактный класс следующего содержания:

AbstractJavaFxApplicationSupport.java

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 и все остальные «плюшки» спринга:

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 и открывающее все его колоссальные возможности.

Ссылка на исходники: https://github.com/ruslanys/fish-springboot-javafx

P.S. Буду счастлив, если кому-нибудь пригодится этот пост. Буду благодарен за подсказки и исправления.

ссылка на оригинал статьи http://habrahabr.ru/post/265511/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *