This week we’re doing a Live Stream on JavaFX, and I wanted to use that opportunity to refresh my JavaFX knowledge.
I wanted to investigate two areas in particular:
- How do the changes in Java 11 (when JavaFX was moved out of the JDK) impact JavaFX applications?
- How do I get a modern (Java 15) JavaFX application working as expected in IntelliJ IDEA (using either Maven or Gradle as the build tool)?
I now have six different applications that showcase JavaFX!
JavaFX and Gradle
Why did I start with Gradle? Because I have been struggling to migrate my JavaFX (Gradle) applications for a little while now, and I wanted to get the most basic one working first to help me figure out my next migration steps for the more complex applications.
These simple examples are all taken from the Getting Started guide on the openjfx site. The first thing I noticed is that there seems to be a number of different ways to configure your JavaFX application, and it’s a bit involved. I wanted to do the simplest thing that works so I can recommend to IntelliJ IDEA users which approach to take. Well. Of course the answer turned out to be “it depends”.
The simplest Gradle approach should be to use Gradle without any extra complications, such as trying to use the new Java Module System (introduced in Java 9). So, let’s try that. If you want to skip to the final code you can find it here.
You can follow the steps in the getting started guide (unfortunately I can’t link directly to the correct section, you’ll have to navigate to the link that says “Non-modular with Gradle” under “JavaFX and IntelliJ IDEA”)
(screenshot of docs page)
If you want to skip all the steps and just download the code, you can get the javafx-gradle-simple example from my GitHub repository.
To get this to work, you need to have at least JDK 11, which you can set as $JAVA_HOME if you want to run from the command line, or set as the project SDK in IntelliJ IDEA. However, I recommend using Java 15 since that’s the one I’ve tested this with:
{screenshot}
You can run it with:
/.gradlew run
from the project directory, or you can run via the Gradle tool window:
(screenshot)
OK now here’s the caveat:
- It doesn’t work running via IntelliJ IDEA’s run configurations.
That means you can’t use the green gutter icon on the HelloFX class, or the Shift Ctrl R shortcut, to run your application. It’s not a showstopper, because you can still use the other two methods, in fact you can use the Gradle version from run anything:
(screenshot)
and it appears in your run configurations too.
(screenshot)
It’s just a little frustrating if you’re used to running things from the editor instead of Gradle.
JavaFX and Gradle with Modules
If you want a seamless experience working with IntelliJ IDEA, JavaFX and Gradle, and you are free to make all the architectural decisions, you might want to create a modular application. What do I mean? I mean using Java’s new Module System, that was introduced in Java 9, to split your application into sections. The only real impact on a small application like the demo app we’re writing here is one additional file, the module-info.java. There are a number of subtleties for working with modules, which I won’t cover in this example but will cover in depth when I go over how I migrated my more complex applications, the main one you need to be aware of is that modular applications use the module path, NOT the classpath we know and love.
The getting started guide walks through creating an application like this (“JavaFX and IntelliJ” -> “Modular with Gradle”). However, if you’re using Gradle 6.7 or above (which you will be if you’ve created a Gradle application from IntelliJ IDEA 2020.3 like me), the javafx plugin doesn’t work, sigh. So you’ll need your build.gradle file to look like:
plugins {
id 'application'
}
group 'com.mechanitis'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
def javaFXPlatform = getJavaFXPlatform()
def javaFXVersion = "15.0.1"
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
implementation "org.openjfx:javafx-base:${javaFXVersion}:${javaFXPlatform}"
implementation "org.openjfx:javafx-controls:${javaFXVersion}:${javaFXPlatform}"
implementation "org.openjfx:javafx-graphics:${javaFXVersion}:${javaFXPlatform}"
}
test {
useJUnitPlatform()
}
application {
mainModule.set('com.mechanitis.demo.javafxgradle')
mainClass.set('com.mechanitis.demo.javafxgradle.HelloFX')
}
java {
modularity.inferModulePath.set(true)
}
private static String getJavaFXPlatform() {
def currentOS = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem;
if (currentOS.isWindows()) {
return 'win'
} else if (currentOS.isLinux()) {
return 'linux'
} else if (currentOS.isMacOsX()) {
return 'mac'
}
return null
}
See my javafx-gradle GitHub project for a very basic, fully working application. This application runs with:
/.gradlew run
from the command line, it runs from the Gradle tool window via the application -> run Task
(screenshot)
and you can run it from the HelloFX class file, either from the green arrows in the gutter, or via Shift Ctrl R or Ctrl Shift F10.
I recommend using Java 15 as the project SDK, that’s what I’m running this application on. However, it should also run with JDK 11 or higher.
(screenshot)
You may not want the added complication of using Java 9 modularity, for your application, if that’s the case see my other Gradle example, and check out the caveats & workarounds.
JavaFX and Maven
Right, so, you’d think that Maven might be simpler, given that it has had a slightly less bumpy ride with regards to both modularity and JavaFX, but we have different issues with Maven. Oh, and some of the same.
The short version is, the simplest way to get a working JavaFX application that uses Maven is the Maven archetype approach (specifically I was using this JavaFX Maven archetype). Using the quick-start, follow the instructions under “JavaFX and IntelliJ” -> “Modular with Maven” (or non-modular with Maven, at the time of writing these seem to both end up with the same result).
The nice thing about using archetypes is that all the basic code is generated for you and you don’t have to do anything. You should end up with a project like my javafx-maven-via-archetype code.
Note: this project also uses Java 9 modules. Again, this should be fine for a simple application, but it’s worth being aware of in case you run into any issues.
With this application you can run it with
mvn clean javafx:run
from the command line.
You can also run it from the green arrows in the gutter of App.java.
You will need to use Java 15 if you want to run my example application, I’ve specifically set that as the JDK in Maven.
OK now here’s the caveat:
- It may not work running via Maven’s tool window.
If your Operating System’s JAVA_HOME is lower than Java 15 (for example, mine’s set to Java 11), you’ll get an error trying to run javafx:run from the tool window:
(screenshot)
It may say something along the lines of “Unsupported major.minor version”. This is a known issue, and is mentioned in the getting started documentation, but I have raised an issue in the IntelliJ IDEA tracker.
Workaround: run from the command line or by running App.java inside IntelliJ IDEA.
Without Modules
I recognise (particularly after my experiences migrating an existing app) that not everyone wants or needs to use the new Java Module System with their new applications (although I recommend people give it a try). If this is the case, you can go ahead and create the app as above, but remove
module-info.java
and in pom.xml remove the module name from mainClass, i.e. change this:
<mainClass>com.mechanitis.demo.javafxmaven/com.mechanitis.demo.javafxmaven.App</mainClass>
to this:
<mainClass>com.mechanitis.demo.javafxmaven.App</mainClass>
(See also my without-modules branch)
Note:
If you use the no-modules approach, you can still run the application from the command line as before, but you will not be able to run it via the editor, i.e. you won’t be able to run App.java via IntelliJ IDEA.
Final Maven note: I use Maven in my Reactive Spring Boot tutorial, which has a JavaFX front end. I’ve upgraded it to use Java 15 and JavaFX 15.0.1, it does not use Java modularity and it seems to work correctly - both running from the command line and running the UI application class from IntelliJ IDEA.
I’ll be honest, I’m not sure why that quite complex application should work correctly, but it might be because Spring is taking care of lots of the dependencies. Feel free to dig around the code.
Migrating existing JavaFX applications (Spring Boot & Maven)
I migrated my Spring Boot demo application to use JavaFX 15.0.1 and Java 15. This started off as a Java 11 / JavaFX 13 application in the first place, so bringing it up to date didn’t require much work:
- Changed pom.xml to use the JavaFX plugin (previously it was just declaring JavaFX dependencies)
- Updated to JavaFX 15.0.1
- (I also had to set
<spring-boot.repackage.skip>true</spring-boot.repackage.skip>, but I’ll be honest I don’t think that’s anything to do with JavaFX since it was in a different module.) - Updated to Spring Boot 2.4.1 from 2.2.2. This meant I had to replace the use of the deprecated-and-then-removed
retryBackoffmethod in my client, but it also meant I could remove the exclusion of the junit-vintage engine. - Set Java version in pom.xml to Java 15. Made sure IntelliJ IDEA was also using this as the SDK and the language level.
- Final step, which was not required: using javafx-weaver to provide the JavaFX / Spring integration, instead of doing it all in the application. It made the code a bit simpler.
Migrating existing JavaFX applications (Gradle, no modules)
I have what is now quite an old demo application, originally designed to showcase Java 8 features. Since JavaFX was moved into the JDK in Java 8, I decided to use JavaFX as the UI. In fact, it’s this demo that was the basis for all my later JavaFX applications, including the Spring Boot one.
I haven’t done much with this for a loooong time, but it turns out I have been periodically updating dependencies and trying to make sure it all compiles. I highly recommend taking the time to do this at least once a year! It turns out that because I’ve been updating dependencies for the last 3-4 years that it wasn’t that painful to bring it right up to date. In fact, I did it in less than 30 minutes!
Steps:
- Upgraded to latest version of Gradle (6.8). When you do this, you want to make sure that not only do you do a full, clean rebuild from the command line, but you also want to reload the Gradle settings for IntelliJ IDEA and do a full rebuild inside the IDE.
- I had to change the way some file access was done. I’ve been fiddling with this for a few versions now, I think it’s because resource files are loaded differently since Java 11, and also I think IntelliJ IDEA loads resource files differently. So I opted to simplify what I was doing there.
- Updated versions of all dependencies in all my back end services to whatever is the most recent version.
- Updated my JavaFX Gradle plugin to 0.0.9. Apparently I was smart enough to start using the JavaFX plugin in early 2020, when I also upgraded the project to Gradle 6.3 and Java 11.
That was it, it worked!
Migrating existing JavaFX applications (Gradle with Java 9 Modules)
Compare and contrast that with my final JavaFX application, which was based off the last project and designed to showcase Java 9 functionality. Since it was being developed at the same time that Java 9 was developed, it was always a little… fragile. After I stopped using it to demo Java 9 features I tried to continue moving the project forward, for example finally fitting Gradle onto it, and fighting with all that modularity entailed in this brave new world. So my problems with this project were:
- Not having updated the code/dependencies in the main branch since August 2018
- Gradle um… fragility? around the later versions of Java in general and modularity specifically
- Not having invested enough time into having a stable, reproducable command-line build for the application
- Java 9 modularity in general
Early last year I made a stab at upgrading to Gradle 6.4 and upgrading everything else along the way. There’s a branch for that but to be honest I couldn’t face going back into it and working out what state it was, so I just started at the [latest state that main was in](https://github.com/trishagee/sense-nine/tree/43d9040a6174143d54eb22a21049b666c70d13a9. At this starting stage, with the current version of IntelliJ IDEA etc, nothing was working.
Steps:
Remove all modules from the application and focus only on the two “leaf” modules that don’t depend on anything else.
Instead of having a common build.gradle for all the modules, I moved all configuration into each individual module, even if that meant having duplication. This way I could get a much better handle on what was really needed and where the complications were.
Removed
ext.moduleNamefrom the build files, this didn’t seem to be required for modular Gradle applications any more.Make sure each module was using the correct plugin - either java-library or application
Added the following to all the build files:
java { modularity.inferModulePath = true }which is now the correct way to identify that this is a modular project.
Updated all dependencies (of just these two leaf modules to begin with) to the most recent versions.
Updated the module name in module-info.java for those automatic modules that now have real automatic module names.
Applied a Gradle plugin to give me some sort of control over automatic modules (i.e. dependencies that are not real Java Modules). This was not my favourite solution but it did pretty much work, although it makes the build file more complicated that I want it to be.
Repeat steps 3-7 with modules that depend only upon these leaf modules. Apply step 8 if required.
Repeat steps 3-7 with the next level of modules. Apply step 8 if required.
Once all the back-end service build files have been fixed, and they all compile and run correctly, then we can finally turn our attention to the JavaFX module.
As per the JavaFX and Gradle with Modules section, the Gradle build cannot use the JavaFX plugin because it’s going to use the updated Gradle support for modularity. I needed to create a
getJavaFXPlatformmethod in my build.gradle file. I also needed to apply step 8.I also had to mess around with where the resource files (FXML and CSS files) were loaded from. Resource loading seems to have changed (since Java 11?), and on top of that it may work differently with modularly. Also, while I could run the application successfully from the command line using Gradle, there was a problem with IntelliJ IDEA not letting the application get to the resource files when the application runs from the IDE. I’m not sure if that’s exactly this issue or something related, but it’s worth being aware of.
All modules: now use the newest dependency configurations (i.e.
implementationnotcompile) in build.gradle.Specifically declared Java 15 as a dependency in all build.gradle files using
toolchain { languageVersion.set(JavaLanguageVersion.of(15)) }Not sure if that actually worked to be honest, but everything still ran.
In the end, the application runs from the command line using Gradle:
./gradlew com.mechanitis.demo.sense.client:run
It does not run via the IntelliJ IDEA because of the aforementioned problem with loading resource files (FXML and CSS). However, you can run it via the Gradle run window, or running as a Gradle run configuration.
