Цель этой статьи — продемонстрировать интеграцию технологий Spring Boot и JavaFx, которая обеспечивает устойчивую и быструю разработку настольных приложений Java. Интеграция будет продемонстрирована в демонстрационном проекте, который будет постепенно обновляться с помощью таких функций и методов, как разделение представления и бизнес-логики, внедрение зависимостей и принципы чистого кода. Статья будет носить более технический характер, поскольку речь идет об интеграции двух инструментов через код.

Короче говоря, чтобы эти технологии работали хорошо, необходимо установить приоритет приложения JavaFx для запуска сначала через метод main(), а затем включить запуск контекста приложения spring через переопределенный метод init() JavaFX. Также для корректной работы внедрения зависимостей необходимо создать программную логику, которая устанавливает управляемый Spring экземпляр контроллера JavaFx перед каждым вызовом FxmlLoader.load().

Для быстрой разработки приложений требуются технологии и инструменты, которые позволяют выполнять задачи быстро. При работе над крупными проектами, которые разрабатываются в течение длительного периода времени, обычно все больше людей и команд меняются в них на разных этапах жизни проекта. Здесь, помимо важности быстрой разработки, также важно, чтобы она была устойчивой и чтобы новые члены команды могли продолжить работу над проектом как можно скорее. Одной из таких новых технологий является Spring Boot, которая установила новые стандарты своей простотой, гибкостью и подходом к конфигурации, основанным на соглашениях. Благодаря быстрой разработке и простоте использования и обслуживания Spring Boot обладает всеми преимуществами среды Spring, такими как внедрение зависимостей.

При разработке настольных Java-приложений для создания графического интерфейса инструменты, которые уже включены в фреймворк, являются естественным выбором, среди популярных инструментов — Swing и JavaFX. Здесь основное внимание будет уделено JavaFx из-за его преимуществ по сравнению с другими инструментами.

JavaFx, долгожданное продолжение инструмента Swing, принес улучшения в более чистый код, такие как написание окна просмотра с синтаксисом FXML на основе XML и возможность стилизовать компоненты с помощью CSS. Инструмент Gluon Scene Builder позволяет перетаскивать и создавать сложные макеты и компоненты на экране, которые отделены от кода в виде отдельного файла fxml. По сравнению с Swing, JavaFx близок к подходу MVC, где компоненты бизнес-логики (контроллеры/сервисы), представления (FXML, CSS) и модели (классы POJO) четко разделены. Все это способствует быстрой разработке, более чистому коду и простоте обслуживания. Кроме того, JavaFX является частью стандартной версии Java 7/8 и является естественным выбором для разработки настольных приложений Java.

Как вы сочетаете две технологии и получаете лучшее от обоих миров?

Так как JavaFX уже включен в JRE/JDK начиная с версии Java 7u6, осталось скачать Spring Boot. Если мы используем более старую версию Java, вам нужно вручную добавить JavaFx в путь к классам или сослаться на него как на системную зависимость в одном из инструментов управления зависимостями. Инструмент Spring Boot можно скачать со страницы https://start.spring.io, который позволяет в несколько кликов создать скелет приложения и добавить дополнительные зависимости.

В этой демонстрации не нужно включать какие-либо дополнительные параметры, в группе и артефакте мы можем (необязательно) ввести имя проекта (артефакт) и группу, в которой находится проект, а затем выбрать «Создать проект», после чего загрузка демонстрационный проект в формате zip начнется. Мы импортируем проект в любимую IDE, после чего видна структура на изображении, с основным классом с именем FxbootApplication.java, с методом main(), запускающим приложение Spring и контекст.

@SpringBootApplication
public class FxbootApplication {
    public static void main(String[] args) {
        SpringApplication.run(FxbootApplication.class, args);
    }
}

Глядя на классическую программу JavaFX HelloWorld, основной класс наследует класс javafx.application.Application, а метод main() запускает метод javafx.application.Application.launch(), а позже JavaFx вызывает метод start(), в котором мы передал компонент Stage, к которому мы можем добавить наши элементы и программную логику.

public class FxbootApplication extends Application {
    public static void main(String[] args) {
        Application.launch();
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        Pane helloPane = new Pane(new Label("Hello JavaFx"));
        primaryStage.setScene(new Scene(helloPane));
        primaryStage.show();
    }
}

JavaFx Hello World после запуска

Один из первых вопросов, безусловно, заключается в том, в каком направлении двигаться дальше, когда обе технологии работают вместе. После пары гугл-фу приходим к одному возможному решению:

