JavaFX, JLink and JPackage
Tags: java, javafx, jlink, jpackage
Published: 2021-03-25 16:57:25

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](https://docs.oracle.com/javafx/2/animations/basics.htm).

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](https://www.oracle.com/java/technologies/javase-downloads.html) 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:
```sh
 $ sdk list java
```
Sdkman should list the Oracle Java SE as follow:
```sh
Unclassified  |     | 8.0.281      | none    | local only | 8.0.281-oracle
```
Nice! Therefore we can now compile our project:
```sh
 $ sdk use java 8.0.281-oracle
Using java version 8.0.281-oracle in this shell.
 $ mvn clean compile exec:java
```
and if everything is fine:
![TreeFX in action](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dwmoh0n8legdqc5x0gkj.png)

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.
```sh
 $ 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.
```xml
<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:
```sh
 $ 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**:
```xml
<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:
```sh
 $ 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`:
```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:
```sh
 $ 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:
```sh
 $ 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](https://github.com/petr-panteleyev/jpackage-maven-plugin))  on Maven Central that we can add:
```xml
<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:
```sh
 $ mvn clean compile javafx:jlink jpackage:jpackage
```
Wait a while and then:
```sh
 $ 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:
```sh
 $ 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!»