javamavenjavafxjavafx-11openjfx

Package a non-modular JavaFX application


I have a Java 8 application, that uses JavaFX and where the main class extends javafx.application.Application . Currently, I deliver it as a fat jar and it runs fine on Oracle Java 8.

Now I want it to be able to run on OpenJDK 11. To add JavaFX, I already added the artifacts from org.openjfx to the classpath and am including them in the fat jar. If I start my jar from the command line, I get

Error: JavaFX runtime components are missing, and are required to run this
application

I found two possible ways around this problem:

  1. The dirty one: Write a special launcher that does not extend Application and circumvent the module check. See http://mail.openjdk.java.net/pipermail/openjfx-dev/2018-June/021977.html
  2. The clean one: Add --module-path and --add-modules to my command line. The problem with this solution is, that I want my end users to be able to just launch the application by double clicking it.

While I could go with 1. as a workaround, I wonder what is currently (OpenJDK 11) the intended way to build/deliver executable fat jars of non-modular JavaFX applications. Can anyone help?


Solution

  • These are a few options for packaging/distributing a (non-modular) JavaFX 11 end application. Most of them are explained in the official OpenJFX docs.

    I'll use this sample as reference. I'll also use Gradle. Similar can be done with Maven (different plugins), and even without build tools (but this is not recommended...). Build tools are a must nowadays.

    Fat Jar

    This is still a valid option, but not the preferred one, as it breaks the modular design and bundles everything all together, and it is not cross-platform unless you take care of that.

    For the given sample, you have a build.gradle file like this:

    plugins {
        id 'application'
        id 'org.openjfx.javafxplugin' version '0.0.5'
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
    }
    
    javafx {
        modules = [ 'javafx.controls' ]
    }
    
    mainClassName = 'hellofx.HelloFX'
    
    jar {
        manifest {
            attributes 'Main-Class': 'hellofx.Launcher'
        }
        from {
            configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
        }
    }
    

    Note the use of a Launcher class. As mentioned by the OP or explained here, a launcher class that not extends from Application is required now to create a fat jar.

    Running ./gradlew jar produces a fat jar (~8 MB) that includes the JavaFX classes, and the native libraries of your current platform.

    You can run java -jar build/libs/hellofx.jar as usual, but only in the same platform.

    As explained in the OpenJFX docs or here, you can still create a cross-platform jar.

    In this case, we can include the three graphics jars, as those are the ones that have platform-dependent code and libraries. Base, controls and fxml modules are the platform-independent.

    dependencies {
        compile "org.openjfx:javafx-graphics:11.0.1:win"
        compile "org.openjfx:javafx-graphics:11.0.1:linux"
        compile "org.openjfx:javafx-graphics:11.0.1:mac"
    }
    

    ./gradlew jar will produce a fat jar (19 MB) now that can be distributed to these three platforms.

    (Note Media and Web have also platform-dependent code/native libraries).

    So this works as it used to on Java 8. But as I said before, it breaks how modules work, and it doesn't align on how libraries and apps are distributed nowadays.

    And don't forget that the users of these jars will still have to install a JRE.

    jlink

    So what about distributing a custom image with your project, that already includes a native JRE and a launcher?

    You will say that if you have a non-modular project, that won't work. True. But let's examine two options here, before talking about jpackage.

    runtime-plugin

    The badass-runtime-plugin is a Gradle plugin that creates runtime images from non-modular projects.

    With this build.gradle:

    plugins {
        id 'org.openjfx.javafxplugin' version '0.0.5'
        id 'org.beryx.runtime' version '1.0.0'
        id "com.github.johnrengelman.shadow" version "4.0.3"
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
    }
    
    javafx {
        modules = [ 'javafx.controls' ]
    }
    
    mainClassName = 'hellofx.Launcher'
    
    runtime {
        options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
    }
    

    When you run ./gradlew runtime it will create a runtime, with its launcher, so you can run:

    cd build/image/hellofx/bin
    ./hellofx
    

    Note it relies on the shadow plugin, and it requires a Launcher class as well.

    If you run ./gradlew runtimeZip, you can get a zip for this custom image of about 32.5 MB.

    Again, you can distribute this zip to any user with the same platform, but now there is no need of an installed JRE.

    See targetPlatform for building images for other platforms.

    Going Modular

    We keep thinking that we have non-modular project, and that can't be changed... but what if we do change it?

    Going modular is not that big of a change: you add a module-info.java descriptor, and you include the required modules on it, even if those are non-modular jars (based on automatic names).

    Based on the same sample, I'll add a descriptor:

    module hellofx {
        requires javafx.controls;
    
        exports hellofx;
    }
    

    And now I can use jlink on command line, or use a plugin for it. The badass-gradle-plugin is a gradle plugin, from the same author as the one mentioned before, that allows creating a custom runtime.

    With this build file:

    plugins {
        id 'org.openjfx.javafxplugin' version '0.0.5'
        id 'org.beryx.jlink' version '2.3.0'
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
    }
    
    javafx {
        modules = [ 'javafx.controls' ]
    }
    
    mainClassName = 'hellofx/hellofx.HelloFX'
    

    you can run now:

    ./gradlew jlink
    cd build/image/bin/hellofx
    ./hellofx
    

    or ./gradlew jlinkZip for a zipped version (31 MB) that can be distributed and run in machines, of the same platform, even if there is no JRE installed.

    As you can see, no need for shadow plugin or Launcher class. You can also target other platforms, or include non-modular dependencies, like in this question.

    jpackage

    Finally, there is a new tool to create executable installers that you can use to distribute your application.

    So far there is no GA version yet (probably we'll have to wait for Java 13), but there are two options to use it now with Java 11 or 12:

    With Java/JavaFX 11 there is a back port from the initial work on the JPackager on Java 12 that you can find here. There is a nice article about using it here, and a gradle project to use it here.

    With Java/JavaFX 12 there is already a build 0 version of the jpackage tool that will be available with Java 13.

    This is a very preliminary use of the tool:

    plugins {
        id 'org.openjfx.javafxplugin' version '0.0.5'
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
    }
    
    javafx {
        version = "12-ea+5"
        modules = [ 'javafx.controls' ]
    }
    
    mainClassName = 'hellofx/hellofx.HelloFX'
    
    def java_home = '/Users/<user>/Downloads/jdk-12.jdk/Contents/Home'
    def installer = 'build/installer'
    def appName = 'HelloFXApp'
    
    task copyDependencies(type: Copy) {
        dependsOn 'build'
        from configurations.runtime
        into "${buildDir}/libs"
    }
    
    task jpackage(type: Exec) {
        dependsOn 'clean'
        dependsOn 'copyDependencies'
    
        commandLine "${java_home}/bin/jpackage", 'create-installer', "dmg",
                '--output', "${installer}", "--name", "${appName}",
                '--verbose', '--echo-mode', '--module-path', 'build/libs',
                '--add-modules', "${moduleName}", '--input', 'builds/libraries',
                '--class', "${mainClassName}", '--module', "${mainClassName}"
    }
    

    Now running ./gradlew jpackage generates a dmg (65 MB), that I can distribute to be installed:

    installer

    Conclusion

    While you can stick to classic fat jars, when moving to Java 11 and beyond, everything is supposed to be modular. The new (soon to be) available tools and plugins, including the IDE support, are helping during this transition.

    I know I've presented here the simplest use case, and that when trying more complex real cases, there will be several issues... But we should better work on solving those issues rather than keep using outdated solutions.