moditect / layrry

A Runner and API for Layered Java Applications
Apache License 2.0
332 stars 33 forks source link

Error launching a JavaFX application #67

Open aalmiray opened 3 years ago

aalmiray commented 3 years ago

I noticed a problem when when trying to launch https://github.com/carldea/worldclock/ (from @carldea) using an external Layers config file. The problem stems form the different options there are to launch a JavaFX application. I've replicated the problem with the following code

App.java

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.net.URL;

public class App extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        URL location = getClass().getResource("app.fxml");
        FXMLLoader fxmlLoader = new FXMLLoader(location);
        HBox hbox = fxmlLoader.load();

        Scene scene = new Scene(hbox);
        stage.setScene(scene);
        stage.setTitle("App");
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

module-info.java

module app {
    exports app;
    requires javafx.base;
    requires javafx.graphics;
    requires javafx.controls;
    requires javafx.fxml;
}

layers.toml

[layers.javafx]
    modules = [
        "org.openjfx:javafx-base:jar:{{os.detected.jfxname}}:{{javafx_version}}",
        "org.openjfx:javafx-controls:jar:{{os.detected.jfxname}}:{{javafx_version}}",
        "org.openjfx:javafx-graphics:jar:{{os.detected.jfxname}}:{{javafx_version}}",
        "org.openjfx:javafx-fxml:jar:{{os.detected.jfxname}}:{{javafx_version}}"]
[layers.core]
    modules = [
        "org.kordamp.ikonli:app:{{project_version}}"]
    parents = ["javafx"]
[main]
  module = "app"
  class = "app.App"

versions.properties

project_version = 12.0.1-SNAPSHOT
javafx_version = 11

The error I get when running is

Exception in thread "main" java.lang.RuntimeException: Couldn't run module main class
    at org.moditect.layrry.internal.LayersImpl.run(LayersImpl.java:139)
    at org.moditect.layrry.Layrry.launch(Layrry.java:56)
    at org.moditect.layrry.Layrry.run(Layrry.java:50)
    at org.moditect.layrry.launcher.LayrryLauncher.launch(LayrryLauncher.java:53)
    at org.moditect.layrry.launcher.LayrryLauncher.main(LayrryLauncher.java:35)
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at org.moditect.layrry.internal.LayersImpl.run(LayersImpl.java:136)
    ... 4 more
Caused by: java.lang.RuntimeException: java.lang.ClassNotFoundException: app.App
    at javafx.graphics/javafx.application.Application.launch(Application.java:304)
    at app@12.0.1-SNAPSHOT/app.App.main(App.java:42)
    ... 9 more
Caused by: java.lang.ClassNotFoundException: app.App
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
    at java.base/java.lang.Class.forName0(Native Method)
    at java.base/java.lang.Class.forName(Class.java:468)
    at javafx.graphics/javafx.application.Application.launch(Application.java:292)

This is caused by invoking launch() in the main() method, as shown before

    public static void main(String[] args) {
        launch();
    }

However if the launch procedure is changed to

    public static void main(String[] args) {
        Application.launch(App.class, args);
    }

Then the application class is found and launched, however it fails due to another problem related to FXML (https://github.com/moditect/layrry/issues/68). The original problem appears to be caused by different strategies involving classloaders:

I wonder if Layrry would have to setup the context classloader as well. I think it does not make any changes as far as I can tell.

aalmiray commented 3 years ago

It's worth mentioning that marking the module as open did not make a difference.

p-zalejko commented 3 years ago

Hi there,

I've been playing with JavaFX and layryy and I think I know what's wrong. It looks the problem appears only when FXML files are used. From this perspective, it might not be an issue of layrry, it is more about internals and default behaviours of JavaFX classes.

When JavaFX loads FXML files it constructs UI controls like VBox, Labels, Buttons etc. Everything that is within an FXML file. The FXMLLoader class, under the hood, resolves a ClaasLoader that will be used for constructing these UI elements BUT, it turns out, that it is a different ClaasLoader that has loaded the application itself (the main class).  By default, FXMLLoader takes a classLoader from the current thread (Thread.currentThread().getContextClassLoader()) whereas the main class was loaded by module's class loader (jdk.internal.loader.Loader) (right?).  

Fortunately, it looks that a fix for this issue is simple - an FXMLLoader instance must be provided with a ClaasLoader that loaded the main (application) class. It can be done in the following way:

            URL location = getClass().getResource("hello.fxml");
            FXMLLoader fxmlLoader = new FXMLLoader(location);
            fxmlLoader.setClassLoader(getClass().getClassLoader());  // <!--- it's needed
            VBox box = fxmlLoader.load();

I prepared a simple app that shows how to use FXML files + layrry. Here is the link.

In addition to that, I realized that when you have JDK/JRE that contains JavaFX (e.g. https://www.azul.com/downloads/zulu-community/?version=java-15-mts&package=jdk-fx ) then working with JavaFX is much simpler. You do not have to include JavaFX modules in layryy configuration files. Still, the classLoader must be configured for the FXMLLoader because it gives access to the JavaFX classes provided by other modules as well as is able to load controllers that can be used within FXML files (which are classes from your application module).

I hope it helps.

aalmiray commented 3 years ago

@p-zalejko thank you for posting this analysis, this particular FXMLLoader setup would have to be documented in Layrry's docs, in a section specific to JavaFX.

Regarding the use of a JDK/JRE + embedded JavaFX, the decisions to use this combination or external JavaFX modules is up to application developers.

JJBRT commented 2 years ago

Please take a look at my new article that explain how to export all modules to all modules at runtime in Java 16 and later without using any JVM parameter