ScalaFX 让 OpenJDK 9+ 真正起作用的魔力是什么?

What's the magic behind ScalaFX to make OpenJDK 9+ actually work?

环境:

我检查了 GitHub 中的 scalafx-hello-world,在 IntelliJ 中构建并 运行 它并且一切正常。在这里快速实现重要的应用程序:

package hello

import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.geometry.Insets
import scalafx.scene.Scene
import scalafx.scene.effect.DropShadow
import scalafx.scene.layout.HBox
import scalafx.scene.paint.Color._
import scalafx.scene.paint._
import scalafx.scene.text.Text

object ScalaFXHelloWorld extends JFXApp {

  stage = new PrimaryStage {
    //    initStyle(StageStyle.Unified)
    title = "ScalaFX Hello World"
    scene = new Scene {
      fill = Color.rgb(38, 38, 38)
      content = new HBox {
        padding = Insets(50, 80, 50, 80)
        children = Seq(
          new Text {
            text = "Scala"
            style = "-fx-font: normal bold 100pt sans-serif"
            fill = new LinearGradient(
              endX = 0,
              stops = Stops(Red, DarkRed))
          },
          new Text {
            text = "FX"
            style = "-fx-font: italic bold 100pt sans-serif"
            fill = new LinearGradient(
              endX = 0,
              stops = Stops(White, DarkGray)
            )
            effect = new DropShadow {
              color = DarkGray
              radius = 15
              spread = 0.25
            }
          }
        )
      }
    }

  }
}

编辑: 我的 build.sbt:

// Name of the project
name := "ScalaFX Hello World"

// Project version
version := "11-R16"

// Version of Scala used by the project
scalaVersion := "2.12.7"

// Add dependency on ScalaFX library
libraryDependencies += "org.scalafx" %% "scalafx" % "11-R16"
resolvers += Resolver.sonatypeRepo("snapshots")

scalacOptions ++= Seq("-unchecked", "-deprecation", "-Xcheckinit", "-encoding", "utf8", "-feature")

// Fork a new JVM for 'run' and 'test:run', to avoid JavaFX double initialization problems
fork := true

// Determine OS version of JavaFX binaries
lazy val osName = System.getProperty("os.name") match {
  case n if n.startsWith("Linux") => "linux"
  case n if n.startsWith("Mac") => "mac"
  case n if n.startsWith("Windows") => "win"
  case _ => throw new Exception("Unknown platform!")
}

// Add JavaFX dependencies
lazy val javaFXModules = Seq("base", "controls", "fxml", "graphics", "media", "swing", "web")
libraryDependencies ++= javaFXModules.map( m=>
  "org.openjfx" % s"javafx-$m" % "11" classifier osName
)

之后,我将实现改为:

package hello

import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.control.Label
import javafx.stage.Stage

class ScalaFXHelloWorld extends Application {
  override def start(stage: Stage): Unit = {
    stage.setTitle("Does it work?")
    stage.setScene(new Scene(
      new Label("It works!")
    ))
    stage.show()
  }
}

object ScalaFXHelloWorld {
  def main(args: Array[String]): Unit = {
    Application.launch(classOf[ScalaFXHelloWorld], args: _*)
  }
}

这里我得到以下错误:

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:567)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:464)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:363)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:567)
    at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1051)
Caused by: java.lang.RuntimeException: Exception in Application start method
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:900)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication(LauncherImpl.java:195)
    at java.base/java.lang.Thread.run(Thread.java:835)
Caused by: java.lang.IllegalAccessError: superclass access check failed: class com.sun.javafx.scene.control.ControlHelper (in unnamed module @0x40ac0fa0) cannot access class com.sun.javafx.scene.layout.RegionHelper (in module javafx.graphics) because module javafx.graphics does not export com.sun.javafx.scene.layout to unnamed module @0x40ac0fa0
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1016)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:151)
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:802)
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:700)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:623)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
    at javafx.scene.control.Control.<clinit>(Control.java:86)
    at hello.ScalaFXHelloWorld.start(ScalaFXHelloWorld.scala:39)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1(LauncherImpl.java:846)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait(PlatformImpl.java:455)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater(PlatformImpl.java:428)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:389)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater(PlatformImpl.java:427)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop(WinApplication.java:174)
    ... 1 more
Exception running application hello.ScalaFXHelloWorld

现在我的问题是:ScalaFX不出现模块问题是什么意思?

我无法完全重现您的问题,但我已经能够获得一个仅使用 JavaFX 的项目(即,它不使用 ScalaFX) 来构建和 运行.

这是我正在使用的(其他所有内容都在构建文件中指定):

(我确实尝试使用 Zulu OpenJDK 12 来构建和 运行 项目,这也奏效了。但是,最好是您使用与 JDK.)

匹配的 OpenJFX 版本

当我尝试您的原始来源和 build.sbt 时,我在从命令行执行 sbt run 命令时遇到以下错误:

