【问题标题】:What's the magic behind ScalaFX to make OpenJDK 9+ actually work?让 OpenJDK 9+ 真正工作的 ScalaFX 背后有什么魔力?
【发布时间】:2019-11-08 11:47:54
【问题描述】:

环境

  • OpenJDK 64 位服务器 VM Zulu12.2+3-CA(内部版本 12.0.1+12,混合模式,共享)
  • Scala 2.12.7
  • Windows 10 专业版,X86_64
  • IntelliJ IDEA 2019.1.3(终极版)

我检查了 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$2(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$9(LauncherImpl.java:846)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:455)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:389)
    at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(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$3(WinApplication.java:174)
    ... 1 more
Exception running application hello.ScalaFXHelloWorld

现在我的问题是:ScalaFX 没有出现什么模块问题?

【问题讨论】:

  • 据我所知,您的代码看起来不错。您可以发布您的 SBT 构建文件(或等效文件)的内容吗?
  • @MikeAllen 我刚刚添加了 build.sbt - 但我没有更改 ScalaFX 示例之一。
  • 我需要测试一下。与此同时,请注意几点:(1) 尝试将scalaVersion 设置为“2.12.8”或“2.13.0”,以防万一,因为与 Java 9+ 的兼容性仍在发展中更新的 Scala 版本可能会更好; (2) 我注意到您使用的是 ZuluFX,其中包括 OpenJFX 12,但您还下载了 OpenJFX 11 模块作为依赖库 -尝试在 build.sbt 中注释掉 libraryDependencies 语句。
  • 实际上,我在 Azul 的网站上找不到 ZuluFX 12。他们似乎只支持 ZuluFX (OpenJDK & OpenJFX) 8 & 11。你能检查一下版本号吗?
  • @MikeAllen 我输入了错误的信息:它实际上是 Azul 的 Zulu 12(我在原帖中编辑了我的环境。)

标签: scala java-platform-module-system scalafx openjdk-11


【解决方案1】:

添加到乔纳森克罗斯默的答案:

命名类和对象不同的原因是因为如果主类扩展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)

第一个方法有一个检查主类是否扩展javafx.application.Application。如果是,此方法将主类替换为嵌套类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 的类名称相同,Scala 编译器将生成一个扩展Application 并具有public static void main(...) 的类文件。这意味着将触发上述特殊行为,并且 Java 启动器将尝试将 JavaFX 运行时作为模块加载。由于 SBT 目前没有关于模块的概念,JavaFX 运行时将不在模块路径上,并且对 findModule(...) 的调用将失败。

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

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

模块化 JAR 文件在所有可能的方面都类似于普通 JAR 文件,只是它还在其根目录中包含一个 module-info.class 文件。

(来自The State of the Module System

但是,如果碰巧调用了一个方法,比如说Application.launch(...),Java 会很高兴地从类路径中加载javafx.application.ApplicationApplication.launch(...) 将同样可以访问 JavaFX 的其余部分并且一切正常。

这也是为什么在没有分叉的情况下运行 JavaFX 应用程序的原因。在这种情况下,SBT 将始终直接调用public static void main(...),这意味着不会触发来自 java 启动器的特殊行为,并且会在类路径中找到 JavaFX 运行时。


这是一个 sn-p,可以查看上述行为的实际效果:

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

【讨论】:

  • 这里肯定有一些有趣的侦探工作,但让我们明确一点:SBT 不会从源代码生成任何东西——它只是一个构建系统。生成代码的是 Scala 编译器。我会进一步研究这个......
  • 太棒了!具有讽刺意味的是,罪魁祸首被命名为FXHelper :)
  • 我希望有一天 Java 和 Scala 能够齐心协力,让构建和部署 JavaFX 应用程序不再那么痛苦!一旦你让它们发挥作用,这些都是很棒的技术,但要想起步,肯定有一些令人困惑的问题需要克服。
  • 还要注意这个在纯 Java 上下文中的讨论:mail.openjdk.java.net/pipermail/openjfx-dev/2018-June/…
【解决方案2】:

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

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

