3

We are developing an JavaFX 11 application with the Adopt JDK 11 and IntelliJ using gradle. At the end we need wanted an EXE file for windows (the application is only designed for windows). In the first try we also used launch4J within gradle, due the following problem we would also be fine using a BAT file which we could migrate to an exe file instead.

So our main goal and question is, how can we create a executable jar file.

We made several approaches with different results and we are completly lost.

FAT JAR

We made a standalone application for testing purpose:

package de.test;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        VBox vBox = new VBox();

        Button button = new Button("Klick");

        TextField textfield = new TextField();
        TextArea area = new TextArea();
        area.setMinHeight(300);

        button.setOnAction(event -> area.setText(area.getText() + " --  Klick Version 1.0.8"));
        vBox.getChildren().addAll(button, textfield,area);

        primaryStage.setScene(new Scene(vBox));
        primaryStage.setTitle("Test");
        primaryStage.show();
    }
}

FAT JAR

And our first try was a FAT-JAR with or without a modules-info.java file using the javafx plugin from gradle. Here is our gradle file

plugins {
    id 'java'
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8'
}

group 'de.test'
version '1.0.8'

sourceCompatibility = 11

javafx {
    modules = ['javafx.controls']
}

mainClassName = 'de.test.Main'

jar {
    manifest {
        attributes 'Main-Class': mainClassName
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

repositories {
    mavenLocal()
    mavenCentral()
}

After running gradle clean jar we tried to execute the jar with the following command java -jar ApplicationTest-1.0.8.jar the result:

Error: Could not find or load main class de.test.Main Caused by: java.lang.NoClassDefFoundError: javafx/application/Application


FAT JAR with dependencies and module-info.java

Then we tried to add the dependencies for JavaFX like this inside the gradle file

compile group: 'org.jboss.resteasy', name: 'resteasy-client', version: '4.3.0.Final'
compile "org.openjfx:javafx-graphics:11.0.2:win"
compile "org.openjfx:javafx-base:11.0.2:win"
compile "org.openjfx:javafx-controls:11.0.2:win"
compile "org.openjfx:javafx-fxml:11.0.2:win"
compile "org.openjfx:javafx-graphics:11.0.2:win"

And this is the module-info.java discriptor we added to the package de.test:

module ApplicationTest.main {

    requires javafx.controls;

    exports de.test;

}

When calling gradle installDist we used this command

cd build\install\ApplicationTest\lib java --add-modules "javafx.controls" --module-path . -jar ApplicationTest-1.0.8.jar

At least the application started at this point, but the layout was destroyed with the following info messages:

Sep. 20, 2019 8:32:55 VORM. com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged INFO: Could not load stylesheet: com/sun/javafx/scene/control/skin/modena/modena.css Sep. 20, 2019 8:32:55 VORM. com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged INFO: Could not load stylesheet: com/sun/javafx/scene/control/skin/modena/modena.css

No layout when using the JAR file

By the way: IntelliJ is always working when using the command gradle clean run

Using IntelliJ the layout is fine


JLink with module-info.java

The next approach was using jlink and the following modules-info.java

module ApplicationTest.main {
    requires javafx.controls;
    requires javafx.graphics;

    exports de.test;

}

We needed to extend the gradle file a bit as the following

plugins {
    id 'java'
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8'
    id 'org.beryx.jlink' version '2.10.4'
}

group 'de.test'
version '1.0.8'

sourceCompatibility = 11

mainClassName = 'de.test.Main'

javafx {
    version = 11
    modules = [ 'javafx.controls']
}

jlink {
    options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
}


repositories {
    mavenLocal()
    mavenCentral()
}

When using gradle jlink and executing the cd build\image\bin\ApplicationTest.bat file, the application starts WITH layout.

At this point we where kind of happy. Now we needed to add some dependencies for our application.

Using the following dependencies worked perfectly

dependencies {
    compile "commons-io:commons-io:2.6"
    compile "org.apache.logging.log4j:log4j-api:2.11.2"
    compile "dom4j:dom4j:1.6.1"
    compile "commons-lang:commons-lang:2.6"
    compile "axis:axis:1.4"
    compile "jaxen:jaxen:1.1.6"
    compile "net.java.dev.jna:platform:3.5.2"
    compile "org.apache.poi:poi:4.1.0"
    compile "org.apache.poi:poi-scratchpad:4.1.0"
    compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.4"
}

But as soon as I add the other dependencies I get different kind of error messages. I tried each dependency alone to isolate the error message.

org.jboss.resteasy - resteasy-client - 3.7.0.Final

Cannot derive uses clause from service loader invocation in: javax/ws/rs/client/FactoryFinder.find().
Cannot derive uses clause from service loader invocation in: javax/ws/rs/ext/FactoryFinder.find().
Cannot derive uses clause from service loader invocation in: javax/ws/rs/sse/FactoryFinder.find().
Cannot derive uses clause from service loader invocation in: javax/xml/bind/ServiceLoaderUtil.firstByServiceLoader().

ApplicationTest\build\jlinkbase\tmpjars\de.test.merged.module\module-info.java:154: error: the service implementation type must be a subtype of the service interface type, or have a public static no-args method named "provider" returning the service implementation
    provides javax.ws.rs.ext.Providers with org.jboss.resteasy.plugins.interceptors.CacheControlFeature,
                                                                                   ^
ApplicationTest\build\jlinkbase\tmpjars\de.test.merged.module\module-info.java:155: error: the service implementation type must be a subtype of the service interface type, or have a public static no-args method named "provider" returning the service implementation
                org.jboss.resteasy.plugins.interceptors.encoding.ClientContentEncodingAnnotationFeature,
                                                                ^
ApplicationTest\build\jlinkbase\tmpjars\de.test.merged.module\module-info.java:156: error: the service implementation type must be a subtype of the service interface type, or have a public static no-args method named "provider" returning the service implementation
                org.jboss.resteasy.plugins.interceptors.encoding.MessageSanitizerContainerResponseFilter,
                                                                ^
...     
                                            ^
25 errors

Dependency: org.apache.logging.log4j:log4j-core:2.11.2

package org.apache.logging.log4j.spi is not visible
(package org.apache.logging.log4j.spi is declared in module org.apache.logging.log4j, but module de.test.merged.module does not read it)
package org.apache.logging.log4j.message is not visible
(package org.apache.logging.log4j.message is declared in module org.apache.logging.log4j, but module de.test.merged.module does not read it)

Dependency: org.jboss.resteasy:resteasy-multipart-provider:3.7.0.Final

module not found: java.activation

Dependecyy: org.jboss.resteasy:resteasy-jackson2-provider:3.7.0.Final

package javax.ws.rs.ext does not exist
package javax.ws.rs.ext does not exist
package javax.ws.rs.ext does not exist

Conclusion

We're not getting anywhere right now. It probably has something to do with modules-info.java, but the instructions and documentation on this topic are quite complex and often very detailed. We just wanted an application that could be started outside the IntelliJ.

We are open for all solutions as long as they work. We don't need a state of the art solution either, but something from which we can somehow get an EXE in the end (also with the help of an external tool). Our application doesn't have to be modular, but we neither get it nor solve it.

EDIT 1) FAT Jar with 2nd main class

Like in this aricle we added a second main java class without extending Application and put that to the gradle file

package de.test;

public class Main {
    public static void main(String[] args) {
        MainFX.main(args);
    }

}

and the other class looks like this:

package de.test;

import ...;

public class MainFX extends Application {

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

    @Override
    public void start(Stage primaryStage) throws Exception {
    ...
    }
}

and the gradle file looks like this:

plugins {
    id 'java'
    id 'application'
}

group 'de.test'
version '1.0.8'

sourceCompatibility = 11

mainClassName = 'de.test.MainFX'

jar {
    manifest {
        attributes 'Main-Class': mainClassName
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    compile "commons-io:commons-io:2.6"
    compile "org.apache.logging.log4j:log4j-api:2.11.2"
    compile "dom4j:dom4j:1.6.1"
    compile "commons-lang:commons-lang:2.6"
    compile "axis:axis:1.4"
    compile "jaxen:jaxen:1.1.6"
    compile "net.java.dev.jna:platform:3.5.2"
    compile "org.apache.poi:poi:4.1.0"
    compile "org.apache.poi:poi-scratchpad:4.1.0"
    compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.4"

    compile "org.jboss.resteasy:resteasy-multipart-provider:3.7.0.Final"
    compile "org.jboss.resteasy:resteasy-jackson2-provider:3.7.0.Final"
    compile "org.apache.logging.log4j:log4j-core:2.11.2"
    compile "org.jboss.resteasy:resteasy-client:3.7.0.Final"
    compile "org.openjfx:javafx-graphics:11.0.2:win"
    compile "org.openjfx:javafx-base:11.0.2:win"
    compile "org.openjfx:javafx-controls:11.0.2:win"
    compile "org.openjfx:javafx-fxml:11.0.2:win"
    compile "org.openjfx:javafx-graphics:11.0.2:win"
}

The result is really strange java -jar ApplicationTest.jar returns

Error: Could not find or load main class de.test.MainFX Caused by: java.lang.ClassNotFoundException: de.test.MainFX

We double checked the jar file and the class files are in the correct location (de\test\Main.class and de\test\MainFX.class)

EDIT 2) FAT Jar with 2nd main class

Based on this article we added the javafx plugin again, removed the javafx dependencies and defined the Launcher-Main inside the manifest and the regular FXML Mail as mainClassName:

...
mainClassName = 'de.test.MainFX'

javafx {
    version = 11
    modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.web']
}

jar {
    manifest {
        attributes 'Main-Class': 'de.test.MainLauncher'
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

The result is

> Exception in thread "main" java.lang.NoClassDefFoundError:
> javafx/application/Application
>         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:174)
>         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 de.test.MainLauncher.main(MainLauncher.java:7) Caused by: java.lang.ClassNotFoundException: javafx.application.Application
>         at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
>         at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
>         at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
>         ... 10 more

So we added the dependencies for javafx again with the following result:

Graphics Device initialization failed for :  d3d, sw
Error initializing QuantumRenderer: no suitable pipeline found
java.lang.RuntimeException: java.lang.RuntimeException: Error initializing QuantumRenderer: no suitable pipeline found
        at com.sun.javafx.tk.quantum.QuantumRenderer.getInstance(QuantumRenderer.java:280)
        at com.sun.javafx.tk.quantum.QuantumToolkit.init(QuantumToolkit.java:222)
        at com.sun.javafx.tk.Toolkit.getToolkit(Toolkit.java:260)
        at com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:267)
        at com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:158)
        at com.sun.javafx.application.LauncherImpl.startToolkit(LauncherImpl.java:658)
        at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:678)
        at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:195)
        at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.RuntimeException: Error initializing QuantumRenderer: no suitable pipeline found
        at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.init(QuantumRenderer.java:94)
        at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:124)
        ... 1 more