D:\src\javafx11>sbt run
[info] Loading global plugins from {my home directory}\.sbt.0\plugins
[info] Loading project definition from D:\src\javafx11\project
[info] Loading settings for project javafx11 from build.sbt ...
[info] Set current project to JavaFX 11 Hello World (in build file:/D:/src/javafx11/)
[info] Running (fork) hello.ScalaFXHelloWorld
[error] Error: JavaFX runtime components are missing, and are required to run this application
[error] Nonzero exit code returned from runner: 1
[error] (Compile / run) Nonzero exit code returned from runner: 1
[error] Total time: 1 s, completed Aug 11, 2019, 3:17:07 PM

正如我在对您问题的原始评论中提到的那样。

我认为这很奇怪,因为编译的代码意味着编译器能够很好地找到 JavaFX 运行 时间。

然后我通过在构建文件中注释掉 fork := true 来尝试 运行 在没有 分叉 的情况下运行程序。你猜怎么着?程序运行没有错误!

关于将 SBTJDK 版本 9+ 一起使用,我可能遗漏了一些东西,但这表明 SBT 以某种方式没有正确 运行 分叉进程。我可以通过将以下内容添加到构建文件的末尾来强制分叉进程正确 运行 :

val fs = File.separator
val fxRoot = s"${sys.props("user.home")}${fs}.ivy2${fs}cache${fs}org.openjfx${fs}javafx-"
val fxPaths = javaFXModules.map {m =>
  s"$fxRoot$m${fs}jars${fs}javafx-$m-11-$osName.jar"
}
javaOptions ++= Seq(
  "--module-path", fxPaths.mkString(";"),
  "--add-modules", "ALL-MODULE-PATH"
)

这通过将下载的 ivy 管理的 JavaFX jar 文件添加到 Java的模块路径。但是,对于 运行ning 独立应用程序来说,这不是一个好的解决方案。 sbt-native-packager或许可以为运行提供完成申请所需的环境,但我没试过。

我已经在 GitHub

上发布了完整的解决方案

让我知道这是否有帮助。与此同时,我将研究 SBTJDK 9+ 模块的支持,看看是否有更简单的解决方案...

更新:

