javaspring-bootjavafxspring-data-jpajavafx-8

Adding Spring Dependency Injection in JavaFX (JPA Repo, Service)


I have a java FX basic application which has a simple Scene(a form). I have a Mysql Db and I am using Spring JPA (spring data jpa i.e repository/entities) to interact with the same.

Now, since we know that javaFx has some lifecycle hooks namely: init() start() and stop().

Let's say I want to insert data in Database using JPA save() method. Usually, if it was my controller, a normal DB injection like:

@Autowired
EmployeeRepo employeeRepo;

Would have worked. But, I am not able to access this (or any Autowired Injection) inside the lifecycle methods.

public void start(Stage primaryStage) throws Exception {

// Some Code

employeeRepo.findAll() <- This is returning null

However, when I add a test method and use the same, It works fine:

@PostConstruct
public void test() {
// Repo object is not giving null
}

Is there a way I can manually inject the dependencies inside my button listener or pass it to the launch method.

Please let me know if there is a solution as I am new to JavaFX


Solution

  • Dependency injection options for JavaFX

    There are numerous ways to get dependency injection into a JavaFX application. For example Gluon have a project called Gluon Ignite which enables JavaFX application for various dependency injection frameworks, such as Guice, Spring and Dagger.

    As you have chosen Spring for your dependency injection framework and you wish to use a bunch of other Spring facilities such as Spring Data repositories, you may wish to consider using a SpringBoot application.

    You could make your JavaFX application a SpringBoot application (though this isn't strictly necessary just to get dependency injection) in order to get a bunch of Spring facilities available within your application. There are some tutorials on that on the web if you search around.

    Basic sample integration of Spring and JavaFX

    Here is an example of a tutorial on integrating JavaFX with a SpringBoot application:

    A critical part of that example is the init() method of the application (which I have just copy and pasted and reproduced here for reference):

    @SpringBootApplication
    public class DemoApplication extends Application {
    
        private ConfigurableApplicationContext springContext;
        private Parent root;
    
        @Override
        public void init() throws Exception {
            springContext = SpringApplication.run(DemoApplication.class);
            FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/sample.fxml"));
            fxmlLoader.setControllerFactory(springContext::getBean);
            root = fxmlLoader.load();
        }
    
        @Override
        public void start(Stage primaryStage) throws Exception {
            primaryStage.setTitle("Hello World");
            Scene scene = new Scene(root, 800, 600);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        @Override
        public void stop() throws Exception {
            springContext.stop();
        }
    
    
        public static void main(String[] args) {
            launch(DemoApplication.class, args);
        }
    }
    

    The sample app is running the SpringBoot application to startup the Spring system and have it provide an application context in the init method. The app then uses the FXMLLoader setControllerFactory() method to allow Spring to instantiate FXML controllers and inject references to Spring beans in the application.

    For annotated autowiring to work in a Spring Bean, the annotated bean should (by default) be in a sibling or child package to the SpringBootApplication.

    Auto-wiring your JavaFX Controllers

    To get your JAVAFX FXML controller autowired, in addition to the following call on the FXMLLoader:

    fxmlLoader.setControllerFactory(springContext::getBean);
    

    You also need to annotate your class as a Spring @Component, and @Autowired in any Spring dependencies you want your controller to use. In this way, the FXMLLoader will inject the @FXML based references to your UI elements and it will also delegate to the spring context to inject the Spring dependencies.

    @Component
    @Scope("prototype")
    public class DemoController {
        @FXML
        private Label usernameLabel; 
    
        @Autowired
        public SpringService mySpringService;
    
        public void initialize() {
            usernameLabel.setText(
                mySpringService.getLoggedInUsername()
            );
        }
    }
    

    Note, Spring has a @Controller annotation, which could be used to annotate the JavaFX controller rather than the @Component annotation. But I would recommend avoiding use of @Controller for that purpose. Instead, only use the @Controller annotation for Spring REST service endpoint controller definitions.

    @Scope("prototype") is used on the controller because we want to get a new controller to back the new view objects created each time the FXML is loaded, not reuse an existing controller, which is related to a different set of view objects.

    Separation of concerns between the Spring Boot Application and the JavaFX Application

    One thing you might want to be careful of is that running the SpringBoot application, generates a new instance of the application and you already have a JavaFX application instance launched by the JavaFX system, so that would result in two JavaFX application instances if the SpringBoot application and the JavaFX application are based upon the same class (as shown above), which would potentially be confusing.

    It is better to separate the Spring application and the JavaFX application. This enhances the separation of concerns between the UI and service portions of the application and makes for easier testing as the Spring application can be unit tested independently of starting up and shutting down the JavaFX application.

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

    Then rename DemoApplication above to DemoFxApplication.

    When running the spring application from the fx application, write:

    springContext = SpringApplication.run(DemoSpringApplication.class);
    

    When testing just the Spring functionality without UI components, use DemoSpringApplication as a main class.

    When running your complete application with a UI use DemoFXApplication as a main class.

    Auto-wiring the JavaFX application class

    Note, using the above setup, it will not auto-wire the JavaFX application class instantiated instance. If you wish to do that, you can use the technique illustrated below to inject beans in the JavaFX instantiated application class:

    Place the following code inside your application's init method:

    springContext
        .getAutowireCapableBeanFactory()
        .autowireBeanProperties(
            this,
            AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, 
            true
        );
    

    The mvvmFX framework uses a similar method to that outlined above to integrate SpringBoot with JavaFX applications:

    Passing command line arguments from JavaFX to SpringBoot

    To pass arguments from the JavaFX application to the SpringBoot application, use:

    SpringApplication.run(
        DemoApplication.class, 
        getParameters().getRaw().toArray(new String[0])
    );
    

    Other issues

    If you need, even more control over the startup of the SpringApplication, you can use the SpringApplicationBuilder for example:

    ConfigurableApplicationContext startupContext =
            new SpringApplicationBuilder(DemoApplication.class)
                    .web(WebApplicationType.NONE)
                    .run(args);
    

    This answer is just written to give you hints on how you might approach this problem rather than as a general-purpose guide on how to integrate dependency injection with JavaFX, which could end up being quite a tricky subject to cover comprehensively.