Understanding how the main class affects JPMS
Asked Answered
A

1

7

I have a very basic JavaFX application that works flawlessly if the Application class is not the Main class:

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;

public class Main {

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

}

public class App extends Application {

    @Override
    public void start(Stage primaryStage) {
        FXMLLoader loader = new FXMLLoader(); // works
    }

}

However, when I merge the two together (which is the recommended way in most tutorials, including OpenJFX's official documentation), the module system throws an IllegalAccessError (at least on OpenJDK 11.0.2):

public class MainApp extends Application {

    @Override
    public void start(Stage primaryStage) {
        FXMLLoader loader = new FXMLLoader(); // throws IllegalAccessError
    }

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

}

The exception is:

java.lang.IllegalAccessError: class com.sun.javafx.fxml.FXMLLoaderHelper (in unnamed module @0x642c1a1b) cannot access class com.sun.javafx.util.Utils (in module javafx.graphics) because module javafx.graphics does not export com.sun.javafx.util to unnamed module @0x642c1a1b

The weird thing is, that I did not actively use the module system. I did not add a module-info.java to my project. So I assumed everything should get exported to any unnamed modules? But that is not even the point.

The main question is: Why does the same code behave differently if distributed across two classes? In both cases FXMLLoader uses com.sun.javafx.fxml.FXMLLoaderHelper, which in turn uses com.sun.javafx.util.Utils. So either I should get the exception in both cases or in none. What is the difference?

Aranyaka answered 18/2, 2019 at 22:15 Comment(3)
Seems strange. Please provide a real MCVE for this.Shetler
@Shetler Just try and run the code. You can get the demo project on GitHubAranyaka
perfect that's what I wanted. Exact package names and build tools are going to be important when investigating this.Shetler
S
5

There are a few answers already posted that could apply partially to your questions, but it might be convenient to collect them here and present them in a full answer.

Application class

In the answer to Maven Shade JavaFX runtime components are missing I explained the reason why, when you use the Application class as your main class, you are expected to use the module system.

In summary:

As you can read here:

This error comes from sun.launcher.LauncherHelper in the java.base module (link).

If the main app extends Application and has a main method, the LauncherHelper will check for the javafx.graphics module to be present as a named module:

Optional<Module> om = ModuleLayer.boot().findModule(JAVAFX_GRAPHICS_MODULE_NAME);
if (!om.isPresent()) {
    abort(null, "java.launcher.cls.error5");
}

If that module is not present, the launch is aborted.

Every JavaFX 11 jar has a module-info.class file, so by definition, these are expected to be added to the module path.

But if you don't run via Application class, that check is not done.

Main class

This other answer to Different behaviour between Maven & Eclipse to launch a JavaFX 11 app explains why it works without the modular system when you use a Launcher class (a Main class not extending Application) with the Maven exec:java plugin.

In summary:

  • Using a Launcher is required to overcome the mentioned sun.launcher.LauncherHelper issue.
  • Like the maven plugin runs in the classpath, loading all the dependencies into an isolated thread, so does IntelliJ in this case.

If you check the command line when you run Main.main():

/path/to/jdk-11.0.2.jdk/Contents/Home/bin/java \
    "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=60556:/Applications/IntelliJ IDEA.app/Contents/bin"  \
    -Dfile.encoding=UTF-8  \
    -classpath /path/to/so-question-54756176-master/target/classes:/path/to/.m2/repository/org/openjfx/javafx-base/11.0.2/javafx-base-11.0.2.jar:.../path/to/.m2/repository/org/openjfx/javafx-fxml/11.0.2/javafx-fxml-11.0.2-mac.jar  \
    Main

All the JavaFX jars from the JavaFX SDK are added to the classpath, and you are running the classic java -cp ... Main.

javafx.fxml missing

These other answer to IntelliJ IDEA - Error: JavaFX runtime components are missing, and are required to run this application explains the error you get when you run on the module system but you don't add javafx.fxml to the --add-modules option.

Caused by: java.lang.IllegalAccessError: class com.sun.javafx.fxml.FXMLLoaderHelper (in unnamed module @0x5fce9dc5) cannot access class com.sun.javafx.util.Utils (in module javafx.graphics) because module javafx.graphics does not export com.sun.javafx.util to unnamed module @0x5fce9dc5
    at com.sun.javafx.fxml.FXMLLoaderHelper.<clinit>(FXMLLoaderHelper.java:38)
    at javafx.fxml.FXMLLoader.<clinit>(FXMLLoader.java:2056)

Your error says that you are using FXML but it can't be resolved in the module-path, so it is trying to access via reflection and that fails since you didn't opened that javafx.graphics to your unnamed module.

So now you will ask: I didn't set the javafx.graphics in the first place!

Well, you didn't, but IntelliJ did it for you!

Check the command line when you run MainApp.main():

/path/to/jdk-11.0.2.jdk/Contents/Home/bin/java \
    --add-modules javafx.base,javafx.graphics \
    --add-reads javafx.base=ALL-UNNAMED \
    --add-reads javafx.graphics=ALL-UNNAMED \
    "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=60430:/Applications/IntelliJ IDEA.app/Contents/bin" \
    -Dfile.encoding=UTF-8 \
    -classpath /path/to/so-question-54756176-master/target/classes:/path/to/.m2/repository/org/openjfx/javafx-base/11.0.2/javafx-base-11.0.2.jar:.../.m2/repository/org/openjfx/javafx-graphics/11.0.2/javafx-graphics-11.0.2-mac.jar \
    MainApp

You can see that IntelliJ by default adds javafx.base and javafx.graphics. So only the javafx.fxml is missing (and then you should of course add the module-path).

The recommended solution, as you pointed out, is in the docs:

Either on command line, using --module-path to include the path to your JavaFX SDK lib folder, and --add-modules to include javafx.fxml in this case (where you don't have controls).

IntelliJ VM arguments

Or using the Maven plugin. At some point you will have to leave your IDE, so you will need to use a plugin to run the application.

Maven exec

A final note on the Maven exec plugin, in case you use it:

What's more, the recommended Maven solution, until the plugin exec:java is fixed for the modular system (and the good news is that this is being done as we speak), will be using exec:exec instead, as explained in this issue, so you can specify both vm arguments.

Sabrasabre answered 19/2, 2019 at 0:14 Comment(2)
Wow, thanks for this in-depth answer! really appreciate all the details. Also it shows how much I dont know about those libraries yet. So when leaving the IDE and distributing the application, I need to add the ˋ—add-modulesˋ option for javafx despite the fact I am not using the JPMS myself, right?Aranyaka
You can still use a launcher class and the current exec:java plugin, but that's going to change really soon, and there won't be another alternative, so you will have to use the modular system, whether you like it or not...Varhol

© 2022 - 2024 — McMap. All rights reserved.