— Technology, Java, JavaFX, Spring Boot — 4 min read
Time to throw my hat into the ring with a JavaFX and Spring Boot article. This information exists elsewhere, but I've accumulated and added what I've learned in the hopes:
As always, feel free to add some comments or push me in the right direction if you've come across this and see that I'm going in the completely wrong way.
The project (in all it's glory) can be found at https://github.com/kenjdavidson/gspro-connector
If it's not apparent, I love golf. Which means in the winter I love virtual golf. The last winter I've been playing E6 Connect using the SLX Micro Simulator and it's gotten me by. Although now that the primary game that I've been playing has disallowed the SLX Micro Sim, I figured I'd see if I could give GS Pro a shot; it seems to have substantially better reviews in terms of graphics and game play, just not internal simulator support.
It does offer the GS Pro Connect API which already have some plugins available for different monitors. Not that I have (nor maybe will ever have) the SLX Micro library, but here's dreaming. This project should be a fully working, pluginable, desktop application that will provide the UI and GS Pro Connect functionality to any willing to create a library plugin.
The source for this can be found at https://github.com/kenjdavidson/gspro-connect.
The project is initialized pretty quickly:
Ctrl-P
to open up the palette and choose Remote Container: Add Development Container Configuration Files
.devcontainer
with the following Java related extensions1"extensions": [2 "vscjava.vscode-java-pack",3 "Pivotal.vscode-boot-dev-pack",4 "GabrielBB.vscode-lombok",5 "redhat.vscode-xml"6 ],
Ctrl-P
> Remote Containers: Rebuild and Run Container
. This is where I started development on the API side of things (non-spring, non-fx).Just a note - Java and VSCode still still seem to have some issues, specifically regarding the Java Language Server and other Java Language plugins. Particularly when the devcontainer is first built.
I've found a couple things that help:
mvn clean install -DskipTests
to the container post build command. mvn install
runs.The project consists of a maven parent module gspro-connector
and a number of sub modules:
gspro-api
which contains the api data and connection functionalitygspro-client
(deprecated) wrapping the api connection gspro-app
which is the JavaFX/Spring Boot application. The application contains a basic form based launch monitor which can be used if no other plugin(s) are provided.gspro-api-garmin-r10
which is a sample plugin, the Python and Javascript version were first developed by Travis Lang and converted to Java to showcase how a plugin is used.Now that the API is pretty good, it's time to start on the interface portion. I know what you're thinking, this could have been a web app (desktop apps are uncool now); but realistically there is just much less to do:
But the main issue is Devcontainers have no desktop!!!
But thanks to a number of people smarter than I, this is manageable using the the Visual Studio Code feature desktop-lite. There are a number of great tutorials on how to get this working, so I'll just provide the short form here:
.devcontainer
with the required feature, desktop-lite, which as described Adds a lightweight Fluxbox based desktop to the container that can be accessed using a VNC viewer or the web. GUI-based commands executed from the built-in VS code terminal will open on the desktop automatically.
which is what we want:
1"features": {2 // Install desktop-lite (Fluxbox) on devcontainer3 // When starting GsProConnectApplicationBoot application loads into desktop4 // https://lucasjellema.medium.com/run-and-access-gui-inside-vs-code-devcontainers-b572643d0d2a 5 "desktop-lite": {6 "password": "vscode",7 "webPort": 6080,8 "vncPort": 59019 }10 },
Both are configured in this case, but I generally just use the web desktop:
1// Use 'forwardPorts' to make a list of ports inside the container available locally.2 "forwardPorts": [3 6080, // desktop-lite web4 5901, // desktop-lite vnc5 ],
Once the devcontainer is started, you'll be able to login to http://localhost:6080 with the password you provided (vscode in my case). Now when you start up your JavaFX application it will be run in the context of the VNC desktop.
The connect application provides a standardized GS Pro Connector
interface and the ability to choose from a number of installed LaunchMonitor
(s) using the LaunchMonitorProvider
interface. By default there is only the FormLaunchMonitor
installed, but these are easy to implement and add to the application by placing the compiled JAR in the class path.
One of the major issues run into across the web is getting the Spring Boot Application
to play nicely with the JavaFX Application
. There are a number of great tutorials on this:
Following a combination of these tutorials the resulting Application
classes look like this:
Spring Boot
The @SpringBootApplication
launches the GsProConnectApplication
(FX Application) during the boot process:
1@SpringBootApplication2public class GsProConnectApplicationBoot {3 public static void main(String[] args) {4 Application.launch(GsProConnectApplication.class, args);5 }6}
JavaFX Application
The Java FX Application
peforms a couple key functions:
1/**2 * Starts the application and fires off an `ApplicationStartupEvent` (which is responsible for building3 * the primary stage) and displaying it.4 */5 @Override6 public void start(Stage stage) throws Exception {7 logger.debug("Starting GsProConnectApplication");8
9 applicationContext.publishEvent(new ApplicationStartupEvent(this, stage));10 }11
12 /**13 * Initialize is responsible for completing the configuration of the `ApplicationContext`. During this process14 * a number of additional beans are applied using the `initializers()`:15 */16 @Override17 public void init() {18 logger.debug("Initializing Spring ApplicationContext");19
20 applicationContext = new SpringApplicationBuilder(GsProConnectApplicationBoot.class)21 .sources(GsProConnectApplicationBoot.class)22 .initializers(initializers())23 .run(getParameters().getRaw().toArray(new String[0]));24 }25
26 /**27 * Return an `ApplicationContextInitializer` which provides some of the specific JavaFX application28 * related beans: `Application`, `Parameters` and `HostServices`. This allows `Controller`(s) to 29 * be injected with application related items. 30 *31 * For example `HostServices` would allow a controller access to disk, etc.32 */33 ApplicationContextInitializer<GenericApplicationContext> initializers() { 34 return ac -> {35 ac.registerBean(Application.class, () -> GsProConnectApplication.this);36 ac.registerBean(Parameters.class, this::getParameters);37 ac.registerBean(HostServices.class, this::getHostServices);38 };39 }
The FXML controllers are implemented through the #setControllerFactory
providing the applicationContext::getBean
method. Controllers are setup as prototype
scope; this isn't really required (based on your application) but just be aware that if you are working with singleton
controllers and attempting to display multiple Views you're run into issues.
Stolen from another project the
ViewManager
provides the direct management of loading FXML views.
1public <T> T load(String view, Stage stage) throws IOException {2 String resource = validateViewPath(view);3 FXMLLoader loader = new FXMLLoader(getClass().getResource(resource), ResourceBundle.getBundle("i18n"));4 5 loader.setControllerFactory(applicationContext::getBean);6
7 // Need to find a way to make an fxml scope so that controller and builder will return the same8 // controller from the context, instead of needing the controller to be a singleton (bad) or 9 // having two different instances of the same class (also bad); since all examples for use of10 // fx:root use #setRoot(this); #setController(this); when performing the load.11 //loader.setBuilderFactory(builderFactory);12 13 T parent = loader.load(); 14
15 if (loader.getController() instanceof StageAware) {16 ((StageAware)loader.getController()).setStage(stage);17 }18
19 return parent;20}
This could have been more easily done with FXWeaver, which is on the todo list for implementation.
Services are designed pretty much how they would be normally.
One of the main issues that I ran into while getting this project going was the inability to use fx:root
to build the FXML components. There are a few ways around this that I found online, while JavaFX allows you to set the FXMLLoader#setControlllerFactory
and FXMLLoader#setBuilderFactory
to the context::getBean
this will result in two separate version of the class (ie. Not the same instance of Controller being the root). This can be worked around by using singleton
scope for your highest level components (application scene, etc) but that also has limitations (only a single window available, which might work for the majority of applications).
As long as you're following a pretty standard structure of:
singleton
Service(s) which are pretty standardprototype
View Controllers which get injected with singleton services fx:root
Which are just display Views (no injected services) 1@Scope('prototype')2public class Controller {3 @Inject DataService dataService;4 5 void buildList() {6 // MyCustomList created with `fx:root` extending the ListView7 ListView list = new MyCustomList(dataService.getItems());8 }9}
The cool thing about backing JavaFX with Spring Boot is that if you'd like to run the application as a service, you have the option of quickly adding some Web Controllers and modifying the Spring Boot startup so that instead of running the JavaFX Application
it runs the Web Application
.
Check out one of the next articles
Check out one of the next articles