Header Image

The latest version of springboot-javafx-support using spring boot 1.x is 1.4.3. When using spring boot 2 please use 2.1.2. See Changelog. You need Java 1.8.0 40 or higher to use it.

Spring Boot and JavaFx8

Veröffentlicht von Felix am 16.03.2017 17:42:47.
Zuletzt geändert am 03.04.2017 21:20:22.

Spring
Frontend
Java
JavaFx

JavaFX is a modern library to write beautiful frontends in Java in a declarative way with FXML. Because in the ideal world, there should be a strict separation between the view and the logic in your code. But writing a GUI in plain FXML is ugly and error prone. We want to use tools, where we can immediately see, how our interface will look like and that allow us to simply build a graphical user interface by drag and drop and moving and aligning elements around. scenebuilder is this tool. On the other side of the code we want to use all the great and fancy stuff that Spring provides us. In the past it wasn't easy to use both together. A lot of boilerplate code needed to be written. The springboot-javafx-support library combines those two worlds to write modern frontend applications in Java. This tutorial will not cover JavaFX in general or the features of scenebuilder. You'll find a lot of good books about JavaFX out there. The following examples concentrate only on how to use springboot-javafx-support. And here is how it works:

Part I: Hello World

Let's start with a simple HelloWorld application. For your convenience, you find the complete code of this tutorial here: https://github.com/roskenet/spring-javafx-examples

Prepare the project

First create a standard maven project and add the necessary dependencies to pom.xml. It should look like this:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
    <groupId>de.roskenet.spring-fx-examples</groupId>
    <artifactId>part-1</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.5.2.RELEASE</version>
      <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <java.version>1.8</java.version>
      <springboot-javafx.version>1.3.25</springboot-javafx.version>
    </properties>

    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
      </dependency>
      <dependency>
        <groupId>de.roskenet</groupId>
        <artifactId>springboot-javafx-support</artifactId>
        <version>${springboot-javafx.version}</version>
      </dependency>
     </dependencies>

     <build>
       <plugins>
         <plugin>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
       </plugins>
     </build>
   </project>

Create a view

If you haven't done so yet, download scenebuilder from http://gluonhq.com/products/scene-builder/ and run it. We will create a Pane element with a welcome text in a Label for now. Make the text a bit larger and style your nice welcome text as you like it. We save the panel in our project under src/main/resources/example/helloworld.fxml

Register your view

Now we create a Java class that will represent our view and will do the whole FXML-magic for us. And even more than this: This class will be a real Spring bean.

Create a class called HelloworldView that extends AbstractFxmlView and annotate it with @FXMLView.

Your class should look like this:

package example;

import de.felixroske.jfxsupport.AbstractFxmlView;
import de.felixroske.jfxsupport.FXMLView;

@FXMLView
public class HelloworldView extends AbstractFxmlView {

}

Create a starter class

Last thing we need to do is to create a spring boot starter class.

It's as easy as this:

package example;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import de.felixroske.jfxsupport.AbstractJavaFxApplicationSupport;

@SpringBootApplication
public class Main extends AbstractJavaFxApplicationSupport{

    public static void main(String[] args) {
	        launchApp(Main.class, HelloworldView.class, args);
    }
}

Start your app... Wow! This is obviously going to be the next big thing!

Part II: Interaction

O.k. Now we know how to link an FXML-File to a view class. Let's look how users can interact with our application. You find the following example under part_2 in the spring-javafx-examples repo.

Open helloworld.fxml again in scenebuilder and add a text field and a button. Add ids to all elements and add a controller class.

Add the controller

Now we implement the controller class that will be responsible for our HelloworldView. Besides the @FXMLController annotation this is simple Java and JavaFX code. In the end it should look like this:

package example;

import org.springframework.beans.factory.annotation.Autowired;

import de.felixroske.jfxsupport.FXMLController;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

@FXMLController
public class HelloworldController {

    @FXML
    private Label helloLabel;
	  
    @FXML
    private TextField nameField;
				      
    // Be aware: This is a Spring bean. So we can do the following:
    @Autowired
    private AwesomeActionService actionService;
								      
    @FXML
    private void setHelloText(final Event event) {
         final String textToBeShown = actionService.processName(nameField.getText());
         helloLabel.setText(textToBeShown); 
    }
}

As you can see, we have a bean of type AwesomeActionService in our ApplicationContext. And because our controller class is a spring bean, too, we can easily autowire it. If you have set up the ids correctly in scenebuilder and added the controller class correctly (take a look into helloworld.fxml) then the label should become interactive.

Part III: Style your views

In this section we want to apply style sheets to our view. You find the example code in the examples subdirectory named part_3. In general styling is done by usual JavaFX means. You simply add your css file with scenebuilder to your view as I did with global.css in the example.

Add per view style

To make applying styles even more convenient for you, you can add additional style by adding a .css file with the name of your fxml file. In the example helloworld.css will be loaded automatically. A third way is to provide an array of style sheets to the @FXMLView annotation (Not covered in this example). Just add @FXMLView(css={"/css/myspecialstyle.css"}) - and that's it.

Part IV: i18n

Resource Bundles

