A few days ago, for the Java User Group Trentino-Alto Adige-Südtirol I shown some experiments on JavaFx, JLink and JPackage. The purpose of the talk was to show the chance to use JavaFX working with new JDK tools such as JLink (since OpenJDK 9) and JPackage (provided by the new OpenJDK 16) for rich client development.
Although some may state «JavaFX is dead» since it's not part of the OpenJDK anymore, JavaFX is actually alive and kicking. OpenJFX (aka Open JavaFX) is a side project still maintained and evolved by Oracle, Gluon, BellSoft and others. Suddenly, because of the advent of OpenJDK, Adoptium and other free OpenJDK builds, it may not be that straightforward to understand how to get started with JavaFX, especially if you still working on an Oracle Jave SE 1.8.0 distribution. Let's try to get there.
Before starting
To grasp the example, you need:
- Git
- Apache Maven
- Sdkman
- a Linux shell or a Wsl2 shell for Windows (sorry I know nothing about MacOS)
- an editor for the pom file (even
Vim
is fine, but I don't remember how to exit from it, so don't ask me).
DISCLAIMER
Keep in mind that I will write regarding module-info
and Java Platform Module System, but I won't explain too much about them.
The goal of this post is not about how you have to organize a project, but how to make the tools work for you. I will instead write something more about JPMS in a future post (it is quite intriguing and intricated).
What is JavaFX?
JavaFX is a framework of graphics and media packages that enables developers to create and deploy rich client applications.
Example
Let's take a basic JavaFX example, for instance, this one:
https://gitlab.com/lucaguada/treefx.
The source code is pretty old and is a JavaFX 2.1 example taken from here: Oracle Docs.
I configured the pom.xml
as simple as possible, or at least this was my intention. I just set two plugins: maven-compiler-plugin
to avoid compilation issues when we advance the Java version during our tests and exec-maven-plugin
for being able to run our application from the terminal.
Oracle Java SE to OpenJDK
As I mentioned before, JavaFX is not part of the OpenJDK anymore. Let's see if such a boomer like me is correct.
Download the tar.gz
file of Oracle Java SE 1.8.0 from here: Oracle Java and don't worry about the license, the FAQ explicitly states this:
Oracle Java SE8 updates, which includes the Oracle JRE with Java Web Start, continue to be free for personal use, development, testing, prototyping, demonstrating and some other important uses explained in this FAQ under the OTN License Agreement for Java SE. Personal users can continue downloading the Oracle Java SE 8 JRE at java.com.
Unzip the package in the .sdkman/candidates/java
folder. You should be able to check the OracleJDK by typing:
$ sdk list java
Sdkman should list the Oracle Java SE as follow:
Unclassified | | 8.0.281 | none | local only | 8.0.281-oracle
Nice! Therefore we can now compile our project:
$ sdk use java 8.0.281-oracle
Using java version 8.0.281-oracle in this shell.
$ mvn clean compile exec:java
Nice twice! However, we don't want to have a headache for Oracle licenses/subscriptions/etc., so we now change the OracleJDK with an Open one and compile again.
$ sdk install java 8.0.272.hs-adpt
... install process by sdkman ...
$ sdk use java 8.0.272.hs-adpt
Using java version 8.0.272.hs-adpt in this shell.
$ mvn clean compile exec:java
and tragically:
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
Errors, errors everywhere! TreeFX can't find any package of the framework JavaFX! How to solve this? JavaFX is now a set of self-contained dependencies, so let's modify the pom.xml
file by defining the right dependencies.
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>16</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>16</version>
</dependency>
</dependencies>
and then compile again. But... again...
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project treefx: Compilation failure
[ERROR] /home/.../treefx/src/main/java/io/trydent/treefx/Flower.java:[7,20] cannot access javafx.scene.Group
[ERROR] bad class file: /home/.../.m2/repository/org/openjfx/javafx-graphics/16/javafx-graphics-16-linux.jar(javafx/scene/Group.class)
[ERROR] class file has wrong version 55.0, should be 52.0
[ERROR] Please remove or make sure it appears in the correct subdirectory of the classpath.
No more errors about missing packages, but class file version! What does it mean? Every time the Java Compiler compiles your code, it annotates your classes with a number to identify the target JDK release. In this case, the class version 55.0 targets JDK 11, but we're using JDK 8, therefore the class version number should be at a maximum of 52.0.
Unfortunately, there is no way to downgrade JavaFX to a version compatible with JDK 8. Sure, as someone already suggested to me, we can download Liberica OpenJDK 8 with JavaFX (or the Azul one) however, we will be forever stuck to JavaFX 8. Being locked to JavaFX 8 doesn't allow us to benefit from having bug fixes and other kinds of improvements.
It's time to upgrade our OpenJDK to 11.
OpenJDK 11
Once we set our JavaFX dependencies in the pom.xml
, we can try to switch to an OpenJDK 11:
$ sdk use java 11.0.10.hs-adpt
Using java version 11.0.10.hs-adpt in this shell.
$ mvn clean compile exec:java
And everything will be fine. No more errors about missing packages and class versions.
Because our application is ready, we can package it into a self-executable-jar and release it to our customers/friends/pets (well, if they are all interested in Tree animations, of course). By doing so, we face an old-fashioned issue: we depend on the customer/friend/pet's installed JDK. We can't upgrade our development JDK (let's think about new API's and runtime improvements, etc.) until our customers don't upgrade their own. Yes, we could package the application with a JRE, but this would lead to having a pretty big sized ZIP for just one animation.
JLink, the Java Linker
As explained in JEP 282, JLink is a tool that can assemble modules and their dependencies into a custom Java runtime image. Since the JRE, JDK and some libraries/frameworks are now restructured in modules, we can create a custom JRE image for our wonderful JavaFX application!
JavaFX is indeed modularized, so we can try!
Before starting, though, I have to inform you that I skipped the official Maven JLink Plugin usage. In a draft of this article, I explained how to work with the JLink plugin and JavaFX, but I realized it was far too long and not very useful in practice (for this example, at least). Instead of the Maven JLink Plugin, we'll use the official JavaFX JLink Plugin provided by the JavaFX development team to simplify the process.
Let us modify the pom.xml
and append a Maven plugin for JavaFX and JLink:
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.5</version>
<configuration>
<compress>2</compress>
<noHeaderFiles>true</noHeaderFiles>
<stripDebug>true</stripDebug>
<noManPages>true</noManPages>
<launcher>treefx</launcher>
<mainClass>treefx/com.acme.treefx.TreeFX</mainClass>
<jlinkImageName>treefx</jlinkImageName>
<jlinkZipName>treefx</jlinkZipName>
</configuration>
</plugin>
The configuration is not quite self-explanatory, here some hints:
- compress enables compression of resources, we set to 2 for compressing to ZIP
- noHeaderFiles excludes C header files for supporting native-code programming (we don't need that for our custom JRE)
- stripDebug excludes JRE debug information (yeah we don't need this too)
- noManPages excludes the JDK docs
- jlinkImageName the runtime final name
- jlinkZipName the ZIP final name
- launcher the executable name
-
mainClass what
main
needs to be launched by specifying module, package and class.
We set all such exclusions for shrinking our custom JRE to a smaller size than the usual one.
However, if we try the new plugin command:
$ mvn clean compile javafx:jlink
Something is still missing:
[ERROR] Failed to execute goal org.openjfx:javafx-maven-plugin:0.0.5:jlink (default-cli) on project treefx: Error: jlink requires a module descriptor
As I said before if JDK, JRE and JavaFX are now modularized, this implies that we need to modularize our application too. Create a new file named module-info.java
in the folder src/main/java
:
module treefx {
requires javafx.controls;
requires javafx.media;
opens com.acme.treefx to javafx.graphics;
}
The file defines our application as a module named treefx
and our module requires some modules as well in order to work, javafx.controls
and javafx.media
.
I don't want to explain too much about it, but keep in mind that modules are a new way to organize your applications and libraries and enable strong encapsulation and implementation/reflection hiding. Knowing this, what about that last statement opens
in module-info
?
There are some cases where you have to enable reflection (because of modules, reflection is inaccessible by default) for your classes, because of frameworks for instance. In our case we have to enable the reflection of our package com.acme.treefx
to the module javafx.graphics
to let JavaFX work.
Then once again:
$ mvn clean compile javafx:jlink
... finally ...
[INFO] Building zip: /home/.../treefx/target/treefx.zip
Yeeeah our application is ready to be propagated to Knowhere (cit.)!
Let's take a look at what actually happened in the target folder:
$ tree target/treefx
target/treefx
├── bin
│ ├── java
│ ├── keytool
│ └── treefx
├── conf
│ ├── [...]
├── legal
│ ├── [...]
├── lib
│ ├── [...]
└── release
What do we have in the generated treefx.zip
? As we can see, it's just a JRE with our application encapsulated inside. In folder bin
, we can find the launcher treefx
.
And the size of the ZIP file? Just 47Mb! The best part? If our application only needs the modules we declared in module-info
, the size will increase only if our application grows in terms of compiled classes.
Nice trice, right? But we want more! For the sake of aesthetics, we prefer to distribute our application with an official deb
package how can we do this?
It's time to upgrade our OpenJDK to 16!
OpenJDK 16 and JPackage
OpenJDK 16 has been released on 16th March. However, this is not the right post to talk about all the new shining features and tools, but it is the right post to talk about a new particular tool: JPackage.
JPackage was introduced as an incubating tool from JDK 14 to JDK 15 and is now production-ready for packaging self-contained Java applications.
As above we want to rely on Maven and fortunately, we got a well-packed plugin (JPackage Plugin) on Maven Central that we can add:
<plugin>
<groupId>org.panteleyev</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<name>TreeFX</name>
<appVersion>1.0.0</appVersion>
<vendor>com.acme</vendor>
<destination>target/dist</destination>
<module>treefx/com.acme.treefx.TreeFX</module>
<runtimeImage>target/treefx</runtimeImage>
<linuxShortcut>true</linuxShortcut>
<linuxPackageName>treefx</linuxPackageName>
<linuxAppCategory>Utilities</linuxAppCategory>
<linuxMenuGroup>Utilities</linuxMenuGroup>
<icon>${project.basedir}/duke.png</icon>
<javaOptions>
<option>-Dfile.encoding=UTF-8</option>
</javaOptions>
</configuration>
</plugin>
I have to spend some words about the configuration because it's a little bit tricky on some points.
Easy part
- name sets the application name (you don't say!)
- appVersion as any other application, our TreeFX has a release version too!
- vendor sets the vendor or our Acme company
- destination sets the destination folder for the generated package
Tricky part
- module we set the module and the main class of our application since JPackage will generate a new executable for our package
- runtimeImage is usually set to our development OpenJDK, because JPackge could work with JLink to create the custom JRE, but because of some issues I found with JavaFX and JLink (the process is not straightforward), we trick JPackage and we tell it to take the previous custom JRE created with the OpenJFX plugin
Linux part (I put it just for the sake of completeness)
- linuxShortcut creates a shortcut after the application installation process
-
linuxPackageName sets the final
deb
package name - linuxAppCategory sets the Linux application category
- linuxMenuGroup sets the Linux menu-group
Maybe I have to spend more words on the tricky part. As mentioned above, JPackage is a tool for packaging a self-contained Java application, any kind of Java application, modularized and non-modularized applications. But for this example, we want to distribute a simple animation and it would be quite weird to distribute a whole JRE image for the sake of one animation. Therefore, always think about what you really need for your application and let the right tool works for you.
About linux part. JPackage allows you to package not only for Linux, but also for Windows and MacOS. However, JPackage depends on the operating system in order to work, so you can't package a Linux app, if you're using Window or MacOS, in a nutshell, no, cross-compiling is not an option. We will try to package our TreeFX app for Windows in an addendum to this article (maybe next week).
Enough chatting. Let's see it work:
$ mvn clean compile javafx:jlink jpackage:jpackage
Wait a while and then:
$ ls target/dist/
treefx_1.0.0-1_amd64.deb
Nice fourice! We now have our package for AMD64 arch (yeah that's mine). If we still have some curiosity about how the deb
package is organized, we can unpack it:
$ dpkg-deb -R target/dist/treefx_1.0.0-1_amd64.deb target/unpacked
$ tree target/unpacked
target/unpacked
├── DEBIAN
│ ├── control
│ ├── postinst
│ ├── postrm
│ ├── preinst
│ └── prerm
└── opt
└── treefx
├── bin
│ └── TreeFX
├── lib
│ ├── app
│ │ └── TreeFX.cfg
│ ├── runtime
│ │ ├── [...]
│ ├── TreeFX.png
│ └── treefx-TreeFX.desktop
└── share
└── doc
└── copyright
We could optimize things and surf into other JPackage configurations, but so far so good (as an old Bryan Adams' album and song said).
Conclusions
We finally reached the bottom of this tutorial, a lot more could be said about JPMS, JLink and JPackage, this is indeed an introduction to tickle your curiosity, and as you can see we can still build nice stuff with JavaFX and Java technology.
Dig into https://gitlab.com/lucaguada/treefx repository and look at the different branches to have a clearer vision of the above steps.
See you, space cowboy!
«A chance to begin again in a golden land of opportunity and adventure!»
Top comments (4)
I had been using JavaFX for a desktop application. After two years of working with I realized that in terms of frontend libraries it is far far behind Javascript. So I abandoned it and started working with ElectronJS.
far behind for doing what? :)
I'm trying to follow this article for creating a java runtime image of my javafx application using jlink and eclipse but when I try to compile I get an error linke this: Module java.desktop not found, required by com.test.calendario
Any idea why is it happening? I this modules in my module-info file, so I can't imagine why they are not find.
Thank you
Hi Neus! Sorry for the late response :) I dunno whether it's still useful, but for understanding better, what JDK version are you using? And what are the deps in your pom file?