Gradle多模块项目无法编译,因为找不到javafx.stage.Stage的类文件
我正在搭建一个Gradle 9.5.0的多模块项目,使用Java 25。
在这个项目中,我使用Spring Boot 4.0.5和 JavaFX 26.0.1。我已经设置了 spring.main.web-application-type: none,因为我正在构建一个桌面应用,GUI使用JavaFX。
settings.gradle.kts
rootProject.name = "media-dashboard"
startParameter.excludedTaskNames += "bootJar"
startParameter.excludedTaskNames += "bootStartScripts"
startParameter.excludedTaskNames += "bootDistTar"
startParameter.excludedTaskNames += "bootDistZip"
module("application")
module("desktop")
fun module(name: String, directory: String = name, buildFileName: String = "$name.gradle.kts") {
val moduleDir = File(settingsDir, directory)
val buildFile = File(moduleDir, buildFileName)
when {
!moduleDir.isDirectory -> throw IllegalArgumentException("$moduleDir is not a directory!")
!buildFile.isFile -> throw IllegalArgumentException("$buildFile is not a file!")
}
include(name)
project(":$name").projectDir = moduleDir
project(":$name").buildFileName = buildFile.name
}
我创建了一个带有 buildScr 模块的Gradle根项目,使其成为一个Composite Build。
buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
}
buildSrc/src/main/kotlin/java-common-conventions.gradle.kts
plugins {
`java-library`
}
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform("org.junit:junit-bom:6.0.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
tasks.compileJava {
options.encoding = "UTF-8"
}
tasks.jar {
archiveFileName = "${rootProject.name}-${project.name}-${project.version}.jar"
}
tasks.withType<Test> {
useJUnitPlatform()
}
我还有两个其他的Gradle子项目:application 和 desktop,每个都包含一个Java模块。这将让我把应用逻辑(后端代码)与GUI逻辑(前端代码)分离。
application/application.gradle.kts
plugins {
application
id("java-common-conventions")
id("org.springframework.boot") version "4.0.5"
id("io.spring.dependency-management") version "1.1.7"
id("org.openjfx.javafxplugin") version "0.1.0"
}
group = "be.mnt.mediadashboard"
version = "0.0.1"
application {
mainModule.set("be.mnt.mediadashboard.application")
mainClass.set("be.mnt.mediadashboard.application.SpringApplication")
applicationDefaultJvmArgs = listOf("--enable-native-access=javafx.graphics")
}
javafx {
version = "26.0.1"
modules = listOf("javafx.graphics") // Change to "javafx.controls" for working example.
}
dependencies {
implementation(project(":desktop"))
implementation("org.springframework.boot:spring-boot-starter")
}
application/src/main/java/module-info.java
open module be.mnt.mediadashboard.application {
requires be.mnt.mediadashboard.desktop;
requires javafx.graphics; // Change to "javafx.controls" for working example.
requires spring.boot;
requires spring.boot.autoconfigure;
requires spring.context;
requires spring.core;
requires org.apache.commons.logging;
requires org.slf4j;
requires org.yaml.snakeyaml;
}
desktop/desktop.gradle.kts
plugins {
`java-library`
id("java-common-conventions")
id("org.openjfx.javafxplugin") version "0.1.0"
}
group = "be.mnt.mediadashboard"
version = "0.0.1"
javafx {
version = "26.0.1"
modules = listOf("javafx.controls", "javafx.fxml")
}
dependencies {
}
desktop/src/main/java/module-info.java
open module be.mnt.mediadashboard.desktop {
requires javafx.controls;
requires javafx.fxml;
exports be.mnt.mediadashboard.desktop;
}
为了访问FXML资源,desktop 的module-info声明中必须使用 open 修饰符来访问 desktop/src/main/resources/fxml/dashboard.fxml。
在 application 模块中,我使用自动配置来搭建Spring
application/src/main/java/be/mnt/mediadashboard/application/SpringApplication.java
@SpringBootApplication
public class SpringApplication {
static void main(String[] args) {
Application.launch(JavaFXApplication.class, args);
}
}
并扩展 javafx.application.Application 来设置JavaFX应用。
application/src/main/java/be/mnt/mediadashboard/application/JavaFXApplication.java
import be.mnt.mediadashboard.desktop.DashboardStage;
import javafx.application.Application;
import javafx.stage.Stage;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
public class JavaFXApplication extends Application {
private DashboardStage dashboardStage;
private ConfigurableApplicationContext applicationContext;
@Override
public void init() {
applicationContext = new SpringApplicationBuilder(SpringApplication.class).run();
}
@Override
public void stop() {
applicationContext.close();
dashboardStage.close();
}
@Override
public void start(Stage primaryStage) throws Exception {
System.out.println("dashboardStage: " + primaryStage);
this.dashboardStage = new DashboardStage(primaryStage); // <-- can't compile!?
this.dashboardStage.show();
}
}
在 desktop 模块中,我创建了一个 DashboardStage.java 类来设置JavaFX的主舞台,该舞台由启动JavaFX线程的JavaFX应用类调用。
现在当我尝试构建上述内容时,在对上述 application 模块的代码进行编译时会出现错误:
> Task :application:compileJava FAILED
C:\Dev\Workspace\mnt-media-dashboard\application\src\main\java\be\mnt\mediadashboard\application\JavaFXApplication.java:29: error: cannot access Stage
this.dashboardStage = new DashboardStage(primaryStage);
^
class file for javafx.stage.Stage not found
1 error
从Gradle构建的调试日志中你可以看到 JavaFX Graphics 位于模块路径上(我把 --module-path略写了)。
2026-05-12T11:16:30.232+0200 [INFO] [org.gradle.jvm.toolchain.internal.DefaultToolchainJavaCompiler] Compiling with toolchain 'C:\Program Files\Java\jdk-25.0.2'.
2026-05-12T11:16:30.235+0200 [DEBUG] [org.gradle.api.internal.tasks.compile.NormalizingJavaCompiler] Compiler arguments: -source 25 -target 25 -d C:\Dev\Workspace\mnt-media-dashboard\application\build\classes\java\main -encoding UTF-8 -h C:\Dev\Workspace\mnt-media-dashboard\application\build\generated\sources\headers\java\main -g -sourcepath C:\Dev\Workspace\mnt-media-dashboard\application\src\main\java -proc:none -s C:\Dev\Workspace\mnt-media-dashboard\application\build\generated\sources\annotationProcessor\java\main -XDuseUnsharedTable=true -classpath C:\Users\K\.gradle\caches\modules-2\files-2.1\org.osgi\org.osgi.resource\1.0.0\343c88949132984b3f2d4175a72c40b77dc65619\org.osgi.resource-1.0.0.jar;C:\Users\K\.gradle\caches\modules-2\files-2.1\org.osgi\org.osgi.service.serviceloader\1.0.0\3728ff37909c6a3c1870b3e851342d9f2bd3cc63\org.osgi.service.serviceloader-1.0.0.jar --module-path C:\Dev\Workspace\mnt-media-dashboard\desktop\build\libs\media-dashboard-desktop-0.0.1.jar; ... ;C:\Users\K\.gradle\caches\modules-2\files-2.1\org.openjfx\javafx-graphics\26.0.1\5ed8530d00b9dbd1713b018fcd8820982ab714ea\javafx-graphics-26.0.1-win.jar; ... -parameters C:\Dev\Workspace\mnt-media-dashboard\application\src\main\java\be\mnt\mediadashboard\application\JavaFXApplication.java C:\Dev\Workspace\mnt-media-dashboard\application\src\main\java\be\mnt\mediadashboard\application\JpaConfiguration.java C:\Dev\Workspace\mnt-media-dashboard\application\src\main\java\be\mnt\mediadashboard\application\SpringApplication.java C:\Dev\Workspace\mnt-media-dashboard\application\src\main\java\module-info.java
如下所述的我的 回答 中所述,当我再添加模块 javafx.controles 并在我的 application/src/main/java/module-info.java 中require它时,我可以编译并运行整个项目。
我的问题是:为什么在编译时 javafx.stage.Stage 类不可用,而我确实在模块路径上有javafx.graphics。
我已经把我的GitHub仓库设为公开,并为上述最小可复现示例创建了一个独立分支,地址为 https://github.com/KevinVertongen/mnt-media-dashboard/tree/issues/MRE-Missing-JavaFX-Stage-class。
解决方案
我可以重现你所看到的编译错误。正确的做法是把你的 desktop 项目的module-info描述符改为:
open module be.mnt.mediadashboard.desktop {
requires transitive javafx.graphics; // <-- added directive
requires javafx.controls;
requires javafx.fxml;
exports be.mnt.mediadashboard.desktop;
}
DashboardStage 类位于一个导出包中,公开暴露自 javafx.graphics 模块的一个类型。当模块A 从模块B 暴露类型时,模块A 必须对模块B 进行 传递性 依赖。因此添加 requires transitive javafx.graphics 解决了问题。
此外,如果你用 -Xlint:exports 编译 desktop 项目,你会看到以下警告:
warning: [exports] class Stage in module javafx.graphics is not indirectly exported using 'requires transitive'
public DashboardStage(Stage stage) {
^
1 warning
也就是说:
- 我不清楚为什么没有那个指令会特定导致以下错误:
none class file for javafx.stage.Stage not found
在编译 application 项目时,尤其是因为它也需要 javafx.graphics 模块,而且该模块已经正确放置在模块路径上(通过查看Gradle的调试输出可以确认)。
- 我也不清楚为什么把你们的
application项目中的module-info描述符里的requires javafx.graphics改为requires javafx.controls就能解决问题。
另外,顺便说一句,获取FXML资源并不需要对你的模块进行 open。你现在这样做是因为你正在使用 ClassLoader::getResource 获取资源。但你其实是在模块内部获取资源。更合适的做法是使用 Class::getResource。我有一个 最近的回答,里面对资源封装有更详细的讲解。
不过,考虑到你使用的Spring目前并不是模块化的,我会重新考虑把自己的代码做成模块化是否值得。我发现只有当你所有的依赖项也使用JPMS模块(或至少所有非服务提供者依赖)时,在你自己的代码中使用JPMS模块才真正有意义。尤其是因为自动模块不能与 jlink 一起使用。
注意,你仍然可以把JavaFX作为命名模块。org.openjfx.javafxplugin 插件在开发阶段(至少在 compileJava 和 run 任务中)会确保JavaFX作为命名模块被解析。你也可以配置像 jpackage 这样的工具,在部署阶段执行相同的操作。