Exception in thread "main" java.lang.RuntimeException: No toolkit found
        at com.sun.javafx.tk.Toolkit.getToolkit(Toolkit.java:272)
        at com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:267)
        at com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:158)
        at com.sun.javafx.application.LauncherImpl.startToolkit(LauncherImpl.java:658)
        at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:678)
        at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:195)
        at java.base/java.lang.Thread.run(Thread.java:834)
Hauke
  • 1,405
  • 5
  • 23
  • 44
  • For a fat JAR, your main-class _cannot_ extend `Application`. – Slaw Sep 20 '19 at 09:35
  • See https://stackoverflow.com/questions/52569724/javafx-11-create-a-jar-file-with-gradle/52571719#52571719 – Slaw Sep 20 '19 at 09:44
  • Also https://stackoverflow.com/questions/54063041/package-a-non-modular-javafx-application?noredirect=1&lq=1 – Slaw Sep 20 '19 at 09:45
  • And https://stackoverflow.com/questions/53453212/how-to-deploy-a-javafx-11-desktop-application-with-a-jre – Slaw Sep 20 '19 at 09:48
  • Dear Slaw, we tried that with the second main class already yesterday without luck. We just forgot to mention that. I edited and added the result of that at the bottom. I will check the other links tonight. – Hauke Sep 20 '19 at 11:53
  • Dear Slaw again, we tried the second link, it is almost the same, only using both main classes in the gradle file instead of just using the launcher class on both elements in the gradle file. I added the result at the bottom again. – Hauke Sep 20 '19 at 12:07
  • Your last Gradle file is wrong. Replace "mainClassName = 'de.test.MainFX'" with "mainClassName = 'de.test.Main'" – mipa Sep 20 '19 at 12:09

