如何创建平台中立的 JavaFX jar(包含可执行代码)以与另一个多平台应用程序一起使用?

问题描述 投票:0回答:1

我的组织正在开发一个大型 Java 应用程序,其旧代码库主要是用 Swing 编写的,而我的任务是开发一个将独立管理的分支项目,使用 JavaFX 编写(在 Temurin JDK 21 中,因此 JavaFX 不是捆绑)并使用 Kotlin Gradle DSL 进行管理。该项目包含一个面板和一些业务逻辑,并且还有两种主要方法来独立测试它(一种在

Main.java
中,一种在 UI 面板中,
Planner.java
)。

我想将其发布到我们的 artefactory,但我没有运气构建我的

build.gradle.kts
文件以便能够:

  1. 使用main方法运行类;和
  2. 创建一个不依赖于特定于平台的 JavaFX 实现的 jar。

这就是我现在的位置:

import org.gradle.internal.os.OperatingSystem

plugins {
    java
    application
    `maven-publish`

    // I can't imagine this is necessary but my module isn't able to import JavaFX without it.
    id("org.openjfx.javafxplugin") version "0.0.14"
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}

val isRelease = project.hasProperty("release")
val appVersion = "0.1" + if (isRelease) "" else "-SNAPSHOT"
val modulePath by lazy { "${configurations.runtimeClasspath.get().asPath}:build/classes/java/main" }
val moduleString by lazy { javafx.modules.joinToString(separator = ",") }

application {
    version = appVersion
    group = "my.organization.subproject"
    mainClass.set("my.organization.subproject.fx.Main")
    mainModule.set("AutoMosaic")
}

repositories {
    mavenCentral()
}

javafx {
    version = "21"
    modules("javafx.controls", "javafx.swing")

    // When this is enabled, I cannot run the classes with Main methods.
    configuration = "compileOnly"
}

val javafxPlatform: String = when {
    OperatingSystem.current().isMacOsX && System.getProperty("os.arch") == "aarch64" -> "mac-aarch64" // ARM Mac
    OperatingSystem.current().isMacOsX && System.getProperty("os.arch") == "x86_64" -> "mac" // Intel Mac
    OperatingSystem.current().isWindows -> "win"
    OperatingSystem.current().isLinux -> "linux"
    else -> throw GradleException("Unsupported OS for JavaFX")
}

dependencies {
    // In an attempt to only rely on platform-neutral dependencies.
    compileOnly("org.openjfx:javafx-base:21")
    compileOnly("org.openjfx:javafx-controls:21")
    compileOnly("org.openjfx:javafx-graphics:21")
    compileOnly("org.openjfx:javafx-swing:21")

    // To execute Main and Planner locally.
    runtimeOnly("org.openjfx:javafx-base:21:$javafxPlatform")
    runtimeOnly("org.openjfx:javafx-controls:21:$javafxPlatform")
    runtimeOnly("org.openjfx:javafx-graphics:21:$javafxPlatform")
    runtimeOnly("org.openjfx:javafx-swing:21:$javafxPlatform")
}

// To create the Main and Planner Gradle tasks that can be run.
fun createJavaExecTask(taskName: String, mainClassName: String) {
    tasks.register<JavaExec>(taskName) {
        group = "application"
        mainClass.set(mainClassName)
        jvmArgs = listOf(
            "--module-path", modulePath,
            "--add-modules", moduleString
        )
        classpath = sourceSets["main"].runtimeClasspath
    }
}

// To run these through IDEA, configure Gradle tasks with Tasks and Arguments:
// 1. Main: main
// 2. Planner: planner
// Then they should be runnable via IDEA. The issue is that the module path for the JavaFX jars
// managed by the plugin are incredibly obtuse and can change, so even grabbing them and
// hard-coding them in an Application task yields issues.
createJavaExecTask("main", "my.organization.subproject.fx.Main")
createJavaExecTask("planner", "my.organization.subproject.fx.Planner")

// Print the arguments necessary to configure an IDEA task.
tasks.register("jvmOpts") {
    doLast {
        println("JVM opts required to make an IDEA task:")
        println("--module-path $modulePath --add-modules $moduleString")
    }
}

val repositoryUrl = "https://jfrog.organization.edu/artifactory/apt-maven"

val sourcesJar by tasks.registering(Jar::class) {
    from(sourceSets.main.get().allSource)
    archiveClassifier.set("sources")
}

