Add Spring to JavaFX

Yesterday I wanted to add Spring to my Pandoc project and I had a lot of trouble with it. My problem was that I wanted to split my FXML files into multiple files and make each file controlled by a separate controller. This is – without Spring – not a real problem, because you just create your controller classes, add fx:controller=”YourController” to each FXML file and everything’s fine. But problems arise if you now want to have some objects to be autowired by Spring. I read a lot of tutorials about the topic, but every tutorial just showed the problem if you have only one main controller for your root FXML file. By the way this and this are nice tutorials to get in touch with the problem.

The initial situation:

<BorderPane xmlns="http://javafx.com/javafx/null" 
xmlns:fx="http://javafx.com/fxml/1" 
fx:controller="MainController">
    
    <left>
        <fx:include source="left.fxml" />
    </left>

    <center>
        <!-- your content is here -->
    </center>

    <right>
        <fx:include source="right.fxml" />
    </right>
</BorderPane>
<GridPane xmlns="http://javafx.com/javafx/null" 
xmlns:fx="http://javafx.com/fxml/1" 
fx:controller="OtherController">

<!-- other elements which are controlled by another controller -->

</GridPane>

The right.fxml is analogue to left.fxml.

The problem was that if you have a controller which is created by JavaFX, only the @FXML annotated fields contain injected data. If you add Spring DI in a simple stupid way, Spring would create a second object and this object would only contain injected fields which are annotated with Spring annotations. So you need to combine both.

Finally I came to the following solution:

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        AnnotationConfigApplicationContext context
                = new AnnotationConfigApplicationContext(AppConfiguration.class);

        SpringFxmlLoader loader = new SpringFxmlLoader(context);
        Parent parent = (Parent) loader.load("/fxml/main.fxml");
        primaryStage.setScene(new Scene(parent, 1000, 900));
        primaryStage.setTitle("Your Title");
        primaryStage.show();
    }


    public static void main(String[] args) {
        launch(args);
    }
}

This class is the entry point for the application. It creates the Spring application context by using the annotation based approach and calls a SpringFxmlLoader class to create the JavaFX application and return the root/parent object for it.

The Spring configuration class is very simple. For the sake of simplicity I provide it here for you:

@Configuration
@ComponentScan("your.package.to.scan")
public class AppConfiguration {

   /* ... */
}

As you can see, it is very simple. It scans my project for beans by setting the @ComponentScan annotation. You can add some more configuration if you need to. I recommend to read the Spring docu about annotation based configuration.

Now here comes the tricky part which costs a lot of pain:

public class SpringFxmlLoader {

    private ApplicationContext context;

    public SpringFxmlLoader(ApplicationContext appContext) {
        this.context = appContext;
    }

    /**
     * Loads the root FXML file and uses Spring's context to get controllers.
     *
     * @param resource location of FXML file
     * @return parent object of FXML layout, see {@link FXMLLoader#load(InputStream)}
     * @throws IOException in case of problems with FXML file
     */
    public Object load(final String resource) throws IOException {
        try (InputStream fxmlStream = getClass().getResourceAsStream(resource)) {
            FXMLLoader loader = new FXMLLoader();
            // set location of fxml files to FXMLLoader
            URL location = getClass().getResource(resource);
            loader.setLocation(location);
            // set controller factory
            loader.setControllerFactory(context::getBean);
            // load FXML
            return loader.load(fxmlStream);
        } catch (BeansException e) {
            throw new RuntimeException(e);
        }
    }
}

The try-catch is created by creating a stream to read the FXML file. Then the FXMLLoader object is created. Two/three very important lines:

  • with loader.setLocation(location) you can set the location of your FXML file, because FXMLLoader can’t get it automatically. But this is only important if you specify some fx:include in your main FXML file. This explains it very nice: http://praxisit.de/fxinclude/ only in german, but basically you’ll get this exception if you don’t set the location and FXMLLoader is searching for the included FXML files:
    javafx.fxml.LoadException: Base location is undefined.
    
  • loader.setControllerFactory(context::bean) Thanks to Java 8 that this is a one-liner! Otherwise you would have to implement a Callback interface like this:
    loader.setControllerFactory(new Callback<Class<?>, Object>() {
        @Override
        public Object call(final Class<?> param) {
            return context.getBean(param);
        }
    });

    And this is the tricky point which none of the tutorials I’ve read is handling. The method setControllerFactory is used if FXMLLoader notices a controller which has to be instantiated (e.g. for your main.fxml). With this method, it tries to get a bean from the implemented controller factory first. If this fails, it creates a bean/object by itself. Read Customizable controller instantiation for additional information. The other tutorials usually use the interface and write

    // they get a controller bean from Spring context or the controller class from somewhere else; then do:
    
    return controller;
    
    // OR
    
    return context.getBean(controllerClass);

    which means they ignore param of the callback method (param can be equals to MainController or OtherController in this example) and thus only get the same controller back.

In order to run your application, you need to add your MainController and OtherController to Spring context, e.g. add @Service to the class.

 

At the moment I don’t know if this solution works fine if you have multiple dialogs/windows for you application, but in case you only have one and only multiple controllers + FXML files, this is one solution for it.