Какая магия в ScalaFX, чтобы заставить OpenJDK 9+ действительно работать?

Окружающая среда:

  • OpenJDK 64-битный сервер ВМ Zulu12.2+3-CA (сборка 12.0.1+12, смешанный режим, совместное использование)
  • Scala 2.12.7
  • Windows 10 Professional, X86_64
  • IntelliJ IDEA 2019.1.3 (Ultimate Edition)

Я проверил 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, что проблема с модулем не возникает?

1 ответ

Решение

Я не смог точно воспроизвести вашу проблему, но мне удалось получить проект, использующий только 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 в файле сборки. Угадай, что? Программа запустилась без ошибок!

Возможно, я что-то упустил, что касается использования 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"
)

Это работает путем добавления загруженных jav-файлов JavaFX под управлением ivy в путь к модулю Java. Однако это не очень хорошее решение для запуска автономных приложений. Это может быть возможно для sbt-native-packager обеспечить необходимую среду для запуска готового приложения, но я не пробовал этого.

Я разместил полное решение на GitHub

Дайте мне знать, помогает ли это. А пока я расскажу о поддержке SBT модулей JDK 9+, чтобы узнать, есть ли более простое решение...

ОБНОВЛЕНИЕ:

Я поднял проблему (#4941) с командой SBT, чтобы рассмотреть это более подробно.

ОБНОВЛЕНИЕ 2

Я исправил проблему, из-за которой решение не работало на Linux. Выполните git pull для обновления источников.

ОБНОВЛЕНИЕ 3

Я должен также упомянуть, что лучше всего, чтобы IntelliJ запускал приложение с использованием SBT, что упрощает работу и обеспечивает правильную настройку среды приложения.

Для этого перейдите в меню IntelliJ Run и выберите опцию Edit Configurations.... Нажмите кнопку " +" в верхнем левом углу диалогового окна, выберите " Задача sbt" в списке под ** Добавить новую конфигурацию, затем настройте следующим образом:

Это сначала скомпилирует и соберет приложение, если потребуется.

Примечание. Параметры _VM предназначены для запуска SBT и не относятся к тому, как SBT выполняет разветвленное приложение.

(Вы также можете добавить конфигурации запуска SBT для проверки вашего кода.)

Добавление к ответу Джонатана Кросмера:

Причина, по которой именование класса и объекта работает по-разному, заключается в том, что средство запуска 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
  }
}

build.sbt:

name := "JavaFXLoadTest"

version := "0.1"

scalaVersion := "2.13.2"

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

fork := true

Я столкнулся с той же самой проблемой и нашел тревожно странное и простое решение. 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 в собственный тип JFXApp, создавая скрытый класс Application.

Почему это работает? Я не совсем уверен, но при запуске каждого фрагмента кода в IntelliJ с использованием стандартной конфигурации запуска (щелкните правой кнопкой мыши HelloWorld и "запустите HelloWorld.main()"), затем в выводе нажмите "/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, используя sbt run, то шаблон тот же (HelloWorld не работает, но переименование класса приложения исправляет), но сообщение об ошибке является более простым, но все же бесполезным. Ошибка: компоненты среды выполнения JavaFX отсутствуют и необходимы для запуска этого заявление". Так что, может быть, это не совсем проблема IntelliJ, а как-то связано с JavaFX и Jigsaw? В любом случае это загадка, но, по крайней мере, у нас есть простое решение.

Другие вопросы по тегам