Home > OS >  How to generate javafx jar from gradle including all dependencies
How to generate javafx jar from gradle including all dependencies

Time:12-02

Been stuck with this issue for days. I'm trying to export a jar file using the gradle build command but it provides a small jar file. When I run the jar file it gives the following error, indicating that the javafx dependency was not included in the build:

Exception in thread "main" java.lang.NoClassDefFoundError: javafx/application/Application
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862)
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
    at newcare.home.Main_1.main(Main_1.java:6)
Caused by: java.lang.ClassNotFoundException: javafx.application.Application
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
    ... 10 more

I've seen posts such as this Creating a Java Gradle project and building the .jar file in IntelliJ IDEA - How to? They seem to help others but not me. Here's my build.gradle:

plugins {
 id 'java'
 id 'application'
 id 'org.openjfx.javafxplugin' version '0.0.8'
}
group 'newcare'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8

dependencies {
    testImplementation group: 'junit', name: 'junit', version: '4.12'
    implementation group: 'org.jasypt', name: 'jasypt', version: '1.9.2'
    implementation 'com.google.code.gson:gson:2.8.7'
    implementation 'joda-time:joda-time:2.10.13'
    implementation 'org.ocpsoft.prettytime:prettytime:4.0.4.Final'


    // here starts JavaFX
    implementation 'org.openjfx:javafx:14'

    implementation 'org.openjfx:javafx-base:14'
    implementation 'org.openjfx:javafx-graphics:14'
    implementation 'org.openjfx:javafx-controls:14'
    implementation 'org.openjfx:javafx-fxml:14'
    implementation 'org.openjfx:javafx-swing:14'
    implementation 'org.openjfx:javafx-media:14'
    implementation 'org.openjfx:javafx-web:14'
}

javafx{
    modules = ['javafx.controls', 'javafx.fxml', 'javafx.media', 'javafx.graphics']
    version = '11.0.2'
}

mainClassName = 'newcare.home.Main_1'




jar {
    from {
        configurations.runtime.collect {
            it.isDirectory() ? it : zipTree(it)
        }
        configurations.compile.collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }
    manifest {
        attributes "Main-Class": "$mainClassName"
    }

    exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
}




sourceSets {
    main {
        resources {
            srcDirs = ["src/main/java"]
            includes = ["**/*.fxml","**/*.css","**/*.png","**/*.jpg"]
        }
    }
}

I've also tried to use the build artifacts methods but nothing seems to work.

CodePudding user response:

Preface: Although it's possible to create a fat/uber JAR file which includes JavaFX, this is not the preferred approach. That results in JavaFX being loaded from the class-path, which is not a supported configuration. However, if you really want to create a fat/uber JAR file then skip to the "Creating a Fat/Uber JAR" section.


Self-Contained Application

The preferred approach for deploying a JavaFX application these days, is to create a custom run-time image and package it into a platform-specific installer/executable.

JLink & JPackage

The jlink tool is used to create custom run-time images. That's really just a fancy way of saying "a custom JRE that contains only the modules you need". The result is a self-contained application, though not a very user-friendly one, in my opinion.

Note the jlink tool only works with explicitly named modules (i.e. there's a module-info.class file present).

The jpackage tool essentially takes a custom run-time image and generates a platform-specific executable for it (e.g. a .exe file on Windows). Then the application is packaged into a platform-specific installation file (e.g. a .msi file on Windows). This tool has a user guide.

Note the jpackage tool supports creating non-modular applications. You can even configure it so all modules end up in the custom run-time image, while your non-modular application and any other non-modular dependencies are placed on the class-path.

Native Code

The JavaFX framework requires platform-specific native code to work. This means you need to make sure that native code is included in the custom run-time image. There are two ways you can do this:

  1. Use the JavaFX JAR files from Maven Central, not the JavaFX SDK.

    • The JAR files published to Maven Central embed the native code. However, JavaFX will have to extract the native code to some place on your computer in order to use it.
  2. (Preferred) Use the JavaFX JMOD files, which can be downloaded from gluonhq.com.

    • You would point jlink / jpackage at the JMOD files instead of the regular JAR files.
    • This results in the native code being included in the same way as all the native code needed by the JRE itself. Now there's no need to extract the native code, which makes this the better option in my opinion.

Gradle

There are two plugins I'd recommend for using jlink / jpackage from Gradle.


Creating a "Fat/Uber JAR"

When using Gradle, I recommend the Gradle Shadow Plugin for creating so-called fat/uber JAR files. It does much of the configuration for you, such as excluding signature files. And it pulls appropriate defaults from the already-existing jar task. It also adds the shadowJar task for building the fat/uber JAR file.

Example

Here is a sample application that creates a fat/uber JAR file. Note I use the Kotlin DSL for Gradle instead of the Groovy DSL, but you can use whichever.

Used:

  • Gradle 7.3
  • Java 17.0.1 (JavaFX not included)

settings.gradle.kts

rootProject.name = "sample"

build.gradle.kts

plugins {
    application
    id("org.openjfx.javafxplugin") version "0.0.10"
    id("com.github.johnrengelman.shadow") version "7.1.0"
}

group = "sample"
version = "1.0"

java {
    modularity.inferModulePath.set(false)
}

javafx {
    version = "17.0.1"
    modules("javafx.controls")
}

application {
    mainClass.set("sample.Main")
}

repositories {
    mavenCentral()
}

tasks {
    shadowJar {
        exclude("module-info.class")
    }
}

Main.java

package sample;

import javafx.application.Application;

public class Main {

    public static void main(String[] args) {
        Application.launch(App.class, args);
    }
}

App.java

package sample;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class App extends Application {
    
    @Override
    public void start(Stage primaryStage) {
        StackPane root = new StackPane(new Label("Hello, World!"));
        primaryStage.setScene(new Scene(root, 500, 300));
        primaryStage.setTitle("Sample");
        primaryStage.show();
    }
}

Running gradle shadowJar will give you a fat/uber JAR file at build/libs/sample-1.0-all.jar.

A few notes:

  • I configure the shadowJar plugin to exclude module-info.class files, because fat/uber JAR files do not work well with the module-path.

    • A JAR can only contain one module.
    • Running the application with -jar puts the JAR on the class-path.
    • Bonus: This avoids having multiple module-info.class files in the root of the JAR, one for each modular dependency.
  • The sample.Main class is necessary, because JavaFX technically does not support being loaded from the class-path. If the main class is assignable to javafx.application.Application, and the javafx.graphics module is not found in the boot layer, then the application will fail to start. The separate main class hacks around that.

  • On JavaFX 16 a warning will be emitted because JavaFX does not support being loaded from the class-path.

  • This JAR file will only work for the platform you ran Gradle on, because only that platform's native code is included.

    • If you want a cross-platform JAR then you need to manually add the JavaFX dependencies for each platform so that the native code for each platform is embedded in the JAR file.
  • Related