In part_4 we want to add internationalization (i18n) to our views. This is as easy as adding a parameter bundle to the @FXMLView annotation and to modify your fxml in scenebuilder accordingly.

@FXMLView(bundle="example.helloworld")
public class HelloworldView extends AbstractFxmlView {

}

Everything else is again usual JavaFX convention: Create resource files called /src/main/resources/example/helloworld.properties and /src/main/resources/example/helloworld_de.properties. The German one looks like:

hello=Hallo
greeting=Hallo {0}!
go=Los!

And that's all you need to do to let our helloworld speak German when started with LC_ALL=de_DE.UTF-8 ... At least partially:

Part V: Testing

The next part is about testing our view/controller logic. This is not springboot-javafx specific, but setting up a GUI testing environment can be cumbersome sometimes. springboot-javafx-test provides all necessary dependencies and a GuiTest class to help you testing your app. It provides testfx and monocle. You find the code for this part of the tutorial under part_5.

Test your view

Add the dependency to your pom.xml with scope test like this:

<dependency>
    <groupId>de.roskenet</groupId>
    <artifactId>springboot-javafx-test</artifactId>
    <version>0.0.4</version>
    <scope>test</scope>
</dependency>

Now let's write a short test for the HelloworldView and HelloworldController classes, that we wrote in part_2. When the String "Anton" was entered, then the Label component changed to "Hello Anton!" while any other string showed "Hello Unknown Stranger!". Add a class HelloworldViewTest:

package example;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import javax.annotation.PostConstruct;

import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import de.roskenet.jfxsupport.test.GuiTest;
import javafx.application.Platform;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloworldViewTest extends GuiTest {

    @PostConstruct
    public void constructView() throws Exception {
        init(HelloworldView.class);
    }
    
    @Test
    public void testClickButton_Anton() {
        clickOn("#nameField")
            .write("Anton")
            .clickOn("#helloButton");

        assertThat(labelText(), is("Hello Anton!"));
    }

    @Test
    public void testClickButton_Berta() {
        clickOn("#nameField")
            .write("Berta")
            .clickOn("#helloButton");

        assertThat(labelText(), is("Hello Unknown Stranger!"));
    }
    
    private String labelText() {
        return ((Label) find("#helloLabel")).getText();
    }
    
    @After
    public void resetValues() {
        // You are responsible for cleaning up your Beans!
        Platform.runLater(() -> {
            TextField helloLabel = (TextField) find("#nameField");
            helloLabel.setText("");
        });
    }

}

TestFX has a nice fluent API where you can concatenate the actions to be performed. There are only two things you need to be aware of: First, the class extends GuiTest and second has a method annotated with @PostConstruct. Here we call GuiTest::init with the view class that we want to test. init is overloaded. When you need direct access to fields or methods in your view, then you can @Autowire your view class and call init with the class' instance (See in HelloworldView2Test. Run the tests and you see TestFX and our app in action.

Testing headless

The last thing that we need to solve is that our tests won't run in our continuous integration environment where we don't have a running user interface like an X-server or Windows. We need to run the tests in headless mode. No problem: Set an environment variable JAVAFX_HEADLESS=true and your tests should run silently without any screen.

Part VI: Advanced techniques

Using Java Code

Sometimes you want or even need to create your view by good old Java code. This can be done by just overriding the getView method from AbstractFxmlView to return a Node, as I have done in this example (part_6_1):

@FXMLView
public class HelloworldJavaView extends AbstractFxmlView {

    private Pane myJavaCodedPane;
	    
    public HelloworldJavaView() {
       Pane pane = new Pane();
       Button button = new Button("A Button");
						         
       pane.getChildren().add(button);
       myJavaCodedPane = pane;
    }
												       
    @Override
    public Parent getView() {
       return myJavaCodedPane;
    }
}

A remark: To gain the most flexibility that is possible you could even call super.getView() first and modify your FXML-generated view with pure Java code.

Change the view

When you want to change your Scene completely, you simply call the showView method of your application as demonstrated in example part_6_2. To make the example as small as possible both views use the same controller class here.

@FXMLController
public class ViewController {
    
    public void showFirstView(Event event) {
        Main.showView(FirstView.class);
    }

    public void showSecondView(Event event) {
        Main.showView(SecondView.class);
    }
}

By the way: You have all the time full access to your JavaFX Stage and Scene objects by calling the static accessor methods MyApp.getStage() and MyApp.getScene() respectively.

Configuration options

Since version 1.3.11 you can configure many things of your main stage with the javafx.* properties. By default the library provides a set of icons in different sizes. Add your own set of application logos to src/main/resources and let the library know that they exists by adding them to your spring application.yaml or application.properties. Add a default title and configure the initial state of your app when needed like this:

javafx:
    title: My cool Application
    appicons:
        - /appicon1.png
        - /appicon2.png
        - /appicon3.png
    stage:
        width: 400 # defaults to the size of the scene
        height: 300
        resizable: false # defaults to true
        style: utility # defaults to DECORATED see: javafx.stage.StageStyle

JavaFx determines the icon sizes automatically and uses the best resolution depending on the output device.

Customizing splash screen

To customize the splash screen while spring boot bootstraps the application context you can extend SplashScreen and call launchApp(Main.class, HelloworldView.class, mySplashScreen, args).