@SpringBootApplication
public class FxbootApplication extends Application {
    public static void main(String[] args) {
        Application.launch();
    }
    @Override
    public void init() {
        SpringApplication.run(getClass()).getAutowireCapableBeanFactory().autowireBean(this);
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        Pane helloFxPane = new Pane(new Label("Hello JavaFx"));
        primaryStage.setScene(new Scene(helloFxPane));
        primaryStage.show();
    }
}

Изучение документации javafx.application.Application.start() показывает, что в отличие от метода start(), который запускает поток GUI и рисует наши компоненты на экране, метод init() запускает поток без GUI, что делает его идеальным местом для инициализации весеннего контекста. Вот, кстати, зарегистрируйте основной класс FxbootApplication в контейнере spring вызовом getAutowireCapableBeanFactory(). AutowireBean (это); поэтому мы можем использовать инъекцию зависимостей из него, что будет полезно несколькими шагами позже.

После этого мы можем открыть шампанское и поздравить себя с успешной интеграцией Spring Boot и JavaFx. Пока мы не сделаем более реалистичный пример: мы создали компонент представления FXML с меткой приветствия в файле welcome.fxml и связанный контроллер JavaFX WelcomeController.java, который будет устанавливать значение метки приветствия через вызов GreetingService.getWelcomeGreeting(). Для краткости примеров значение welcomeGreeting записывается в самом сервисе, но логика остается той же, если за сервисом стоит другой сервис или уровень доступа к данным.

<Pane xmlns: fx = "http://javafx.com/fxml/1" fx: controller = "en.kingict.java.controller.WelcomeController">
     <Label fx: id = "welcomeLabel" />
</Pane>

welcome.fxml

@Component
public class WelcomeController implements Initializable{
    @FXML public Label welcomeLabel;
    @Autowired
    private GreetingService greetingService;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        welcomeLabel.setText(greetingService.getWelcomeGreeting());
    }
    public String getWelcomeMessage() {
        return greetingService.getWelcomeGreeting();
    }
}

welcomeController.java

@Service
public class GreetingService {
    public String getWelcomeGreeting() {
        return "Welcome and have a nice day!";
    }
}

GreetingService.java

@Override
public void start(Stage primaryStage) throws Exception {
    System.out.println(welcomeController.getWelcomeMessage());
    // primaryStage.setScene(new Scene(new Pane(new Label("Hello JavaFx"))));
    Parent welcomePane = FXMLLoader.load(getClass().getResource("/welcome.fxml"));
    primaryStage.show();
}

FxbootApplication.java с момента последнего изменения

Запуск метода main() должен печатать приветственные сообщения в консоли и отображать метку сообщения из службы GreetingService в графическом интерфейсе.

Консоль может видеть распечатку приветственного сообщения от зарегистрированного контроллера WelcomeController в классе FxbootApplication и сразу после этого распечатку трассировки стека, которая произошла из-за сбоя приложения с исключением NullPointerException.

Переход к последней ссылке в трассировке стека обвиняется в исключении исключения строки 22 из класса WelcomeController.

welcomeLabel.setText(greetingService.getWelcomeGreeting());

Дальнейшая отладка приводит к тому, что виновником является неинициализированный GreetingService, который имеет значение null в момент, когда JavaFX извлекает и анализирует файл welcome.fxml. Чего ждать. Служба в контроллере не инициализирована (нулевая), и только строкой ранее от того же зарегистрированного контроллера WelcomeController в классе FxbootApplication получено приветственное сообщение от службы (распечатывается перед трассировкой стека). Что это за колдовство?

Будет только один.

Предполагается, что если мы используем внедрение зависимостей (DI), то контейнер DI по умолчанию сохраняет уникальный экземпляр зарегистрированного класса. В приведенном выше случае один и тот же экземпляр ведет себя по-разному. В одном случае у нас есть сервис, который возвращает сообщение, а в другом он не инициализирован. Сверяясь старым добрым Object.toString() над экземпляром WelcomeController, зарегистрированным в классе FxbootApplication, и экземпляром, активным при вызове метода JavaFx initialize() в контроллере, мы получаем разные шестнадцатеричные/хэш-значения экземпляра. Теперь ясно, что у нас есть два экземпляра, один управляемый Spring и один неизвестный, который выдает исключение NPE и, очевидно, ничего не знает о DI, вопрос только в том, откуда берется другой.

После дальнейшего применения техники Google-Fu и поиска в документации FxmlLoader.load() мы приходим к интересному факту, что JavaFx создает свой экземпляр контроллера, который, конечно, ничего не знает о DI. Мы будем благодарны JavaFX за эту работу, но принцип DI по-прежнему представляет для нас более интересный принцип. Если бы только был способ объяснить JavaFx FxmlLoader, чтобы он брал уже существующий экземпляр DI для контроллера.