1 Answers1

0

According to my (painfull) experience until now, the only way to get a large JavaFX 11+ project with a lot of external dependencies working, which you cannot control, is to keep everything on the classpath (also the JavaFX dependencies) and use that wrapper for the main class as you did in your last example. (Your current Gradle file is wrong though. See my comment above.) Trying to fiddle arround with the module system will just take you nowhere and will just waste your time.

I currently do the following to create an exe (or an app on the Mac).

  1. Use the general setup as described above.
  2. Via Maven/Gradle collect all dependencies in a lib folder.
  3. With jlink create a deticated runtime for your program.
  4. Use the EA version of jpackage to create the exe/app.

Once you have figured out all the options that you need this approach works like a charm.

Building your own runtime with jlink is simple.

JAVA_HOME=<the java version you want to use>

$JAVA_HOME/bin/jlink --no-header-files --no-man-pages --compress=2 --strip-debug \
--add-modules <a list of the required or just all modules> \
--output java-runtime

You can use the jdeps tool to find out the modules you need.

mipa
  • 10,369
  • 2
  • 16
  • 35
  • Painfull experience is the correct expression for this. I don't get it why this is so complicated to distribute something. I followed your steps, but I used "gradle installDist" for collecting the dependencies and I just copied a whole JRE instead of using jlink. Its is almost working. As soon as it works I will post my solution here. (And I am not using any module-info.java at all) – Hauke Sep 25 '19 at 05:42
  • Starting with a whole JRE is probably the best thing you can do to get you started but once it works that way the use of jlink can significantly reduce the size of your bundle even if you do not care for the modules. This is because jlink can strip everything from your JRE which you will not need in a bundled application. I'd be interested in the outcome. – mipa Sep 25 '19 at 12:45