2

I have a program written in Clojure and JavaFX back in 2014. A dependency for the program was revised recently to use Java 17. Simply substituting the new version of the dependency produces an error related to not being able to read the new class file format. I would like to update the application but have not been able to generate an uberjar with current versions of Java (17) and JavaFX (17.0.1).

Here are the project.clj and source file for an SSCCE.

(defproject sutest "0.1.0-SNAPSHOT"
  :description "Test for including JavaFX components in uberjar"
  :dependencies [[org.clojure/clojure "1.10.3"]
                 [org.openjfx/javafx-controls "17.0.1"]]
  :aot :all
  :main sutest.core)
(ns sutest.core
  (:gen-class
    :extends javafx.application.Application)
  (:import
    [javafx.application Application Platform]
    [javafx.event EventHandler]
    [javafx.geometry Insets Pos]
    [javafx.scene Scene]
    [javafx.scene.control Button Label]
    [javafx.scene.layout VBox]))

(defn -start [this stage]
  (let [hiLbl (Label. "Hello World!")
        exitBtn (Button. "Exit")
        root (VBox. 12.0)]
    (.setOnAction exitBtn (reify EventHandler (handle [_ _]
                                                (Platform/exit))))
    (.setPadding root (Insets. 0 10 0 10))
    (.addAll (.getChildren root) [hiLbl exitBtn])
    (.setAlignment root Pos/CENTER)
    (.setScene stage (Scene. root 250 150)))
  (.show stage))

(defn -main [& args]
  (Application/launch sutest.core args))

The program works as expected when executed directly with lein run or from an IntelliJ IDEA/Cursive "run" configuration. Running lein uberjar completes without errors, but attempting to run the uberjar with

java -jar target/sutest-0.1.0-SNAPSHOT-standalone.jar

produces

Error: JavaFX runtime components are missing, and are required to run this application

I'm using Leiningen 2.9.6 because of this issue with 2.9.7. When running the program, Leiningen builds a classpath containing the needed jars in the local dependency repository.

I've seen a few questions about including the newer JavaFX modules in "fat" jars for Java and tried including them in the Leiningen build. Few related to Clojure and Java. For example, see the project.clj for the fn-fx library. That resorts to a special "leaky" profile to include the modules. That didn't work for me for some reason.

I've tried adding the modules to the IDEA/Cursive project. That correctly downloaded the modules in the project information, but still does not build an uberjar with the modules.

I've fiddled with the "Artifacts" section of the IDEA/Cursive project too. But that was unsuccessful.

There are tutorials on the Gluon site that walk through making a "fat" jar containing the JavaFX components, but those are directed towards Java projects.

Can Leiningen be used to create an uberjar containing the dependencies?

If not Leiningen, how about tools.deps or boot?

Has anyone been successful falling back to a plain Maven build by adapting the Gluon instructions?

clartaq
  • 5,320
  • 3
  • 39
  • 49
  • Please, edit your question to show the content of your jar. – jewelsea Oct 29 '21 at 21:59
  • Leiningen supports [classifiers for dependencies](https://github.com/technomancy/leiningen/issues/58). If you check your jar content and it does not include the required native components, you should be able to add them by creating additional dependencies to those native components as is done when performing the same task fof a [shared maven build](https://stackoverflow.com/questions/52653836/maven-shade-javafx-runtime-components-are-missing), at least that is what makes sense to me, though I don’t know for sure, given I am unfamiliar with your tool chain choice. – jewelsea Oct 30 '21 at 07:39

1 Answers1

1

After @jewelsea's suggestion, I looked at the contents of the uberjar created in the original question. I used

jar tvf jar tvf target/sutest-0.1.0-SNAPSHOT-standalone.jar

which produced pages and pages of class listings.

Near the top were the lines:

...
   306 Sat Oct 23 15:23:46 EDT 2021 javafx-controls-17.0.1.jar
   306 Sat Oct 23 15:23:46 EDT 2021 javafx-graphics-17.0.1.jar
   302 Sat Oct 23 15:23:46 EDT 2021 javafx-base-17.0.1.jar
746012 Sat Oct 23 15:23:46 EDT 2021 javafx-base-17.0.1-mac.jar
2545243 Sat Oct 23 15:23:48 EDT 2021 javafx-controls-17.0.1-mac.jar
4852153 Sat Oct 23 15:23:48 EDT 2021 javafx-graphics-17.0.1-mac.jar
...

The same jars with the same sizes as used in the classpath built by the lein run command.

So, the JavFX module jars needed were already included in the uberjar.

That triggered a faint memory of a message thread about special treatment of JavaFX applications by the sun.launcher.LauncherHelper class of the java.base module. (The source for the Java 17 version of the class is here if you are interested.) The launcher does a special check of the main class being launched. If it extends javafx.application.Application, the JavaFX modules must be present on the module path, otherwise the error message in the original question is displayed and the launch is aborted. The message thread mentioned above and this StackOverflow answer give a much better explanation.

The net result is that you can't have the main program entry for a JavaFX "fat" jar extend from javafx.application.Application class. Instead, you can use a "normal" main class that then calls the JavaFX part of the program.

The project listings below are slight edits of those in the original question and implement the solution described.

Here is the revised project.clj. Only the name of the :main class with the program entry point has changed.

(defproject sutest "0.1.0-SNAPSHOT"
  :description "Test for including JavaFX components in uberjar"
  :dependencies [[org.clojure/clojure "1.10.3"]
                 [org.openjfx/javafx-controls "17.0.1"]]
  :aot :all
  :main sutest.launcher)

Here is the new class used to launch the JavaFX portion:

(ns sutest.launcher
  (:gen-class)
  (:require [sutest.core :as sc]))

(defn -main [& args]
  (sc/start-it args))

It calls into the sutest.core namespace with the new start-it function

Here is the revised version of the original JavaFX program. The start-it function launches things instead of the -main function that was used earlier. Note the change to the args parameters though. It no longer has the & rest signature. It must be a (possibly empty) vector of strings. The names of the variables for the label and button have been changed to reflect Clojure variable naming conventions.

(ns sutest.core
  (:gen-class
    :extends javafx.application.Application)
  (:import
    [javafx.application Application Platform]
    [javafx.event EventHandler]
    [javafx.geometry Insets Pos]
    [javafx.scene Scene]
    [javafx.scene.control Button Label]
    [javafx.scene.layout VBox]))

(defn -start [_ stage]
  (let [hi-lbl (Label. "Hello World!")
        exit-btn (Button. "Exit")
        root (VBox. 12.0)]
    (.setOnAction exit-btn (reify EventHandler (handle [_ _]
                                                (Platform/exit))))
    (.setPadding root (Insets. 0 10 0 10))
    (.addAll (.getChildren root) [hi-lbl exit-btn])
    (.setAlignment root Pos/CENTER)
    (.setScene stage (Scene. root 250 150)))
  (.show stage))

(defn start-it [args]
  (Application/launch sutest.core args))

That's it. Now you can build an uberjar and run it from the command line. Also, it can still be run directly from Leiningen with the lein run command and from within the IDE (after changing the name of the class with the entry point in the run configuration.)

This is a dirty hack

It is based on detailed knowledge of the internals of the launcher rather than using modules. When launched, the program still gives a warning like:

WARNING: Unsupported JavaFX configuration: classes were loaded from 'unnamed module @25b2b199'

Hopefully it will work long enough to figure out how to use Java modules in "fat" jars in Clojure though.

clartaq
  • 5,320
  • 3
  • 39
  • 49