如何从 gradle 生成包含所有依赖项的 javafx jar

How to generate javafx jar from gradle including all dependencies

几天来一直被这个问题困扰。我正在尝试使用 gradle 构建命令导出一个 jar 文件,但它提供了一个小的 jar 文件。当我 运行 jar 文件时,它给出了以下错误,表明 javafx 依赖项未包含在构建中:

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

我看过这样的帖子Creating a Java Gradle project and building the .jar file in IntelliJ IDEA - How to?他们似乎帮助了别人而不是我。这是我的 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"]
        }
    }
}

我也尝试过使用构建工件方法,但似乎没有任何效果。

前言: 虽然可以创建包含 JavaFX 的 fat/uber JAR 文件,但这不是首选方法.这导致 JavaFX 从 class 路径加载,这不是受支持的配置。但是,如果您真的想创建 fat/uber JAR 文件,请跳至“创建 Fat/Uber JAR”部分。


自包含应用程序

目前部署 JavaFX 应用程序的首选方法是创建自定义 运行 时间映像并将其打包到特定平台 installer/executable.

JLink 和 JPackage

jlink 工具用于创建自定义 运行 时间图像。这实际上只是一种奇特的说法,“一个只包含您需要的模块的自定义 JRE”。结果是一个独立的应用程序,虽然在我看来不是一个非常用户友好的应用程序。

请注意 jlink 工具仅适用于明确命名的模块(即存在 module-info.class 文件)。

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.

请注意 jpackage 工具支持创建非模块化应用程序。您甚至可以配置它,以便所有模块最终都在自定义 运行-time 图像中,而您的非模块化应用程序和任何其他非模块化依赖项都放在 class-path.

本机代码

JavaFX 框架需要特定于平台的本机代码才能工作。这意味着您需要确保本机代码包含在自定义 运行 时间图像中。有两种方法可以做到这一点:

  1. 使用来自 Maven Central 的 JavaFX JAR 文件,而不是 JavaFX SDK。

    • 发布到 Maven Central 的 JAR 文件嵌入了本机代码。但是,JavaFX 必须将本机代码提取到您计算机上的某个位置才能使用它。
  2. (首选)使用JavaFX JMOD文件,可从gluonhq.com.

    下载
    • 您可以将 jlink / jpackage 指向 JMOD 文件 而不是 常规 JAR 文件。
    • 这导致本机代码以与 JRE 本身所需的所有本机代码相同的方式被包含。现在不需要提取本机代码,这在我看来是更好的选择。

Gradle

我推荐使用 jlink / jpackage 来自 Gradle 的两个插件。


正在创建“Fat/Uber JAR”

当使用 Gradle 时,我推荐使用 Gradle Shadow Plugin 来创建所谓的 fat/uber JAR 文件。它为您做了很多配置,例如排除签名文件。它从已经存在的 jar 任务中提取适当的默认值。它还添加了用于构建 fat/uber JAR 文件的 shadowJar 任务。

例子

这是一个创建 fat/uber JAR 文件的示例应用程序。请注意,我将 Kotlin DSL 用于 Gradle 而不是 Groovy DSL,但您可以使用任何一个。

已用:

  • Gradle 7.3
  • Java 17.0.1(Java不含外汇)

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();
    }
}

运行 gradle shadowJar 会在 build/libs/sample-1.0-all.jar.

处给你一个 fat/uber JAR 文件

一些注意事项:

  • 我将 shadowJar 插件配置为排除 module-info.class 文件,因为 fat/uber JAR 文件不能很好地与模块路径一起工作。

    • 一个 JAR 只能包含一个模块。
    • 运行 带有 -jar 的应用程序将 JAR 放在 class-路径上。
    • 好处:这避免了在 JAR 的根目录中有多个 module-info.class 文件,每个模块依赖一个文件。
  • sample.Main class 是必要的,因为 JavaFX 在技术上不支持从 class 路径加载。如果main class assignable to javafx.application.Application,而在boot layer中没有找到javafx.graphics模块,那么应用程序将无法启动。单独的主要 class 破解。

  • 在 JavaFX 16+ 上会发出警告,因为 JavaFX 不支持从 class 路径加载。

  • 此 JAR 文件仅适用于您 运行 Gradle 所在的平台,因为仅包含该平台的本机代码。

    • 如果您想要一个跨平台的 JAR,那么您需要为每个平台手动添加 JavaFX 依赖项,以便每个平台的本机代码嵌入到 JAR 文件中。