Какая магия в 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? В любом случае это загадка, но, по крайней мере, у нас есть простое решение.