publishing {
    publications {
        create<MavenPublication>("mavenJava") {
            groupId = "my.organization.mainproject"
            from(components["java"])
            artifact(sourcesJar.get())
        }
    }
    repositories {
        maven {
            val repoPassword: String? by project
            val repoUsername: String? by project
            url = uri(repositoryUrl + (if (project.hasProperty("release")) { "-releases" } else { "-snapshots" }))
            credentials {
                username = repoUsername
                password = repoPassword
            }
        }
    }
}

我不是此类领域的 Gradle 专家:我们将不胜感激任何帮助。

java gradle javafx build.gradle gradle-kotlin-dsl
1个回答
0
投票

根据您既定的目标和当前的构建脚本,我认为以下构建脚本示例至少应该帮助您指明正确的方向。一些注意事项:

  • 我(非详尽)使用 Gradle 8.11 测试了这些示例。

  • 两个示例都使用

    0.1.0
    插件的
    org.openjfx.javafxplugin
    版本。这是截至此答案的最新版本,并且是使示例正常工作所必需的。依赖于您的项目的其他 Gradle 项目可能也需要应用该插件才能找到正确的 JavaFX 工件。

    您正在使用的版本 (

    0.0.14
    ) 和旧版本的工作方式不同,并且会导致分类器包含在生成的 POM 中(例如,
    <classifier>win</classifier>
    )。

  • 我应用了 Java Library Plugin 而不是 Application Plugin。看起来您的项目更像是一个库而不是应用程序。您可以应用这两个插件,但目前您似乎并未真正使用 Application Plugin 的任何功能。

  • 对于第一个示例,由于您似乎正在创建一个库,因此我将 JavaFX 依赖项放在

    api
    配置中。假设您的公共 API 公开 JavaFX。它还导致生成的 POM 中的依赖项被放入
    compile
    范围内。如果您的公共 API
    公开 JavaFX,您可以使用 implementation 配置。请注意,这会将生成的 POM 中的依赖项放在
    runtime
    范围内。

  • withSourcesJar()
    块中的
    java
    会自动添加一个
    sourcesJar
    任务,并且该任务创建的JAR将包含在Maven发布中。如果您想包含项目 Javadoc 的 JAR,您可以添加
    withJavadocJar()
    调用。

  • 您当前的构建脚本表明您的项目有一个

    module-info.java
    文件。因此,我设置了两个
    mainModule
    任务的
    JavaExec
    属性。与
    modularity.inferModulePath
    true
    (默认情况下)相结合,任务会将文件放入
    classpath
    上的
    --module-path
    属性中,并通过
    --module
    启动程序。至少在最新版本的 Gradle 上是这样的。

  • 两个注册的

    JavaExec
    任务依赖于
    jar
    任务,并且 JAR 放在模块路径上而不是
    main.output
    上。两者都是因为您当前的构建脚本表明您的项目是模块化的。这是确保任何资源(即
    src/main/resources
    下的那些文件)最终位于模块内的一种方法。这也是如何将
    Application Plugin
    中的 run 任务配置为适用于模块化项目。

在 POM 中包含 JavaFX 依赖项

plugins {
  id("org.openjfx.javafxplugin") version "0.1.0"
  `java-library`
  `maven-publish`
}

val isRelease = hasProperty("release")

group = "my.organization.subproject"
version = "0.1" + if (isRelease) "" else "-SNAPSHOT"

java {
  withSourcesJar()
  toolchain {
    languageVersion = JavaLanguageVersion.of(21)
  }
}

repositories {
  mavenCentral()
}

dependencies {
  val javafxVersion = "21.0.5"
  api("org.openjfx:javafx-controls:$javafxVersion")
  api("org.openjfx:javafx-swing:$javafxVersion")
}

publishing {
  publications {
    create<MavenPublication>("mavenJava") {
      from(components["java"])
    }
  }

  repositories {
    maven {
      val baseUrl = "https://jfrog.organization.edu/artifactory/apt-maven-"
      url = uri(baseUrl + if (isRelease) "release" else "snapshot")
      credentials {
        username = findProperty("repoUsername")?.toString() ?: ""
        password = findProperty("repoPassword")?.toString() ?: ""
      }
    }
  }
}

