JavaFX controllers are typically instantiated and managed by the JavaFX framework. Hence, if your app is using a spring framework then you won't be able to autowire a spring managed bean inside a JavaFX UI controller. We need to somehow register the JavaFX UI controllers into the spring container. In other words we need to let spring framework instantiate and manage the JavaFX UI controllers. In this blog we will see how to do that.
Consider a use case of showing product list by fetching it from DB. You have products list page designed in FXML with its controller. A service class will facilitate fetching records from DB and converting it to DTOs. We will see how to do this with Autowired service class in the Product List Controller. For simplicity we are skipping the repository layer logic.
Below are the steps for the same
Setup the package scanning to include the controllers package
Define controller with annotation and dependencies
Define the service with mock data
Attach the spring bean factory to JavaFX controller factory
Define the FXML file
Ensure to include the package containing the controllers, services and other beans which are annotated with spring annotations. In my case my packages are like :
abm.basicjavafx.controllers
abm.basicjavafx.services
package abm.basicjavafx.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("abm.basicjavafx")
public class AppConfig {
}
Ensure the controller is annotated with @Component annotation since in this case this controller class is not handling any http requests and is a UI controller.
package abm.basicjavafx.controllers;
import abm.basicjavafx.controllers.dtos.ProductDto;
import abm.basicjavafx.services.ProductService;
import javafx.collections.FXCollections;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import org.springframework.stereotype.Component;
import java.net.URL;
import java.util.List;
import java.util.ResourceBundle;
@Component
public class ProductListController implements Initializable {
private final ProductService productService;
public TableView<ProductDto> productListTable;
public TableColumn<ProductDto, String> titleCol;
public TableColumn<ProductDto, String> brandCol;
public TableColumn<ProductDto, Double> priceCol;
public ProductListController(ProductService productService) {
this.productService = productService;
}
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
List<ProductDto> products = productService.getAll();
products.forEach(System.out::println);
var productsData = FXCollections.observableArrayList(products);
titleCol.setCellValueFactory(new PropertyValueFactory<>("name"));
brandCol.setCellValueFactory(new PropertyValueFactory<>("brand"));
priceCol.setCellValueFactory(new PropertyValueFactory<>("price"));
productListTable.setItems(productsData);
}
}
Create a service class and return a mock data for now. Later you can integrate the repository class and fetch the data from the DB.
package abm.basicjavafx.services;
import abm.basicjavafx.controllers.dtos.ProductDto;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
public List<ProductDto> getAll() {
return List.of(
new ProductDto(1, "IPhone", "Apple Inc.", 70_000d),
new ProductDto(2, "Galaxy", "Samsung", 45_000d)
);
}
}
package abm.basicjavafx.controllers.dtos;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
public record ProductDto(
SimpleIntegerProperty id,
SimpleStringProperty name,
SimpleStringProperty brand,
SimpleDoubleProperty price
) {
public ProductDto(Integer id, String name, String brand, Double price) {
this(new SimpleIntegerProperty(id), new SimpleStringProperty(name), new SimpleStringProperty(brand), new SimpleDoubleProperty(price));
}
public int getId() {
return id.get();
}
public SimpleIntegerProperty idProperty() {
return id;
}
public String getName() {
return name.get();
}
public SimpleStringProperty nameProperty() {
return name;
}
public String getBrand() {
return brand.get();
}
public SimpleStringProperty brandProperty() {
return brand;
}
public double getPrice() {
return price.get();
}
public SimpleDoubleProperty priceProperty() {
return price;
}
@Override
public String toString() {
return "ProductDto{" +
"id=" + id +
", name='" + name + '\'' +
", brand='" + brand + '\'' +
", price=" + price +
'}';
}
}
Now we need to register the spring bean factory to the JavaFX controller factory. Now the question here would be who is in control is it a JavaFX framework or the Spring Container. In this case the spring framework is in control of the bean instantiation and dependency injection. But JavaFX takes control once the bean is instantiated. We get the benefit of both.
package abm.basicjavafx;
import abm.basicjavafx.config.AppConfig;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class BasicJavaFXApp extends Application {
private static ApplicationContext applicationContext;
public static void main(String[] args) {
launch(args);
}
@Override
public void init() throws Exception {
applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
}
@Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("ProductList.fxml"));
fxmlLoader.setControllerFactory(applicationContext::getBean);
Parent root = fxmlLoader.load();
primaryStage.setTitle("Spring Managed JavaFX");
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
@Override
public void stop() throws Exception {
((AnnotationConfigApplicationContext) applicationContext).close();
}
}
Create and FXML file and design the UI using JavaFX Scene Builder. Generally the FXML files should be kept in the src/main/resources folder with the package name same as that of the loading class. But you can keep it in any other sub-package but do remember to use proper path for the same.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="abm.basicjavafx.controllers.ProductListController">
<children>
<TableView fx:id="productListTable" prefHeight="200.0">
<columns>
<TableColumn fx:id="titleCol" prefWidth="200.0" text="Title" />
<TableColumn fx:id="brandCol" prefWidth="150.0" text="Brand" />
<TableColumn fx:id="priceCol" prefWidth="120.0" text="Price" />
</columns>
</TableView>
</children>
</AnchorPane>
If you are using latest java version with module support then refer the module-info.java file in the adjacent panel.
module abm.basicjavafx {
requires javafx.controls;
requires javafx.fxml;
requires org.slf4j;
requires ch.qos.logback.core;
requires garuda.sdk;
requires java.desktop;
requires spring.core;
requires spring.context;
requires spring.beans;
requires spring.tx;
requires spring.data.jpa;
requires spring.jdbc;
requires spring.orm;
opens abm.basicjavafx to javafx.fxml;
exports abm.basicjavafx;
exports abm.basicjavafx.controllers;
exports abm.basicjavafx.controllers.dtos;
exports abm.basicjavafx.services;
opens abm.basicjavafx.controllers to javafx.fxml;
opens abm.basicjavafx.config to spring.core, spring.beans, spring.context;
opens abm.basicjavafx.controllers.dtos to javafx.fxml, javafx.base;
}
Now run the app using the main Application class i.e. BasicJavaFXApp.java and you should see some UI as shown in the screenshot. In this case you are now able to fully manage and wire all beans in your app starting from the JavaFX UI controller.
UI for the app Spring Managed JavaFX