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

时间:2019-06-26 13:46:31

标签: scala jigsaw scalafx openjdk-11

环境

  • 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中检出了CORS configuration,在IntelliJ中构建并运行了它,一切正常。这里是重要的应用程序的快速实现:

scalafx-hello-world

编辑:我的build.sbt:

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

  }
}

之后,我将实现更改为:

// 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: _*)
  }
}

现在我的问题是:ScalaFX什么不会发生模块问题?

3 个答案:

答案 0 :(得分:4)

在乔纳森·克罗斯默的回答中:

命名类和对象的方法不同的原因是,如果主类扩展了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运行时。但是,这样做的方法是先通过javafx.graphics加载模块java.lang.ModuleLayer.boot().findModule(JAVAFX_GRAPHICS_MODULE_NAME)。 如果此调用失败,则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运行时放在模块路径上,但实际上DID却将其放在类路径上。这意味着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运行时。


以下是查看上述行为的摘要:

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

答案 1 :(得分: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

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

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

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

JavaFX application running

关于将 SBT JDK 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

上发布了完整的解决方案

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

更新

我有raised an issue (#4941) with the SBT team对此进行了更详细的研究。

更新2

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

更新3

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

要执行此操作,请进入 IntelliJ 运行菜单,然后选择 Edit Configurations ... 选项。单击对话框左上角的 + 按钮,从“添加新配置”下的列表中选择 sbt Task” ,然后进行如下配置:

Adding an SBT run configuration

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

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

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

答案 2 :(得分:3)

我遇到了同样的问题,找到了一个令人困扰的奇怪而简单的解决方案。 tldr; 使主类的名称与JavaFX Application类的名称不同。首先是一个例子:

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 Application封装为自己的JFXApp类型,从而创建了一个隐藏的Application类。

为什么起作用?我不确定,但是当使用标准的Run config(右键单击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”,您会收到错误消息。但是,当主类与应用程序名称不匹配时,它所做的任何魔术推断都将关闭,并且build.sbt中的标准类路径也将起作用。

有趣的跟进:如果我使用sbt run从sbt外壳运行应用程序,则模式是相同的(HelloWorld失败,但是重命名应用程序类可以解决该问题),但是错误消息则更多-直接但仍然无济于事“错误:缺少JavaFX运行时组件,并且是运行此应用程序所必需的”。因此,也许不完全是IntelliJ问题,而是与JavaFX和Jigsaw有关?无论如何,这都是一个谜,但至少我们有一个简单的解决方案。