JavaFX 11 : Create a jar file with Gradle

Flpe picture Flpe · Sep 29, 2018 · Viewed 25.3k times · Source

I am trying to upgrade a JavaFX project from the 8 Java version to the 11 version. It works when I use the "run" Gradle task (I followed the Openjfx tutorial), but when I build (with the "jar" Gradle task) and execute (with "java -jar") a jar file, the message "Error: JavaFX runtime components are missing, and are required to run this application" appears.

Here is my build.gradle file :

group 'Project'
version '1.0'
apply plugin: 'java'
sourceCompatibility = 1.11

repositories {
    mavenCentral()
}

def currentOS = org.gradle.internal.os.OperatingSystem.current()
def platform
if (currentOS.isWindows()) {
    platform = 'win'
} else if (currentOS.isLinux()) {
    platform = 'linux'
} else if (currentOS.isMacOsX()) {
    platform = 'mac'
}
dependencies {
    compile "org.openjfx:javafx-base:11:${platform}"
    compile "org.openjfx:javafx-graphics:11:${platform}"
    compile "org.openjfx:javafx-controls:11:${platform}"
    compile "org.openjfx:javafx-fxml:11:${platform}"
}

task run(type: JavaExec) {
    classpath sourceSets.main.runtimeClasspath
    main = "project.Main"
}

jar {
    manifest {
        attributes 'Main-Class': 'project.Main'
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

compileJava {
    doFirst {
        options.compilerArgs = [
                '--module-path', classpath.asPath,
                '--add-modules', 'javafx.controls,javafx.fxml'
        ]
    }
}

run {
    doFirst {
        jvmArgs = [
                '--module-path', classpath.asPath,
                '--add-modules', 'javafx.controls,javafx.fxml'
        ]
    }
}

Do you know what I should do ?

Answer

José Pereda picture José Pereda · Sep 29, 2018

With Java/JavaFX 11, the shadow/fat jar won't work.

As you can read here:

This error comes from sun.launcher.LauncherHelper in the java.base module. The reason for this is that the Main app extends Application and has a main method. If that is the case, the LauncherHelper will check for the javafx.graphics module to be present as a named module:

Optional<Module> om = ModuleLayer.boot().findModule(JAVAFX_GRAPHICS_MODULE_NAME);

If that module is not present, the launch is aborted. Hence, having the JavaFX libraries as jars on the classpath is not allowed in this case.

What's more, every JavaFX 11 jar has a module-info.class file, at the root level.

When you bundle all the jars content into one single fat jar, what happens to those files with same name and same location? Even if the fat jar keeps all of them, how does that identify as a single module?

There is a request to support this, but it hasn't been addressed yet: http://openjdk.java.net/projects/jigsaw/spec/issues/#MultiModuleExecutableJARs

Provide a means to create an executable modular “uber-JAR” that contains more than one module, preserving module identities and boundaries, so that an entire application can be shipped as a single artifact.

The shadow plugin still does make sense to bundle all your other dependencies into one jar, but after all you will have to run something like:

java --module-path <path-to>/javafx-sdk-11/lib \
   --add modules=javafx.controls -jar my-project-ALL-1.0-SNAPSHOT.jar

This means that, after all, you will have to install the JavaFX SDK (per platform) to run that jar which was using JavaFX dependencies from maven central.

As an alternative you can try to use jlink to create a lightweight JRE, but your app needs to be modular.

Also you could use the Javapackager to generate an installer for each platform. See http://openjdk.java.net/jeps/343 that will produce a packager for Java 12.

Finally, there is an experimental version of the Javapackager that works with Java 11/JavaFX 11: http://mail.openjdk.java.net/pipermail/openjfx-dev/2018-September/022500.html

EDIT

Since the Java launcher checks if the main class extends javafx.application.Application, and in that case it requires the JavaFX runtime available as modules (not as jars), a possible workaround to make it work, should be adding a new Main class that will be the main class of your project, and that class will be the one that calls your JavaFX Application class.

If you have a javafx11 package with the Application class:

public class HelloFX extends Application {

    @Override
    public void start(Stage stage) {
        String javaVersion = System.getProperty("java.version");
        String javafxVersion = System.getProperty("javafx.version");   
        Label l = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
        Scene scene = new Scene(new StackPane(l), 400, 300);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

Then you have to add this class to that package:

public class Main {

    public static void main(String[] args) {
        HelloFX.main(args);
    }
}

And in your build file:

mainClassName='javafx11.Main'
jar {
    manifest {
        attributes 'Main-Class': 'javafx11.Main'
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Now you can run:

./gradlew run

or

./gradlew jar
java -jar build/libs/javafx11-1.0-SNAPSHOT.jar

JavaFX from FAT jar

The final goal is to have the JavaFX modules as named modules on the module path, and this looks like a quick/ugly workaround to test your application. For distribution I'd still suggest the above mentioned solutions.