Creating a Spring Boot JavaFX Application with FxWeaver
Update 2019-10-28 |
FxWeaver 1.3.0 released with Spring Boot Starter, auto-configuration and direct injection support. Blog post updated. |
In the first post in this article series I introduced the history and rationale behind FxWeaver. In this post I explain how to create a Spring Boot JavaFX application utilizing FxWeaver, and describe some notable FxWeaver usecases as well as what is going on behind the scenes.
FxWeaver Core is DI framework agnostic, it can be used with any bean management framework around. In this example, we will use FxWeaver Spring, which is just a thin convenience wrapper that knows how to deal with Spring’s
.ApplicationContext
The source code in this post is based on the FxWeaver 1.3.0 Spring Boot Sample found on GitHub.
Setup
Manual setup
Create a simple Spring Boot project. Add JavaFX dependencies as required.
Then add
in the desired version:javafx-weaver-spring
For Maven:
<dependency>
<groupId>net.rgielen</groupId>
<artifactId>javafx-weaver-spring</artifactId>
<version>1.3.0</version>
</dependency>
For Gradle:
implementation 'net.rgielen:javafx-weaver-spring:1.3.0'
Spring Boot Starter
Since version 1.3.0, a Spring Boot Starter is available.
It introduces the
dependency as well as an autoconfiguration module.
To use it, include the javafx-weaver-spring
dependency.javafx-weaver-spring-boot-starter
For Maven:
<dependency>
<groupId>net.rgielen</groupId>
<artifactId>javafx-weaver-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
For Gradle:
implementation 'javafx-weaver-spring-boot-starter:1.3.0'
If using the the starter, it is not necessary to provide a
bean as described in the FxWeaver bean provisioning example, unless you want to customize it further.
Auto-configuration takes care of configuring a suitable FxWeaver
instance.FxWeaver
Same goes with direct injection factory bean for
references as described in the direct injection configuration example.
The starter provides auto-configuration for such a factory, such that FxControllerAndView
injection can be used out of the box.FxControllerAndView
Check out the dedicated example using the Spring Boot Starter and a reduced setup in general.
Bootstrap
The bootstrap process is heavily inspired by Mr. Awesome Josh Long’s Spring Tips: JavaFX installment.
The main class looks a bit different than usual:
@SpringBootApplication
public class JavafxWeaverSpringbootSampleApplication {
public static void main(String[] args) {
Application.launch(SpringbootJavaFxApplication.class, args); (1)
}
@Bean
public FxWeaver fxWeaver(ConfigurableApplicationContext applicationContext) {
// Would also work with javafx-weaver-core only:
// return new FxWeaver(applicationContext::getBean, applicationContext::close);
return new SpringFxWeaver(applicationContext); (2)
}
}
1 | Instead of calling , use a custom bootstrap class inheriting from JavaFX . This is needed to initialize JavaFX correctly |
2 | Provide a bean for making weaving functionality accessible.
Can be either a plain instance or a more convenient . This is not needed when using the Spring Boot Starter , since it provides auto-configuration for a instance. |
It is accompanied by the
which does the heavy lifting for creating a proper JavaFX application with initialized Spring context.
The actual sample source is a little bit more elaborate, but essentially it boils down to:SpringbootJavaFxApplication
public class SpringbootJavaFxApplication extends Application {
private ConfigurableApplicationContext context;
@Override
public void init() throws Exception {
this.context = new SpringApplicationBuilder() (1)
.sources(JavafxWeaverSpringbootSampleApplication.class)
.run(getParameters().getRaw().toArray(new String[0]));
}
@Override
public void start(Stage primaryStage) throws Exception {
context.publishEvent(new StageReadyEvent(primaryStage)); (2)
}
@Override
public void stop() throws Exception { (3)
this.context.close();
Platform.exit();
}
}
1 | Programmatically create a Spring Boot context in the method. |
2 | Kick off application logic by sending a containing the primary Stage as payload. |
3 | Support graceful shutdown for both Spring context and JavaFX platform |
Using FxWeaver
Create Main Window with a weaved View
We are now ready to create our main application window (aka Scene), and it can be done within a Spring managed bean consuming the
emitted earlier:StageReadyEvent
@Component
public class PrimaryStageInitializer implements ApplicationListener<StageReadyEvent> {
private final FxWeaver fxWeaver;
@Autowired
public PrimaryStageInitializer(FxWeaver fxWeaver) { (1)
this.fxWeaver = fxWeaver;
}
@Override
public void onApplicationEvent(StageReadyEvent event) { (2)
Stage stage = event.stage;
Scene scene = new Scene(fxWeaver.loadView(MainController.class), 400, 300); (3)
stage.setScene(scene);
stage.show();
}
}
1 | Use constructor injection to get a reference |
2 | Consume , which contains the applications primary stage as payload |
3 | Use to obtain a based on the annotation found in
|
Here is where we see
in action for the first time.
To get the full picture, we need have a look at the important parts of FxWeaver
as well:MainController
package net.rgielen.fxweaver.samples.springboot.controller;
@Component
@FxmlView // equal to: @FxmlView("MainController.fxml") (1)
public class MainController {
private final String greeting;
@FXML (2)
private Label label;
// ...
public MainController(@Value("${spring.application.demo.greeting}") String greeting) { (3)
this.greeting = greeting;
}
// ...
}
1 | Declare that a FXML view belongs to this class.
If no value provided, infer it to be <Simple Class Name>.fxml in the same package.
As configured here, the declared expectation is to find in
|
2 | In a correctly instantiated JavaFX controller class bound to an FXML view definition via , elements defined in FXML can be bound to controller fields annotated with .
Expect to take care of this. |
3 | This is also a Spring managed bean, so takes care that the JavaFX controller factory utilizes Spring for bean creation and management. |
Also, let’s look at the FXML view definition:
<VBox xmlns:fx="http://javafx.com/fxml" spacing="10" alignment="CENTER"
fx:controller="net.rgielen.fxweaver.samples.springboot.controller.MainController"> (1)
<Label fx:id="label"/> (2)
</VBox>
1 | Declare the controller class to be instantiated with the view.
This is where is supposed to help, such that Spring is used for instantiation during FXML load mechanism. |
2 | A Label component that get’s injected into the controller’s field based on the annotation and field name matching value in attribute. |
What FxWeaver actually does
When calling one of the FxWeaver
methods supplying a controller class, load*
does the following:FxWeaver
-
Introspect controller class for existence of
annotation@FxmlView
-
Infer the FXML resource location by either taking the exact name provided as
value attribute or by using the simple classname plus@FxmlView
suffix. If not referencing an absolute path within the classpath, it is assumed that the resource is located in the same package as the controller class.fxml
-
Construct a
and set theFXMLLoader
, if provided, and the controller factory. The controller factory used will be the bean creation function provided to theResourceBundle
constructor. In case of Spring, this isFxWeaver
applicationContext::getBean
-
Let
load the FXML view resource, and once it contains aFXMLLoader
attribute, let it instantiate the controller instance by using the provided controller factory. Along the way,fx:controller
will also take care of injectingFXMLLoader
annotated fields.@FXML
-
Return either
-
the controller instance when using
methods<C> C loadController(Class<C> controllerClass …)
-
the view instance when using
methods<V extends Node, C> V loadView(Class<C> controllerClass …)
-
or both when using
methods.<V extends Node, C> FxControllerAndView<C, V> load(Class<C> controllerClass …)
-
Any
thrown during loading is wrapped in a more usefulIOException
deriving fromFxLoadException
RuntimeException
-
Make the Controller responsible for showing the View
By being able to obtain a controller instance with a weaved FXML view, a controller can easily be enhanced by a
method that can be called from the outside.show()
@Component
@FxmlView
public class MainController {
private final FxWeaver fxWeaver;
@FXML
private Button openSimpleDialogButton;
public MainController( FxWeaver fxWeaver) {
this.fxWeaver = fxWeaver;
}
@FXML
public void initialize() {
openSimpleDialogButton.setOnAction(
actionEvent -> fxWeaver.loadController(DialogController.class).show() (1)
);
}
}
1 | Obtain a controller instance weaved with its view and call the method |
@FxmlView("SimpleDialog.fxml") (1)
@Component
public class DialogController {
private Stage stage;
@FXML
private VBox dialog;
@FXML
public void initialize() { (2)
this.stage = new Stage();
stage.setScene(new Scene(dialog));
}
public void show() {
stage.show(); (3)
}
}
1 | Use a custom FXML resource |
2 | Initialize a new stage with the controller bean and create a scene containing the root node element of the given FXML view (VBox in this case) |
3 | Show the stage |
<VBox fx:id="dialog" alignment="CENTER" prefHeight="200.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/8.0.232-ea"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="net.rgielen.fxweaver.samples.springboot.controller.DialogController">
<Label text="Hello!"/>
</VBox>
SpringFxWeaver: Directly Inject a FxControllerAndView
Reference
FxControllerAndView
From 1.3.0 on
supports direct injection for javafx-weaver-spring
references, based on their generic typing.FxControllerAndView
To use this feature, a suitable bean factory method has to be provided. This can be done by using the Spring Boot Starter which provides auto-configuration for such a bean, or by proving it manually as follows:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) (1)
public <C, V extends Node> FxControllerAndView<C, V> controllerAndView(FxWeaver fxWeaver,
InjectionPoint injectionPoint) {
return new InjectionPointLazyFxControllerAndViewResolver(fxWeaver)
.resolve(injectionPoint);
}
1 | For the inspection of the injection point to work for each injection point, the bean definition must be protopye scoped. |
Based on the injection point definition, generic types will be resolved to actual types to be used for the actual weaving. A
instance will be provisioned, to do the actual FXML loading and weaving on the GUI thread. Please note that LazyFxControllerAndView
is a class name in the best tradition of long but expressive identifiers in the Spring Framework ;)InjectionPointLazyFxControllerAndViewResolver
Given that, a component consuming a
may be defined like this:FxControllerAndView
@Component
@FxmlView
public class DialogController {
private Stage stage;
@FXML
private Button openAnotherDialogButton;
@FXML
private VBox dialog;
private final FxControllerAndView<AnotherDialog, VBox> anotherControllerAndView; (1)
public DialogController(FxControllerAndView<AnotherDialog, VBox> anotherControllerAndView) { (2)
this.anotherControllerAndView = anotherControllerAndView;
}
@FXML
public void initialize() {
this.stage = new Stage();
stage.setScene(new Scene(dialog));
openAnotherDialogButton.setOnAction(
actionEvent -> anotherControllerAndView.getController().show() (3)
);
}
public void show() {
stage.show();
}
}
1 | Operate directly on a instance rather than an injected instance |
2 | Use constructor based injection based on the generic types of the contructor parameter |
3 | Directly use the reference to show the dialog. The actual FXML loading and weaving is done now on the GUI thread, since the reference is actually a . |
Your IDE might tell you otherwise, but the actual injection based on generic types does work. This pattern might be helpful to enhance testability.
Manipulating the View after loading
By retrieving both the view and the controller from
, a view can be manipulated before requesting the controller to show it.FxWeaver
@Component
@FxmlView
public class MainController {
private final FxWeaver fxWeaver;
@FXML
private Button openTiledDialogButton;
public MainController( FxWeaver fxWeaver) {
this.fxWeaver = fxWeaver;
}
@FXML
public void initialize() {
openTiledDialogButton.setOnAction(
actionEvent -> {
FxControllerAndView<TiledDialogController, VBox> tiledDialog =
fxWeaver.load(TiledDialogController.class);
tiledDialog.getView().ifPresent(
v -> {
Label label = new Label();
label.setText("Dynamically added Label");
v.getChildren().add(label); (1)
}
);
tiledDialog.getController().show(); (2)
}
);
}
}
1 | Obtain the view, and if present, programmatically add a label to it |
2 | Use the controller show method to display the dialog |
Tiled Views, (re-)using independent Components
FXML’s
mechanism is fully supported in fx:include
.
View tiles can have independent controllers are correctly managed and injected by both Spring and FxWeaver
.
SceneBuilder is fully supported.FXMLLoader
@Component
public class TiledDialogController {
private Stage stage;
@FXML
private VBox dialog;
@FXML
private Button closeButton;
@FXML
public void initialize() {
this.stage = new Stage();
stage.setScene(new Scene(dialog)); (1)
}
public void show() {
stage.show();
closeButton.setOnAction(
a -> stage.close()
);
}
}
1 | Create and use the "master view" as usual |
<VBox fx:id="dialog" alignment="CENTER" prefHeight="200.0" prefWidth="200.0" spacing="10"
xmlns="http://javafx.com/javafx/8.0.232-ea"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="net.rgielen.fxweaver.samples.springboot.controller.TiledDialogController">
<fx:include source="tiles/SimpleTileController.fxml"/> (1)
<Button fx:id="closeButton" mnemonicParsing="false" text="Close"/>
</VBox>
1 | Use to embed another view defined using FXML |
<VBox alignment="CENTER" xmlns="http://javafx.com/javafx/8.0.232-ea"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="net.rgielen.fxweaver.samples.springboot.controller.tiles.SimpleTileController" (1)
style="-fx-background-color: #ffffff">
<Label fx:id="label" text="A Simple Tile"/>
<Button text="Do nothing"/>
</VBox>
1 | The view tile declares its own controller bean, which gets instantiated and managed correctly and automatically |
@FxmlView
@Component (1)
public class SimpleTileController {
@FXML
Label label;
@FXML
public void initialize() {
label.setText(label.getText() + " initialized");
}
}
1 | The weaved controller instance will be a fully managed Spring bean |
If used like this, view tiles can also be re-used, even as standalone views.
Contributing
Feel free to open issues and pull requests on GitHub. This is a side project of mine, so please don’t expect enterprise grade support.