Search code examples
javaspring-bootjavafx

Creating new Instance of @Component each time FXMLLoader calls applicationContext.getBean(class)


I have a sort of list in my JavaFX Application. Said Application is based on the Spring Boot Framework.

That list I want to populate with multiple instances of my own JavaFX Object created with an .fxml. To do that I am currently loading the .fxml for each Item in the list with

FXMLLoader fxmlLoader = new FXMLLoader(PATH_TO_FXML);
fxmlLoader.setControllerFactory(applicationContext::getBean);

VBox listItem = fxmlLoader.load();
contentArea.getChildren().add(listItem);

But this would lead to all Items sharing the same Controller, wouldn't it? Said controller is currently annotated with @Component like the other only once initialized .fxml-Controllers Is there a way to tell spring to create a new instance of that controller each time it gets requested?

Or is there a nicer way to implement this idea of mine?

Spring Version: 3.2.1 JavaFx Version: 19.0.2 Maven Project

If there are more questions, let me know.


Solution

  • As you surmised with

    But this would lead to all Items sharing the same Controller, wouldn't it?

    Spring by default creates a single instance of each bean and shares the same instance whenever a request is made for that bean. This behavior is controlled by Spring's scopes, and this default scope is the singleton scope. To create a new instance of a bean class on each request, you need the prototype scope.

    You can (and really should, since JavaFX controllers should be created once each time the FXML is loaded) tell Spring to create a new instance each time the controller is requested by annotating the controller class @Scope("prototype"):

    @Component
    @Scope("prototype")
    public class MyControllerClass {
       // ...
    }
    

    Here is a very quick example. Here's an FXML, which I'll call Adder.fxml. It contains two Spinner<Integer> and a Label in a HBox. The idea is that the label will display the sum of the two values in the spinners.

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.geometry.Insets?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.control.Spinner?>
    <?import javafx.scene.layout.HBox?>
    <?import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory?>
    <HBox spacing="20.0" xmlns:fx="http://javafx.com/fxml"
          fx:controller="org.jamesd.examples.springscope.AddingController">
        <padding>
            <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
        </padding>
    
        <Spinner fx:id="firstSummand" onValueChange="#updateSum">
            <valueFactory><SpinnerValueFactory.IntegerSpinnerValueFactory min="-20" max="20" initialValue="0"/></valueFactory>
        </Spinner>
        <Spinner fx:id="secondSummand" onValueChange="#updateSum">
            <valueFactory><SpinnerValueFactory.IntegerSpinnerValueFactory min="-20" max="20" initialValue="0"/></valueFactory>
        </Spinner>
        <Label fx:id="sumText" text="0" HBox.hgrow="ALWAYS"/>
    </HBox>
    

    Here is the controller. Note is it annotated with both @Component and @Scope("prototytpe").

    package org.jamesd.examples.springscope;
    
    import javafx.fxml.FXML;
    import javafx.scene.control.Label;
    import javafx.scene.control.Spinner;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    
    @Component
    @Scope("prototype")
    public class AddingController {
        @FXML
        private Label sumText;
        @FXML
        private Spinner<Integer> firstSummand;
        @FXML
        private Spinner<Integer> secondSummand;
    
        @FXML
        public void updateSum() {
            sumText.setText(Integer.toString(firstSummand.getValue() + secondSummand.getValue()));
        }
    }
    

    Here is a spring boot application class, designed to launch a JavaFX application:

    package org.jamesd.examples.springscope;
    
    import javafx.application.Application;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class SpringApp {
        public static void main(String[] args) {
            Application.launch(HelloApplication.class, args);
        }
    }
    

    and the FX Application:

    package org.jamesd.examples.springscope;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import org.springframework.boot.builder.SpringApplicationBuilder;
    import org.springframework.context.ConfigurableApplicationContext;
    
    import java.io.IOException;
    
    public class HelloApplication extends Application {
    
        private ConfigurableApplicationContext applicationContext;
    
        @Override
        public void init() {
            applicationContext = new SpringApplicationBuilder(SpringApp.class).run();
        }
        @Override
        public void start(Stage stage) throws IOException {
    
            VBox root = new VBox(10);
            root.setAlignment(Pos.CENTER);
            root.setPadding(new Insets(20));
            for (int i = 0 ; i < 3; i++) {
                root.getChildren().add(createAdder());
            }
            Scene scene = new Scene(root, 600, 375);
            stage.setTitle("Adding");
            stage.setScene(scene);
            stage.show();
        }
    
        private Node createAdder() throws IOException {
            FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("Adder.fxml"));
            fxmlLoader.setControllerFactory(applicationContext::getBean);
            return fxmlLoader.load();
        }
    
    }
    

    The application creates three instances of the UI defined in Adder.fxml, which work independently.

    If you remove the @Scope("prototype") annotation in the controller, you'll see it doesn't work correctly. (Since the fields are reassigned to the same controller instance each time the FXML is loaded, only the last instance loaded gets updated.)


    Note you can also use Spring's meta-annotations to create your own meta-annotation which represents a @Component which always has @Scope("prototype"):

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    @Scope("prototype")
    public @interface FXController {}
    

    and then you can just do

    @FXController
    public class AddingController {
        // ...
    }
    

    This might be worthwhile, because FX Controllers in a spring-managed application should always be prototype scope.