Dependency Injection and Javafx

Dependency Injection and JavaFX

You can specify a controller factory for the FXMLLoader. The controller factory is a function that maps the controller class to an object (presumably, but not necessarily, an instance of that class) which will be used as the controller.

So if you want Spring to create the controller instances for you, this can be as simple as:

ApplicationContext context = ... ;

FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/fxml"));
loader.setControllerFactory(context::getBean);
Parent root = loader.load();
SomeController controller = loader.getController(); // if you need it...
// ...

And now the FXMLLoader will create controller instances for a Class<?> c by calling context.getBean(c);.

So, e.g., you could have a configuration:

@Configuration
public class AppConfig {

@Bean
public MyService service() {
return new MyServiceImpl();
}

@Bean
@Scope("prototype")
public SomeController someController() {
return new SomeController();
}

// ...
}

with

public class SomeController {

// injected by FXMLLoader:
@FXML
private TextField someTextField ;

// Injected by Spring:
@Inject
private MyService service ;

public void initialize() {
someTextField.setText(service.getSomeText());
}

// event handler:
@FXML
private void performAction(ActionEvent e) {
service.doAction(...);
}
}

If you're not using a DI framework, and you want to do the injection "by hand", you can do so, but it involves using quite a lot of reflection. The following shows how (and will give you an idea of how much ugly work Spring is doing for you!):

FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/fxml"));
MyService service = new MyServiceImpl();
loader.setControllerFactory((Class<?> type -> {
try {
// look for constructor taking MyService as a parameter
for (Constructor<?> c : type.getConstructors()) {
if (c.getParameterCount() == 1) {
if (c.getParameterTypes()[0]==MyService.class) {
return c.newInstance(service);
}
}
}
// didn't find appropriate constructor, just use default constructor:
return type.newInstance();
} catch (Exception exc) {
throw new RuntimeException(exc);
}
});
Parent root = loader.load();
// ...

and then just do

public class SomeController {

private final MyService service ;

public SomeController(MyService service) {
this.service = service ;
}

// injected by FXMLLoader:
@FXML
private TextField someTextField ;

public void initialize() {
someTextField.setText(service.getSomeText());
}

// event handler:
@FXML
private void performAction(ActionEvent e) {
service.doAction(...);
}
}

Finally, you might want to check out afterburner.fx, which is a very lightweight (in all the best ways) JavaFX-specific DI framework. (It uses a convention-over-configuration approach, where you just match FXML file names to controller class names, and optionally CSS file names, and everything just works.)

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

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:

  • Let Spring Be Your JavaFX Controller Factory

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.

Auto-wiring your JavaFX Controllers

To get your JAVAFX FXML controller autowired, in addition to the following call one 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
public class DemoController {
@FXML
private Label usernameLabel;

@Autowired
public SpringService mySpringService;

public void initialize() {
usernameLabel.setText(
mySpringService.getLoggedInUsername()
);
}
}

Note, Spring has an @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, and instead @Controller annotation for Spring REST service endpoint controller definitions.

Separation of concerns between the Spring Boot Application and 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.

So it may is likely better to separate out 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 of starting up and shutting down the JavaFX application.

Auto-wiring the JavaFX application class

Note, using the above setup, it will not autowire 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:

  • Injecting beans into a class outside the Spring managed context

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:

  • mvvmFX code for integrating SpringBoot.

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 being quite a tricky subject to cover comprehensively.

Dependency injection with JavaFX gives NullPointerExceptions

By default, the FXMLLoader will create controller instances by calling the no-argument constructor. Consequently, the AddingWindowController instance that's created is not managed by Spring, and Spring cannot inject @Autowired dependencies into it. (Basically, the Spring ApplicationContext has no knowledge that the AddingWindowController instance exists.)

You can supply a ControllerFactory to the FXMLLoader which asks that the controller is created by the spring application context:

@Component
public class Initialization implements ApplicationListener<StarterClass.StageReadyEvent>{

@Autowired
private ApplicationContext applicationContext ;

@Override
public void onApplicationEvent(StarterClass.StageReadyEvent event) {
Stage stage = event.getStage();
HBox mainPane= null;
try {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/addingWindow.fxml"));
fxmlLoader.setControllerFactory(applicationContext::getBean);
mainPane = fxmlLoader.load();
} catch (IOException e) {
e.printStackTrace();
}
Scene scene=new Scene(mainPane);
stage.setScene(scene);
stage.show();
}
}

Also note that you should create a new controller instance each time the FXML is loaded, so JavaFX controllers should have prototype scope. It probably doesn't matter too much, but the @Controller annotation in Spring is really intended for MVC controllers in a web application, so the @Component stereotype is really more appropriate here:

@Component
@Scope("prototype")
public class AddingWindowController {

// ...

}

cannot inject a dependency in a javafx project

Youre not creating your domainhandler through guice and therefore the @inject annotations do nothing. If you want to create this object both ways just use constructor injection and remove the @inject from your fields.

@inject
public DomainHandler(Injector injector, WidgetFactory widgetFactory) {
this.injector = injector;
this.widgetFactory = widgetFactory
}

This way passing them in manually or using getInstance will do the same thing.

@inject on getters doesn't make sense, it can be used on getters but even in the docs is says not normally, and your passing it a null when you print it from your constructor.



Related Topics



Leave a reply



Submit