tasks {
  val main by sourceSets
  val modPath = (main.runtimeClasspath - main.output) + files(jar.flatMap { it.archiveFile })

  register<JavaExec>("runMain") {
    dependsOn(jar)
    group = "application"

    mainModule = "AutoMosaic"
    mainClass = "my.organization.subproject.fx.Main"
    classpath = modPath
  }

  register<JavaExec>("runPlanner") {
    dependsOn(jar)
    group = "application"

    mainModule = "AutoMosaic"
    mainClass = "my.organization.subproject.fx.Planner"
    classpath = modPath
  }
}

发布到 Maven 存储库时,这将生成以下 POM(在本例中项目名称为

example
):

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <!-- This module was also published with a richer model, Gradle metadata,  -->
  <!-- which should be used instead. Do not delete the following line which  -->
  <!-- is to indicate to Gradle or any Gradle module metadata file consumer  -->
  <!-- that they should prefer consuming it instead. -->
  <!-- do_not_remove: published-with-gradle-metadata -->
  <modelVersion>4.0.0</modelVersion>
  <groupId>my.organization.subproject</groupId>
  <artifactId>example</artifactId>
  <version>0.1-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>org.openjfx</groupId>
      <artifactId>javafx-controls</artifactId>
      <version>21.0.5</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.openjfx</groupId>
      <artifactId>javafx-swing</artifactId>
      <version>21.0.5</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

如果您希望依赖项最终位于

provided
范围内,那么不幸的是,似乎没有一种简单的方法可以做到这一点。请参阅 How Map gradle 'compileOnly' to 'provided' in generated pom (generatePom...) 寻找潜在的解决方案,但我不知道如何将 Groovy DSL 转换为 Kotlin DSL。请注意,流行的 JavaFX 库ControlsFX,只是从生成的 POM 中删除了 JavaFX 依赖项。因此,您可能只想使用第二个示例的方法。

从 POM 中排除 JavaFX 依赖项

这通过定义自定义配置(在此示例中名为

javafx

)排除了 JavaFX 依赖项。 
Maven Publish Plugin 不会包含该配置的依赖项,至少默认情况下不会。然后,将 compileClasspath
runtimeClasspath
 配置配置为从 
javafx
 配置扩展,以确保 
JavaCompile
JavaExec
 任务继续工作(或者至少使配置它们变得更容易)。

plugins { id("org.openjfx.javafxplugin") version "0.1.0" `java-library` `maven-publish` } val isRelease = hasProperty("release") group = "my.organization.subproject" version = "0.1" + if (isRelease) "" else "-SNAPSHOT" java { withSourcesJar() toolchain { languageVersion = JavaLanguageVersion.of(21) } } repositories { mavenCentral() } configurations { val javafx = create("javafx") compileClasspath.configure { extendsFrom(javafx) } runtimeClasspath.configure { extendsFrom(javafx) } } dependencies { val javafxVersion = "21.0.5" "javafx"("org.openjfx:javafx-controls:$javafxVersion") "javafx"("org.openjfx:javafx-swing:$javafxVersion") } publishing { publications { create<MavenPublication>("mavenJava") { from(components["java"]) } } repositories { maven { val baseUrl = "https://jfrog.organization.edu/artifactory/apt-maven-" url = uri(baseUrl + if (isRelease) "release" else "snapshot") credentials { username = findProperty("repoUsername")?.toString() ?: "" password = findProperty("repoPassword")?.toString() ?: "" } } } } tasks { val main by sourceSets val modPath = (main.runtimeClasspath - main.output) + files(jar.flatMap { it.archiveFile }) register<JavaExec>("runMain") { dependsOn(jar) group = "application" mainModule = "AutoMosaic" mainClass = "my.organization.subproject.fx.Main" classpath = modPath } register<JavaExec>("runPlanner") { dependsOn(jar) group = "application" mainModule = "AutoMosaic" mainClass = "my.organization.subproject.fx.Planner" classpath = modPath } }
发布到 Maven 存储库时,这将生成以下 POM(在本例中项目名称为 

example

):

<?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <!-- This module was also published with a richer model, Gradle metadata, --> <!-- which should be used instead. Do not delete the following line which --> <!-- is to indicate to Gradle or any Gradle module metadata file consumer --> <!-- that they should prefer consuming it instead. --> <!-- do_not_remove: published-with-gradle-metadata --> <modelVersion>4.0.0</modelVersion> <groupId>my.organization.subproject</groupId> <artifactId>example</artifactId> <version>0.1-SNAPSHOT</version> </project>
    
© www.soinside.com 2019 - 2024. All rights reserved.