Search code examples
javajavafxjar

unable to run jar files (LinkageError, UnsupportedClassVersionError)


I tried creating a jar file from a project that's using JavaFX in Visual Studio Code.. When I run the app inside VSCode it works fine, but when I export it to a jar file it throws an error:

Error: LinkageError occurred while loading main class HelloFX java.lang.UnsupportedClassVersionError: Preview features are not enabled for HelloFX (class file version 65.65535). Try running with '--enable-preview'

So I gathered this error is caused because I compiled the jar file with a newer version than my JRE.. but when I use "javac -version" and "java -version" I get the same (21.0.2) (this has been set up in my system variables JAVA_HOME and Path variables).. I also found I needed to add '--enable-preview' inside the vmArgs parameters of the launch.json file (full text is now: "vmArgs": "--enable-preview --module-path "C:/Program Files/Java/javafx-sdk-21.0.2/lib" --add-modules javafx.controls,javafx.fxml" because I need to add the javafx sdk as well)

I created a new project that would display my java version and javaFX version

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class HelloFX extends Application {

    @Override
    public void start(Stage stage) {
        String javaVersion = System.getProperty("java.version");
        String javafxVersion = System.getProperty("javafx.version");
        Label l = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
        Scene scene = new Scene(new StackPane(l), 640, 480);
        stage.setScene(scene);
        stage.show();
    }

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

}

The text inside the label gives me: "Hello, JavaFX 21.0.2, running on Java 21.0.2" when I run it in VSCode, but same error when ran as jar file

I configured my Java Runtime inside VSCode (Java:Configure java Runtime) and the version is set to 21

I don't use Maven or Gradle (No build tools) since I don't really know how they work..

I use "java -jar MyApp.jar" when using the command line

Am I missing something?


