10

环境

  • OpenJDK 64-Bit Server VM Zulu12.2+3-CA(build 12.0.1+12,混合模式,共享)
  • 斯卡拉 2.12.7
  • Windows 10 专业版,X86_64
  • IntelliJ IDEA 2019.1.3(终极版)

scalafx-hello-world我从GitHub中查看,在 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 没有出现什么模块问题?

4

3 回答 3

4

我无法完全重现您的问题,但我已经能够获得一个仅使用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\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你猜怎么着?程序运行无误!

JavaFX 应用程序正在运行

关于将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+ 模块的支持,看看是否有更简单的解决方案......

更新

我向SBT团队提出了一个问题 (#4941),以便更详细地研究这个问题。

更新 2

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

更新 3

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

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

添加 SBT 运行配置

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

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

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

于 2019-08-11T19:52:33.510 回答
4

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

以不同方式命名类和对象的原因是,如果主类扩展,Java 启动器实际上具有特殊的行为javafx.application.Application。如果您有可用的 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 运行时放在模块路径上,但实际上它确实把它放在了类路径上。这意味着 JavaFX 类对 JVM 是可见的,它们只是不能作为模块加载。毕竟

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

(来自模块系统的状态

但是,如果碰巧调用了一个方法,比如说Application.launch(...),Java 会很高兴地javafx.application.Application从类路径加载。Application.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
  }
}

构建.sbt:

name := "JavaFXLoadTest"

version := "0.1"

scalaVersion := "2.13.2"

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

fork := true
于 2020-05-25T11:50:25.887 回答
3

我遇到了同样的问题,发现了一个令人不安的奇怪而简单的解决方案。 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 类型中,从而创建了一个隐藏的应用程序类。

为什么它有效?我不完全确定,但是当使用标准运行配置在 IntelliJ 中运行每段代码时(右键单击 HelloWorld 并“运行 HelloWorld.main()”),然后在输出中单击“/home/jonathan/.jdks /openjdk-14.0.1/bin/java ...”展开它会显示一个命令,其中包括“--add-modules javafx.base,javafx.graphics”等。在第二个版本中,使用重命名的 HelloWorld2 应用程序,该命令不包含此内容。我无法弄清楚 IntelliJ 是如何决定使命令与众不同的,但我只能推测它与推断它是一个 JavaFX 应用程序有关,并试图通过自动添加“--add-modules”来提供帮助。 .? 在任何情况下,模块列表都不包括所有需要的模块,因此例如创建一个按钮需要“

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

于 2020-05-11T22:20:22.143 回答