К счастью, перед вызовом метода FxmlLoader.load() есть возможность передать DI-экземпляр контроллера через вызов FXMLLoader.setControllerFactory().

@Override
public void start(Stage primaryStage) throws Exception {
    System.out.println(welcomeController.getWelcomeMessage() + " " + welcomeController.toString());
    // Parent welcomePane = FXMLLoader.load(getClass().getResource("/welcome.fxml"));
    Parent welcomePane = loadFxml("/welcome.fxml");
    primaryStage.setScene(new Scene(welcomePane));
    primaryStage.show();
}
private Parent loadFxml(String view) {
    FXMLLoader loader = new FXMLLoader(getClass().getResource(view));
    loader.setControllerFactory(param -> welcomeController);
    try {
        loader.load();
    } catch (IOException ex) {
        System.err.println("IOException while loading resource " + view);        }
    return loader.getRoot();
}

На этом интеграция завершена, FXMLLoader будет использовать зарегистрированный DI-контроллер при анализе файла fxml.

Примечание. В приведенном выше примере используется лямбда-выражение, введенное в Java 8. Если мы используем более старую версию, нам нужно заменить строку

loader.setControllerFactory(param -> welcomeController);

со следующим блоком кода (отличный пример уменьшения детализации кода в Java 8)

loader.setControllerFactory(new Callback<Class<?>, Object>() {
    @Override
    public Object call(Class<?> param) {
        return welcomeController;
    }
});

Как мы изначально упоминали о принципах чистого кода, дальнейшее обновление состоит в извлечении логики для размещения правильного экземпляра контроллера вне класса FxbootApplication и создании логики для упрощения навигации между несколькими экранами. Навигация не требуется, если у нас есть только один экран в приложении.

Навигация

Например, для реализации навигации мы обновим существующую демонстрацию другим экраном, user.fxml и связанным с ним контроллером UserController.java. Экран откроется, если щелкнуть кнопку в представлении welcome.fxml и показать список пользователей, которых мы получим из сервисного слоя.

Чтобы максимально упростить навигацию, мы конвертируем его в управляемый компонент Spring (@Component annotation), который мы будем регистрировать по мере необходимости в контроллерах fxml. Класс навигации будет иметь ссылку на все существующие контроллеры и логику для установки соответствующего контроллера при смене экрана. Само действие навигации будет вызываться из любого контроллера с помощью метода show…View().

@Component
public class Navigation {
    private static final Logger LOG = LoggerFactory.getLogger(Navigation.class);
    private static final String WELCOME_VIEW = "/fxml/welcome.fxml";
    private static final String USER_VIEW = "/fxml/user.fxml";
    private static final String APP_CSS = "/css/application.css";
    private Stage stage;
    @Autowired
    private WelcomeController welcomeController;
    @Autowired
    private UserController userController;
    public void showWelcomeView() {
        show(WELCOME_VIEW);
    }
    public void showUserView() {
        show(USER_VIEW);
    }
    private void show(String view) {
        Scene scene = new Scene(loadFxml(view), 400, 400);
        stage.setScene(scene);
        stage.show();
    }
    private Parent loadFxml(String view) {
        FXMLLoader loader = new FXMLLoader(getClass().getResource(view));
        loader.setControllerFactory(param -> getViewController(view));
        try {
            loader.load();
        } catch (IOException ex) {
            LOG.error("IOException while loading resource {}: ", view, ex);
        }
        Parent root = loader.getRoot();
        root.getStylesheets().add(getClass().getResource(APP_CSS).toExternalForm());
        return root;
    }
    private Object getViewController(String view) {
        if (USER_VIEW.equals(view)) {
            return userController;
        }
        return welcomeController;
    }
    public void setStage(Stage stage) {
        this.stage = stage;
    }
}

Класс Navigation.java

@SpringBootApplication
public class FxbootApplication extends Application {
    @Autowired
    private Navigation navigation;
    public static void main(String[] args) {
        Application.launch(args);
    }
    @Override
    public void init() {
        SpringApplication.run(getClass())
            .getAutowireCapableBeanFactory().autowireBean(this);
    }
    @Override
    public void start(Stage stage) {
        navigation.setStage(stage);
        navigation.showWelcomeView();
    }
}

Класс FxbootApplication.java после извлечения логики для правильной загрузки fxmls

Экраны приветствия и пользователя, связанные через навигацию

Мы можем сделать вывод, что, хотя во время интеграции технологий JavaFX и Spring Boot в начале была некоторая честность с парой волнений, проблемы интеграции были относительно быстро решены, и усилия были хорошо вложены, поскольку мы продолжаем развивать основы, которые обеспечивают быстрое и устойчивое развитие, то есть лучшее из обоих миров.

Веб-сайт: https://www.alenibric.com.tr