I am trying to create a Java desktop application that can be distributed on Windows using a combination of SpringBoot and OpenJFX. However, I can create an executable Jar, but I am having trouble launching it as an exe using exewrap or launch4j. When I run the exe created by exewrap, I also get an error that Start-Class is not registered in MANIFEST.MF. However, Start-Class is registered in MANIFEST.MF. There are many QAs with similar problems and I have tried all kinds of information but cannot solve the problem. Please help me with your wisdom.
When creating an exe in exewrap or Launch4j, after checking dependencies in jdeps, create a lightweight JRE from OpenJFX and load it from the same directory level.
The build environment is Windows 11 and uses intellij.
I also attach the pom.xml I made.
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.2
Build-Jdk-Spec: 21
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: myPakage.myApp.MyAppApplication
Spring-Boot-Version: 3.4.1
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>myPakage</groupId>
<artifactId>myApp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>myApp</name>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
</parent>
<properties>
<start-class>myPakage.myApp.MyAppApplication</start-class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
<jfx.version>21.0.1</jfx.version>
<javacpp.version>1.5.10</javacpp.version>
<javacpp.windows-x86_64>windows-x86_64</javacpp.windows-x86_64>
<poi.version>5.3.0</poi.version>
<jackson.version>2.16.1</jackson.version>
<slf4j.version>2.0.16</slf4j.version>
<log4j.version>2.23.1</log4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
<version>6.6.1.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- OpenJFX -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${jfx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${jfx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>${jfx.version}</version>
</dependency>
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>11.2.1</version>
</dependency>
<!-- logger -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/libs
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<encoding>UTF-8</encoding>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>${start-class}</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>${start-class}</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
This gets a little tricky because Spring is non-modular and JavaFX should be run with the JavaFX libraries on the module path, not the classpath.
Here's how I made this work. This may not be optimal. The basic idea is to create a non-modular application, use Maven to copy the dependencies (except the JavaFX modules) to lib
folder and biuld a jar file with the application. Then use jlink
to create a JDK with the JAvaFX modules included, and finally use jpackage
to build a native package using that JDK, the dependencies, and the jar file.
I'm using IntelliJ (version 2024.3.2.1) and this includes some IntelliJ-specific steps which can probably easily be adapted for other IDEs. I'm also using JDK 23.
You will need to download the JavaFX modules from here. Make sure you get the modules for your target platform and download the jmods, not the SDK. I downloaded the zip file to ~/javafx-mods
and unzipped it, creating ~/javafx-mods/javafx-jmods-23.0.2
inside of which there are seven .jmod
files.
First I created a new project in IntelliJ. I chose the "JavaFX" generator, "Java" for language, and "Maven" for build tool. It uses (by default) JDK 23.
I deleted the module.java
file, to make the project non-modular, and then edited the pom.xml
file. These edits added the Spring dependencies, a plugin to copy the dependencies to the lib folder, and made sure I had the correct source and target JDK versions. Here is the finished pom.xml
(note you can't copy this directly, you'll at least need to edit the groupId, artifactId, name, etc.):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.jamesd.examples</groupId>
<artifactId>springfx23</artifactId>
<version>1.0-SNAPSHOT</version>
<name>springfx23</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.2</junit.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.2</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>23.0.2</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>23.0.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>23</source>
<target>23</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<excludeGroupIds>org.openjfx</excludeGroupIds>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
I then modified the demo application to use spring. There are several discussions on this site about the best ways to do this (it's a bit complicated because you have two different frameworks both with their own application startup mechanisms). I won't repeat those discussions here, but my strategy is to use Spring Boot to start up the application and then Platform.startup(...)
to bypass the standard JavaFX Application startup process and start the JavaFX runtime manually.
Here is a Spring bean that provides a message:
package org.jamesd.examples.springfx23;
public class MessageProvider {
public String getMessage() {
return "Hello from JavaFX with Spring";
}
}
The controller class modified to use Spring bean injection:
package org.jamesd.examples.springfx23;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import org.springframework.beans.factory.annotation.Autowired;
public class HelloController {
@Autowired
private MessageProvider messageProvider;
@FXML
private Label welcomeText;
@FXML
protected void onHelloButtonClick() {
welcomeText.setText(messageProvider.getMessage());
}
}
The spring configuration class:
package org.jamesd.examples.springfx23;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Config {
@Bean
public HelloController helloController() {
return new HelloController();
}
@Bean
public MessageProvider messageProvider() {
return new MessageProvider();
}
}
The FXML (still called hello-view.fxml
):
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jamesd.examples.springfx23.HelloController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
</padding>
<Label fx:id="welcomeText"/>
<Button text="Hello!" onAction="#onHelloButtonClick"/>
</VBox>
and finally the HelloApplication
class, modified to use Spring:
package org.jamesd.examples.springfx23;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import java.io.IOException;
@SpringBootApplication
public class HelloApplication implements ApplicationRunner {
@Autowired
private ApplicationContext appContext;
public void run(ApplicationArguments args) {
Platform.startup(this::runUI);
}
public void runUI() {
try {
Stage stage = new Stage();
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
fxmlLoader.setControllerFactory(appContext::getBean);
Scene scene = new Scene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
} catch (IOException exc) {
exc.printStackTrace();
}
}
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
Next I ran the Maven build (from IntelliJ, View -> Tool Windows -> Maven, and then press the green run button in that view "Run Maven Build"). This will create the jar file and the lib
folder inside the target
directory.
The next step is to build the JDK with JavaFX included. From the terminal (I am on a mac, it should be essentially the same for windows) I ran
jlink --add-modules javafx.base,javafx.graphics,javafx.controls,javafx.fxml --module-path javafx-mods/javafx-jmods-23.0.2 --output JavaWithFX23
(here javafx-jmods
is the directory to which I downloaded the openjfx jmod archive, and javafx-jmods-23.0.2
was created when I unzipped it). This creates a new JDK inside JavaWithFX23
which includes the javafx.base
, javafx.controls
and javafx.fxml
modules. (Note here javafx.base
is redundant, the others require it transitively anyway, so you can omit it. You might need to adjust the list of modules in the jlink
command, e.g. if you also need javafx.media
etc. Just make them match what's in your pom.xml
.)
Finally, run jpackage
to create your native installation bundle. My project folder is ~/Documents/Java/FX Projects/springfx23
so the command is
jpackage --runtime-image JavaWithFX23/ --name "Hello Spring World" --dest HelloSpring --main-jar ~/Documents/Java/FX\ Projects/springfx23/target/springfx23-1.0-SNAPSHOT.jar --main-class org.jamesd.examples.springfx23.HelloApplication --input ~/Documents/Java/FX\ Projects/springfx23/target/lib
This creates a .dmg
(since I'm on a Mac) in the HelloSpring
folder which can install the application.
See the documentation for jlink
and jpackage
for more on using those tools.
As I said earlier, this may be non-optimal, and it makes the build process a little too hands-on, but it works. Others may have more streamlined approaches and/or Maven plugins that can automate this process.
It is possible to combine the jlink
and jpackage
steps into a single step using
jpackage --add-modules javafx.controls,javafx.fxml --module-path javafx-mods/javafx-jmods-23.0.2 --name "Hello Spring World" --dest HelloSpring --main-jar ~/Documents/Java/FX\ Projects/springfx23/target/springfx23-1.0-SNAPSHOT.jar --main-class org.jamesd.examples.springfx23.HelloApplication --input ~/Documents/Java/FX\ Projects/springfx23/target/lib
but I usually find it helpful to have the custom runtime available to debug anything that goes wrong. Also consider using --type app-image
as a jpackage
command to create an on-disk runnable version, which again can be useful for debugging.