上面编写的代码引发了原始问题的异常。如果我将类 HelloWorld 重命名为 HelloWorld2(保留对象 HelloWorld,并将启动调用更改为 classOf[HelloWorld2]),它运行良好。我怀疑这是让 ScalaFX 也能正常工作的“魔力”,因为它将 JavaFX 应用程序包装在自己的 JFXApp 类型中,创建了一个隐藏的应用程序类。

为什么有效?我不完全确定,但是当使用标准运行配置(右键单击 HelloWorld 和“运行 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”,你会得到错误。但是,当主类与应用程序名称不匹配时,它所做的任何魔术推理都会被关闭,并且 build.sbt 中的标准类路径正常工作。

有趣的跟进:如果我使用 sbt run 从 sbt shell 运行应用程序,那么模式是相同的(HelloWorld 失败,但重命名应用程序类可以解决它),但错误消息更直截了当-但仍然无济于事“错误:JavaFX 运行时组件丢失,需要运行此应用程序”。所以可能不完全是 IntelliJ 问题,而是与 JavaFX 和 Jigsaw 有关?无论如何这是一个谜,但至少我们有一个简单的解决方案。

【讨论】:

  • 我可能已经找到了为什么会这样。请看我的回答
  • 这很奇怪,因为伴侣 objects 总是 与关联的 classes 有不同的名称。 Scala 编译器将objectHelloWorld 的名称修饰为HelloWorld$ 类型(也就是说,它附加了一个$),但类仍然是HelloWorld,所以它们应该始终与 Java 不同。我会再调查一下...
  • Scala 编译器将始终在 HelloWorld 类中放置 static 版本的对象方法,它只是将调用转发到 HelloWorld$。这使得它们可以在 Java 代码中使用,而无需使用 $ 编写标识符。 Scala tour 调用此静态转发。这意味着如果您的主对象有一个伴生类,它有一个public static void main(...),即使该方法的实际内容将在带有$ 的类文件中。跨度>
【解决方案3】:

我无法准确重现您的问题,但我已经能够获得一个仅使用 JavaFX 的项目(也就是说,它不使用 ScalaFX) 来构建和运行。

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

(我确实尝试使用 Zulu OpenJDK 12 来构建和运行该项目,这也很有效。但是,最好使用 OpenJFX 的版本匹配 JDK。)

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

D:\src\javafx11>sbt run
[info] Loading global plugins from {my home directory}\.sbt\1.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

正如我在原始 cmets 中提到的那样。

我认为这很奇怪,因为代码已编译,这意味着编译器能够很好地找到 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 的模块路径来工作。但是,这不是运行独立应用程序的好解决方案。 sbt-native-packager 或许可以为已完成的应用程序运行提供必要的环境,但我没有尝试过。

我已经在GitHub上发布了完整的解决方案

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

更新

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

更新 2

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

更新 3

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

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

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

注意:_VM 参数用于运行 SBT,与 SBT 如何运行您的分叉应用程序无关。

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

【讨论】:

  • 非常感谢您的努力!只剩下一个问题:有没有办法在 IntelliJ 中调试应用程序?调试模式似乎没有使用 sbt,因此无法启动应用程序...
  • @Hannes 是的,您可以调试 运行配置。在 IntelliJ Run 菜单中,最上面的两项是运行和调试,分别是你上次执行的运行配置。您可以通过 Run 菜单的 Debug... 选项选择要调试的特定运行配置,该选项允许您选择要调试的运行配置。如果您还有其他问题,请告诉我...
  • 我看到当我单击调试图标时应用程序已启动,但它不会因任何原因在断点处停止。你试过吗?
  • @Hannes 是的。测试代码中有断点吗?如果是这样,您必须调试运行测试的运行配置。
  • 不,例如,它们在 HelloWorld.scala 的第 20 行。
猜你喜欢
  • 2017-09-26
  • 1970-01-01
  • 2012-05-15
  • 2012-11-30
  • 2011-04-02
  • 2012-07-29
  • 1970-01-01
  • 2010-09-24
  • 1970-01-01
相关资源
最近更新 更多