SpringBoot and JavaFx cookbook

SpringBoot and JavaFx cookbook

The goal of this article is to showcase the integration of Spring Boot and JavaFx technologies that enable the sustainable and rapid development of Java desktop applications. The integration will be demonstrated through a demo project that will be gradually upgraded with functionalities and practices such as separation of presentation and business logic, dependency injection and clean code principles. The article will be more technical in nature since it is about integrating two tools through code.

Long story short – In order for these technologies to work well, it is necessary to prioritize the JavaFx application to run first through the main () method, and then include running the spring application context through the overridden JavaFX init () method. Also, for dependency injection to work properly, it is necessary to create program logic that sets the spring managed instance of the JavaFx controller before each FxmlLoader.load () call.

Rapid application development requires technologies and tools that allow things to be done quickly. When working on large projects that develop over a long period of time, it is common for more people and teams to change across them throughout the different stages of the project life. Here, in addition to the importance of rapid development, it is also important that it be sustainable development and that new team members can continue working on the project as soon as possible. One such emerging technology is Spring Boot, which has set new standards with its simplicity, flexibility, and convention over configuration approach. With its rapid development and ease of use and maintenance, Spring Boot brings with it all the benefits of the Spring framework like dependency injection.

With the development of desktop Java applications for graphical interface creation, the tools that the framework already includes are a natural choice, among the popular tools are Swing and JavaFX. The focus here will be on JavaFx because of its advantages over other tools.

JavaFx, the long-awaited sequel to the Swing tool, has brought about improvements to cleaner code, such as writing a viewport in XML-based FXML syntax and the ability to style components with CSS. The Gluon Scene Builder tool enables drag and drops the creation of complex on-screen layouts and components that are separated from the code as a separate fxml file. Compared to Swing, JavaFx is close to the MVC approach where business logic (controllers/services), view (FXML, CSS) and model (POJO classes) components are clearly separated. All this contributes to rapid development, cleaner code, and easier maintenance. In addition, JavaFX is part of the standard Java version 7/8 and is a natural choice for developing desktop java applications.

How do you combine the two technologies and get the best of both worlds?

As JavaFX is already included in JRE / JDK since Java version 7u6, it remains to download Spring Boot. If we’re on an older version of Java, you need to manually add JavaFx to the classpath or reference it as system dependency in one of the dependency management tools. The Spring Boot tool can be downloaded from the https://start.spring.io page, which allows you to create an application skeleton and add additional dependencies in a few clicks.

This demo does not need to include any of the additional options, under group and artifact we can (optionally) enter the project name (artifact) and the group where the project is located, and then select Generate Project, after which the download of the demo project in zip format will begin. We import the project into a favorite IDE after which the structure in the image is visible, with the main class named FxbootApplication.java, with the main () method launching the Spring application and context.

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

Looking at the classic JavaFX HelloWorld program, the main class inherits the javafx.application.Application class and the main () method runs the javafx.application.Application.launch () method and JavaFx later invokes the start () method in which we are handed a Stage component to which we can build on our elements and programming logic.

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

One of the first questions is certainly in which direction to go from here, with both technologies working together. After a couple of Google-Fu’s we come to one possible solution:

@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();
    }
}

Studying the javafx.application.Application.start () documentation reveals that unlike the start () method that runs the GUI thread and draws our components to the screen, the init () method runs a non-GUI thread, making it an ideal place to initialize spring context. Here, by the way, register the main FxbootApplication class in the spring container with the call getAutowireCapableBeanFactory (). AutowireBean (this); so we can use dependency injection from it which will be useful a few steps later.

After this, we can open the champagne and congratulate ourselves on the successful integration of Spring Boot and JavaFx. Until we make a more realistic example: We have created an FXML view component with a welcome label in the welcome.fxml file and an associated JavaFX controller WelcomeController.java that will set the welcome label value through a GreetingService.getWelcomeGreeting () call. For the sake of brevity of the examples, the welcomeGreeting value is written in the service itself, but the logic remains the same if there was another service or data access layer behind the service.

<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 since last modified

Running the main () method should print greeting messages in the console and plot a message label from the GreetingService service on the graphical interface.

The console can see a greeting message print from a registered WelcomeController controller in the FxbootApplication class and immediately thereafter a stack trace print that occurred because the application crashed with the NullPointerException exception.

Going to the last link in the stack trace accused of the exception of dropping line 22 from the WelcomeController class.

welcomeLabel.setText(greetingService.getWelcomeGreeting());

Further debugging comes to the fact that the culprit is an uninitialized GreetingService that is null at the time JavaFX retrieves and parses the welcome.fxml file. Wait, what. Service in the controller is non-initialized (null), and only a line before from the same registered WelcomeController controller in the FxbootApplication class received a welcome message from the service (printed before the stack trace). What kind of sorcery is this?

There will only be one.

The assumption is that if we use dependency injection (DI), then the DI container preserves a unique instance of the registered class by default. In the above case, the same instance behaves differently. In one case, we have a service that returns a message, while in another it is uninitialized. By checking with the good old Object.toString () over the WelcomeController instance registered in the FxbootApplication class and the instance that is active when invoking the JavaFx initialize () method in the controller, we get different hex/hash values of the instance. Now it is clear that we have two instances, one spring managed and one unknown that throws an NPE exception and obviously knows nothing about DI, the only question is where the other one comes from.

After further application of Google-Fu technique and searching through FxmlLoader.load () documentation we come to the interesting fact that JavaFx instantiates its controller instance which of course knows nothing about DI. We will thank JavaFX for this effort, but the DI way of things is still a more interesting principle to us. If only there was a way to explain the JavaFx FxmlLoader to take an already existing DI instance for the controller.

Fortunately, before calling the FxmlLoader.load () method, it is possible to pass a DI instance of the controller through the FXMLLoader.setControllerFactory () call.

@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();
}

This completes the integration, FXMLLoader will use a DI registered controller when parsing the fxml file.

Note: The above example uses lambda expression introduced in Java 8 if we are on an older version, we need to replace the line

loader.setControllerFactory(param -> welcomeController);

with the following code block (a great example of reduced verbosity of code in Java 8)

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

As we initially mentioned the clean code principles, a further upgrade consists of extracting logic around placing the correct controller instance outside the FxbootApplication class and creating logic for easier navigation between multiple screens. Navigation is not required if we only have one screen in the app.

Navigation

For example implementation of navigation, we will upgrade the existing demo with another screen, user.fxml and the associated UserController.java controller. The screen will open by clicking on a button in the welcome.fxml view and showing a list of users we will retrieve from the service layer,

To make navigation as easy as possible, we convert it to a spring managed component (@Component annotation) which we will register as needed in fxml controllers. The navigation class will have a reference to all existing controllers and logic to set the appropriate controller when changing the screen. The navigation action itself will be called from any controller with the show … View () method.

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

@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 class after extracting logic to load fxmls correctly

welcome and user screens connected through the navigation

We can conclude that although during the integration of JavaFX and Spring Boot technology there was some honesty with a couple of turbulence at the beginning, the challenges of integration were relatively quickly solved and the effort was well invested as we continue to build on the foundations that enable fast and sustainable development, that is, the best of both worlds.

Leave a Reply

Your email address will not be published. Required fields are marked *