我有 raised an issue (#4941) with the SBT team 来更详细地研究这个问题。

更新 2

我修补了一个问题,该问题使解决方案无法在 Linux 上运行。执行 git pull 更新源。

更新 3

我还应该提一下,最好让 IntelliJ 运行 使用 SBT 的应用程序,这样可以使事情变得简单和确保正确配置应用程序的环境。

为此,进入 IntelliJ 运行 菜单,select 编辑配置... 选项。单击对话框左上角的 + 按钮,从 **Add New Configuration 下的列表中 select sbt Task” ,然后配置如下:

如果需要,这将首先编译和构建应用程序。

注意:_VM参数是针对运行ning SBT,与SBT 运行 是您的分叉应用程序。

(您也可以添加 SBT 运行 配置来测试您的代码。)

我 运行 解决了同样的问题,并找到了一个令人不安的奇怪而简单的解决方案。 tldr; 使主要 class 与 JavaFX 应用程序 class 具有不同的名称。先举个例子:

import javafx.application.Application
import javafx.event.ActionEvent
import javafx.event.EventHandler
import javafx.scene.Scene
import javafx.scene.control.Button
import javafx.scene.layout.StackPane
import javafx.stage.Stage

object HelloWorld {
  def main(args: Array[String]): Unit = {
    Application.launch(classOf[HelloWorld], args: _*)
  }
}

// Note: Application class name must be different than main class name to avoid JavaFX path initialization problems!  Try renaming HelloWorld -> HelloWorld2
class HelloWorld extends Application {
  override def start(primaryStage: Stage): Unit = {
    primaryStage.setTitle("Hello World!")
    val btn = new Button
    btn.setText("Say 'Hello World'")
    btn.setOnAction(new EventHandler[ActionEvent]() {
      override def handle(event: ActionEvent): Unit = {
        System.out.println("Hello World!")
      }
    })
    val root = new StackPane
    root.getChildren.add(btn)
    primaryStage.setScene(new Scene(root, 300, 250))
    primaryStage.show()
  }
}

上面写的代码抛出了原问题的异常。如果我将 class HelloWorld 重命名为 HelloWorld2(保留对象 HelloWorld,并将启动调用更改为 classOf[HelloWorld2]),它 运行 没问题。我怀疑这也是使 ScalaFX 工作的 "magic",因为它将 JavaFX 应用程序包装在它自己的 JFXApp 类型中,创建了一个隐藏的应用程序 class.

为什么有效?我不完全确定,但是当 运行 使用标准 运行 配置(右键单击 HelloWorld 和 "run HelloWorld.main()")在 IntelliJ 中设置每段代码时,然后在输出中单击“/home/jonathan/.jdks/openjdk-14.0.1/bin/java ...”展开它显示了一个包含“--add-modules javafx.base,javafx.graphics”的命令,除其他事项外。在第二个版本中,重命名的 HelloWorld2 应用程序中,该命令不包含此内容。我无法弄清楚 IntelliJ 如何决定使命令不同,但我只能推测它与推断它是一个 JavaFX 应用程序并试图通过自动添加“--add-modules”来提供帮助有关。 .?在任何情况下,模块列表都不包括所有需要的模块,因此例如创建一个按钮需要 "javafx.controls",并且您会收到错误消息。但是当主 class 与应用程序名称不匹配时,它所做的任何魔术推理都会被关闭,并且来自 build.sbt 的标准 class 路径才有效。

有趣的跟进:如果我 运行 来自 sbt shell 的应用程序使用 sbt run,那么模式是相同的(HelloWorld 失败,但重命名应用程序 class 修复它),但错误消息更直接但仍然无用 "Error: JavaFX runtime components are missing, and are required to run this application"。所以也许不完全是 IntelliJ 问题,而是与 JavaFX 和 Jigsaw 有关?无论如何这是一个谜,但至少我们有一个简单的解决方案。

添加到 Jonathan Crosmer 的回答中:

命名 class 和对象不同的原因是因为如果主 class 扩展 javafx.application.Application,Java 启动器实际上有特殊的行为。如果您有 Java 个可用资源,则可以在 JAVA_HOME/lib/src.zip/java.base/sun/launcher/LauncherHelper.java 中找到相关代码。特别是有两种方法很有趣:

public static Class<?> checkAndLoadMain(boolean, int ,String)

//In nested class FXHelper
private static void setFXLaunchParameters(String, int)

第一种方法有一个检查,检查主要 class 是否扩展 javafx.application.Application。如果是,此方法将主 class 替换为嵌套的 class FXHelper,它有自己的 public static void main(String[] args)

第二种方法,由第一种方法直接调用,尝试加载JavaFX运行时。然而,它这样做的方式是首先通过 java.lang.ModuleLayer.boot().findModule(JAVAFX_GRAPHICS_MODULE_NAME) 加载模块 javafx.graphics。 如果此调用失败,Java 将抱怨未找到 JavaFX 运行时,然后立即通过 System.exit(1).

退出

回到 SBT 和 Scala,还有一些其他细节在起作用。首先,如果主对象和扩展 javafx.application.Application 的 class 同名,Scala 编译器将生成一个 class 文件,它扩展 Application 并且有一个 public static void main(...)。这意味着将触发上述特殊行为,并且 Java 启动器将尝试将 JavaFX 运行时作为模块加载。由于 SBT 目前没有关于模块的概念,JavaFX 运行时将不在模块路径上并且对 findModule(...) 的调用将失败。

另一方面,如果 main 对象的名称与 main class 不同,Scala 编译器会将 public static void main(...) 放在不扩展 Application 的 class 中,这反过来意味着 main() 方法将正常执行。

在我们继续之前,我们应该注意虽然 SBT 没有将 JavaFX 运行时放在模块路径上,但它确实将它放在 class 路径上。这意味着 JavaFX classes 对 JVM 可见,只是不能作为模块加载。毕竟

A modular JAR file is like an ordinary JAR file in all possible ways, except that it also includes a module-info.class file in its root directory.

(来自 The State of the Module System

然而,如果恰好调用了一个方法,比方说Application.launch(...),Java会愉快地从class路径加载javafx.application.ApplicationApplication.launch(...) 将同样可以访问 JavaFX 的其余部分,一切正常。

这也是 运行 JavaFX 应用无需分叉的原因。在这种情况下,SBT 将始终直接调用 public static void main(...),这意味着不会触发 java 启动器的任何特殊行为,并且 JavaFX 运行时将在 class 路径上找到.


这里是一个片段,可以看到上面的行为:

Main.scala:

object Main {
  def main(args: Array[String]): Unit = {
    /*
    Try to load the JavaFX runtime as a module. This is what happens if the main class extends
    javafx.application.Application.
     */
    val foundModule = ModuleLayer.boot().findModule("javafx.graphics").isPresent
    println("ModuleLayer.boot().findModule(\"javafx.graphics\").isPresent = " + foundModule) // false

    /*
    Try to load javafx.application.Application directly, bypassing the module system. This is what happens if you
    call Application.launch(...)
     */
    var foundClass = false
    try{
      Class.forName("javafx.application.Application")
      foundClass = true
    }catch {
      case e: ClassNotFoundException => foundClass = false
    }
    println("Class.forName(\"javafx.application.Application\") = " + foundClass) //true
  }
}

build.sbt:

name := "JavaFXLoadTest"

version := "0.1"

scalaVersion := "2.13.2"

libraryDependencies += "org.openjfx" % "javafx-controls" % "14"

fork := true