Solution

  • Preface: Everything below assumes you've followed the directions from Getting Started with JavaFX to set up your project, specifically the directions under:

    • JavaFX and Visual Studio Code → Non-modular from IDE

    TL;DR: You are not using any preview features so you need to stop --enable-preview from being passed to the compiler. You can do this by:

    1. Creating a .settings/org.eclipse.jdt.core.prefs file, if it does not already exist, in your project directory.

    2. Adding the following line to said file:

      org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
      

    If you decide to use preview features, then either don't do the above or change disabled to enabled. But in that case, you must pass --enable-preview at run-time as well.

    java --enable-preview --module-path <path-to-javafx-sdk>\lib --add-modules <mods> -jar <jarfile>
    

    Important: That command assumes you did not include JavaFX in the JAR file.


    Background: Preview Features

    The concept of preview features was added around the time of Java 12. Here's the summary of the linked JEP:

    A preview feature is a new feature of the Java language, Java Virtual Machine, or Java SE API that is fully specified, fully implemented, and yet impermanent. It is available in a JDK feature release to provoke developer feedback based on real world use; this may lead to it becoming permanent in a future Java SE Platform.

    Preview features are opt-in. In other words, you have to explicitly tell Java you want to use preview features both at compile-time and run-time. This is accomplished by passing the --enable-preview argument. If a class file indicates that preview features were enabled at compile-time and you fail to enable them at run-time, then you'll see the error you're getting.

    JDK 21

    You can see which Java 21 features are in preview at https://openjdk.org/projects/jdk/21/. For example, one such feature is JEP 430: String Templates (Preview). Using that feature would let you create your Label like so:

    Label l = new Label(STR."Hello, JavaFX \{javafxVersion}, running on Java \{javaVersion}.");
    

    The Problem

    You are compiling your code with --enable-preview but failing to pass that same option when executing your JAR file. However, your example code is not using any Java 21 preview features, so while you can solve your error by running:

    java --enable-preview --module-path <path-to-javafx-sdk>\lib --add-modules <mods> -jar <jarfile>
    

    Important: That command assumes you did not include JavaFX in the JAR file.

    That does not fix the underlying problem, which is that --enable-preview is being passed to the compiler despite no apparent intent to do so on your part.

    Why are Preview Features Being Enabled?

    You are most likely using the Language Support for Java(TM) by Red Hat extension. Since you are not using Maven or Gradle, your project is apparently known as an "Eclipse project". This documentation implies you have to explicitly enable preview features for such projects:

    Eclipse projects need to add the the following preferences to .settings/org.eclipse.jdt.core.prefs:

    eclipse.preferences.version=1
    org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
    org.eclipse.jdt.core.compiler.codegen.targetPlatform=20
    org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
    org.eclipse.jdt.core.compiler.compliance=20
    org.eclipse.jdt.core.compiler.debug.lineNumber=generate
    org.eclipse.jdt.core.compiler.debug.localVariable=generate
    org.eclipse.jdt.core.compiler.debug.sourceFile=generate
    org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
    org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
    org.eclipse.jdt.core.compiler.release=enabled
    org.eclipse.jdt.core.compiler.source=20
    
    org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=enabled
    org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
    

    However, a comment to (closed) issue 1971 states:

    We only force org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures to enabled if Java 16 is used and the project is also unmanaged (eg. not Maven, Gradle, etc.).

    That comment is a little old, so the Java version mentioned is probably out of date. My understanding is that preview features will only be forcibly enabled on the latest supported version of Java, which is Java 21 at the time of this answer. Your project is using Java 21 and is unmanaged, so it meets the conditions to enable preview features.


    Solution

    The solution is to configure your "Eclipse project" to not enable preview features. Based on the documentation to enable preview features and another comment to the previously linked issue, you need to do the following to disable preview features:

    1. Create a .settings/org.eclipse.jdt.core.prefs file if it does not already exist.

    2. Add the following line to said file:

      org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
      

    The .settings directory must be directly under your project directory. Note that since that directory is hidden (starts with a .) it may not show up in VS Code's explorer view. I am not sure how to make it show up in that view, but you can open the file in VS Code by executing:

    code .settings/org.eclipse.jdt.core.prefs
    

    In the terminal (the above assumes the project directory is the current directory).

    After you disable preview features, rebuild your project and export a new JAR file. You should be able to run it with:

    java --module-path <path-to-javafx-sdk>\lib --add-modules <mods> -jar <jarfile>
    

    Important: That command assumes you did not include JavaFX in the JAR file.

    Without issue now. Also, you can now remove --enable-preview from the VM arguments in launch.json.


    ECJ vs javac

    The compiler that comes with OpenJDK is named javac. The RedHat VS Code extension, however, uses ECJ (a different compiler implementation from Eclipse) to compile your code, at least for "Eclipse projects". Apparently, there is a difference in behavior between the two compilers. From my experience:

    • javac will not record preview features were enabled if the source file does not actually "use" (as defined by JEP 12) any preview features, even if --enable-preview was passed.

    • ECJ will unconditionally record preview features were enabled if --enable-preview was passed.


    Executable "Fat" JAR

    All the above commands used to launch your JAR file assumed that you did not include JavaFX in the JAR file (i.e., you deselect the JavaFX JAR files when exporting your project to a JAR file). And that means you have to specify the --module-path and which modules to include with --add-modules on the command line. That's not very convenient, but if you want to create a so-called fat/uber/shadowed JAR file then there are a few things you must do.

    1. Create a separate launcher class. This class must be your main class and not be a subclass of javafx.application.Application. All it must do, at minimum, is launch the JavaFX application.

      package hellofx;
      
      import javafx.application.Application;
      
      public class Launcher {
      
          public static void main(String[] args) {
              Application.launch(HelloFX.class, args);
          }
      }
      

      This is required because JavaFX does not technically support being loaded into the unnamed module. There is some internal code that checks if the main class is a subclass of Application and, if it is, checks to see if the javafx.graphics module is in the boot module layer. If that module is not present, it fails. The above is a workaround.

    2. You must include the native code of JavaFX in the JAR file. I could not find a way to do that via the "Export JAR" feature of VS Code, but you can execute the following after creating the JAR file to add the native code:

      jar -uf <jarfile> -C <path-to-javafx-sdk>\bin .
      

      Warning: That command will include everything in the bin directory of the JavaFX SDK. That means, for instance, that it will include the native code for the javafx.web module even if you do not need it. But I do not know exactly which shared libraries go with which JavaFX module off the top of my head.

    Once you do those two things, then executing:

    java -jar <jarfile>
    

    Should work (include --enable-preview if you decided to use preview features). Though note you'll get a warning, which you can read about here: JavaFX WARNING: Unsupported JavaFX configuration: classes were loaded from 'unnamed module @...'.