javaspring-bootjavafxjpackage

How to make a Windows/MacOS/Linux installer for JavaFX & Spring Boot application


I am a beginner for JavaFX. I have a JavaFX & Spring Boot application which runs as a STOMP Websocket client. I did build it as a jar file and it runs well with Java command. My question is how to wrap it in a Windows/Mac/Linux installer (including JRE) so that it can be installed easily on other computers. My pom.xml

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.my-group</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0</version>
    <name>my-app</name>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <version>5.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>19.0.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>19.0.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</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.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

My main class

package com.mygroup.myapp;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.awt.*;

@SpringBootApplication
public class MyApplication extends Application {

    private ConfigurableApplicationContext applicationContext;
    private Parent rootNode;

    public static void main(String[] args) {
        if (!SystemTray.isSupported()) {
            System.setProperty("java.awt.headless", "false");
        }
        Application.launch(args);
    }

    @Override
    public void init() throws Exception {
        applicationContext = SpringApplication.run(MyApplication.class);
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/login.fxml"));
        fxmlLoader.setControllerFactory(applicationContext::getBean);
        rootNode = fxmlLoader.load();
    }

    @Override
    public void start(Stage primaryStage) {
        Platform.setImplicitExit(false);

        primaryStage.setResizable(false);
        primaryStage.setScene(new Scene(rootNode));
        primaryStage.show();
    }

    @Override
    public void stop() {
        applicationContext.close();
        Platform.exit();
    }
}

I have tried this tutorial using JPackage but it does not run after installation.


Solution

  • Background

    This example is only for creating a Windows Installer for a JavaFX SpringBoot application, using Maven and jpackage running on Windows, with the assistance of IntelliJ Idea, though the steps will work for any Java application and can performed without Idea, (given some adaption and modification).

    This is not a guide on how to integrate SpringBoot with JavaFX, but rather how to package and deploy such an application to a Windows machine.

    There are a lot of steps here, the reason for that is:

    1. This guide needs enough detail that somebody with little knowledge could follow it and still have a reasonable chance of succeeding.
    2. Packaging apps to native installers is a large topic and has some complex details.
    3. There are many questions asking how to do this on StackOverflow, and there are indications that many people fail to achieve their goal of creating an easily installed and shared packaged application.
    4. A lot of those who fail seem to find alternate low-quality resources on the web or in youtube channels, which lead them to failure.

    As long as you follow this guide, you should be able to create an installer in a reasonable amount of time and have some chance of troubleshooting the build and installation process when something goes wrong.

    Steps

    Package your App into an Installer

    1. Install Java

    2. Configure your JAVA_HOME with JDK 21+.

    3. Install the JavaFX SDK 21.0.1+.

    4. Install Wix 3.x.

      • Wix 4.x does not work with JDK 21 jpackage.
    5. In Idea 2023.3.2 or later, create a new JavaFX project.

      • Name the project wininstalled.
      • Choose Maven as the build system.
      • Don't select any other options from the new project wizard.
    6. Delete the module-info.java file.

      • SpringBoot 3.2.x works poorly with the Java Platform Module System.
      • You will have a non-modular project, but will reference the JDK and JavaFX modules as modules, not as jars on the classpath. All of your Spring related stuff and other app dependencies will be referenced as jars on the classpath and not as modules.
    7. Upgrade the maven version in the maven wrapper of the generated project.

      • Edit <your project home>\.mvn\wrapper\maven-wrapper.properties

      • Ensure the version listed in the distributionUrl is at least 3.9.6.

        distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
        
      • This is because the akman jpackage plugin used here requires at least that version of maven to work.

    8. Replace the generated pom.xml file, with the one below.

      • Leave everything in the pom.xml as is.
      • You can adjust it to custom fit your app later, but ensure you get the example HelloWorld app up and running first.
    9. Reimport the Maven project into Idea.

      • Because of the project Maven upgrade, the import may not work immediately, in which case close the project, restart the IDE and open the project again, then resync the Maven project, hopefully it will be synched with the IDE OK after that.
    10. Go to the HelloApplication, and try to run it.

      • The app will fail due to missing JavaFX components.
    11. Edit the run configuration that your execution attempt generated so that you can set the VM arguments for it.

      • Set these values (adjust for additional or removed JavaFX modules as needed):

        --add-modules javafx.controls,javafx.fxml --module-path <your JavaFX SDK path>/lib
        
      • Make sure you set VM arguments NOT program arguments.

      • Do actually click the link and study the image of how the VM arguments are set, so you don't make a mistake here.

    12. Run the application again, it should run in the IDE.

    13. Copy the icon file for your application from icon archive (Choose All Download formats | Download ICO) to

      /src/main/resources/com/example/wininstalled/coffee.ico
      
    14. Go to the Maven panel in the IDE, your project name there should be the name configured in the pom.xml (wininstalled), under Lifecycle, double click clean, then click install.

    15. Your application will be built and packaged.

    What the packaging did

    Install the app

    Updating the App

    Troubleshooting

    When things go wrong, which is quite likely . . . for instance, maybe you click the icon of the installed application and nothing happens, and then you think you are stuck, but you are not . . .

    References

    Creating installers for non-SpringBoot or non-JavaFX applications

    Most of the steps described here are general for packaging any Java program and not specific for SpringBoot or JavaFX. So you can apply these steps for other application types with some modifications.

    Even though the target here is SpringBoot, most of the information is generic to deploying a Java application on Windows using a Windows Installer and not that specific for SpringBoot. The only real complication with SpringBoot is that it works best with a non-modular application, but other than that there is nothing special about it in respect to creating installers.

    Similarly, for JavaFX it is only supported when used as modules, so that is what is demonstrated here. Note that a non-modular application can still use JDK and JavaFX modules, so having a non-modular application that uses SpringBoot in a non-modular way, but uses a custom modular Java runtime and JavaFX modules is fine.

    Creating a cross-platform build

    The steps described here are for a Windows-only build.

    Steps to create installers for other platforms can be followed and are largely similar to the steps outlined here for Windows. However, they must be run on a machine of the same type as the target platform you are building for, and will create a platform-specific installer for that machine type.

    The installers built by jpackage will differ, not only by OS, but by architecture type (for instance a Mac Intel installer must be created on a Mac Intel machine, and a Mac M-series processor installer must be created on a Mac M-series machine).

    The steps for other platforms will differ slightly (e.g. linux builds will use rpm or deb packaging rather than wix packaging, and mac builds will create Mac-type package formats such as pkg or dmg). There are different requirements for some things like icon types and formats for different platforms, as well as possibly requirements for signing the app for distribution, which may require paid developer certificates. None of those topics are covered here, but there is information on them in the jpackage resource guide from Oracle.

    Example pom.xml

    <?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>com.example</groupId>
        <artifactId>wininstalled</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>wininstalled</name>
        <description>An installable SpringBoot JavaFX application for Windows</description>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <javafx.version>21.0.1</javafx.version>
            <jpackageInputDirectory>${project.build.directory}/jpackage-input</jpackageInputDirectory>
            <maven.build.timestamp.format>yy.MM.ddHH.mmss</maven.build.timestamp.format>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>${javafx.version}</version>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-fxml</artifactId>
                <version>${javafx.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
                <version>3.2.1</version>
            </dependency>
    
            <dependency>
                <groupId>com.h2database</groupId>
                <artifactId>h2</artifactId>
                <version>2.2.224</version>
                <scope>runtime</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.3.0</version>
                    <configuration>
                        <outputDirectory>${jpackageInputDirectory}</outputDirectory>
                    </configuration>
                </plugin>
    
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-dependency-plugin</artifactId>
                    <version>3.6.0</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>copy-dependencies</goal>
                            </goals>
                            <configuration>
                                <outputDirectory>
                                    ${jpackageInputDirectory}/lib
                                </outputDirectory>
                                <excludeGroupIds>
                                    org.openjfx
                                </excludeGroupIds>
                                <includeScope>
                                    runtime
                                </includeScope>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
    
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.11.0</version>
                    <configuration>
                        <source>21</source>
                        <target>21</target>
                        <parameters>true</parameters>
                    </configuration>
                </plugin>
    
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.3.1</version>
                    <executions>
                        <execution>
                            <id>auto-clean</id>
                            <phase>verify</phase>
                            <goals>
                                <goal>clean</goal>
                            </goals>
                            <configuration>
                                <excludeDefaultDirectories>true</excludeDefaultDirectories>
                                <filesets>
                                    <fileset>
                                        <directory>${project.build.directory}/jpackage</directory>
                                    </fileset>
                                </filesets>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
    
                <plugin>
                    <groupId>com.github.akman</groupId>
                    <artifactId>jpackage-maven-plugin</artifactId>
                    <version>0.1.5</version>
                    <executions>
                        <execution>
                            <phase>verify</phase>
                            <goals>
                                <goal>jpackage</goal>
                            </goals>
                            <configuration>
                                <name>wininstalled</name>
                                <appversion>${maven.build.timestamp}</appversion>
                                <copyright>Unrestricted freeware</copyright>
                                <description>JavaFX windows installed app demo.</description>
                                <vendor>Acme Widgets, Inc.</vendor>
                                <icon>${project.basedir}/src/main/resources/com/example/wininstalled/coffee.ico</icon>
                                <modulepath>
                                    <dependencysets>
                                        <dependencyset>
                                            <includeoutput>false</includeoutput>
                                            <excludeautomatic>true</excludeautomatic>
                                            <includes>
                                                <!-- todo would it be better to fetch jmods and use them? -->
                                                <include>glob:**/javafx-*-win.jar</include>
                                            </includes>
                                        </dependencyset>
                                    </dependencysets>
                                </modulepath>
                                <addmodules>
                                    <!-- we add required modules here,
                                         we need to include base ones from the jdk which are not
                                         part of the minimum service set that jpackage uses by default,
                                         for example jdk.crypto.cryptoki is needed for ssl support and
                                         jdk.crypto.ec if you need to support elliptic curve ciphers in ssl
                                         and java.sql if you (or a library you use) uses jdbc, etc.
                                         you would want different ones for another app,
                                         libraries that are not treated as modular should need to be listed,
                                         transitively included modules don`t need to be listed -->
                                    <addmodule>jdk.crypto.cryptoki</addmodule>
                                    <addmodule>jdk.crypto.ec</addmodule>
                                    <addmodule>java.sql</addmodule>
                                    <addmodule>java.naming</addmodule>
                                    <addmodule>java.net.http</addmodule>
                                    <addmodule>java.instrument</addmodule>
                                    <addmodule>javafx.controls</addmodule>
                                    <addmodule>javafx.fxml</addmodule>
                                    <!-- if you want these other javafx modules then
                                         uncomment them and ensure you
                                         also have maven dependencies for them -->
    <!--                                <addmodule>javafx.media</addmodule>-->
    <!--                                <addmodule>javafx.swing</addmodule>-->
    <!--                                <addmodule>javafx.web</addmodule>-->
                                </addmodules>
                                <!-- our app is non-modular, so we wont have a module entry, we set the mainjar and mainclass instead -->
                                <!--                            <module>com.example.wininstalled/HelloApplication</module>-->
                                <input>${jpackageInputDirectory}</input>
                                <mainjar>wininstalled-1.0-SNAPSHOT.jar</mainjar>
                                <mainclass>com.example.wininstalled.HelloApplication</mainclass>
                                <!--                            <javaoptions>-Dfile.encoding=UTF-8</javaoptions>-->
                                <!--                            <installdir>Utilities/Win Installed FX App</installdir>-->
                                <!--                            <licensefile>${project.basedir}/config/jpackage/LICENSE</licensefile>-->
                                <!--                            <resourcedir>${project.basedir}/config/jpackage/resources</resourcedir>-->
                                <!--                            <windirchooser>false</windirchooser>-->
                                <winmenu>true</winmenu>
                                <!--                            <winmenugroup>Utilities/Win Installed FX App</winmenugroup>-->
                                <winperuserinstall>true</winperuserinstall>
                                <winshortcut>true</winshortcut>
                                <!--                            <winupgradeuuid>${project.build.uuid}</winupgradeuuid>-->
    
                                <!-- if something goes wrong (and it will..) enable the winconsole and run the app from the command line
                                     then if the app aborts with an exception you can see it
                                     To run from the command line execute
                                        <your user home>\AppData\Local\<your app>\<your app>.exe
                                     -->
                                <!--                            <winconsole>true</winconsole>-->
                                <type>EXE</type>
                                <verbose>true</verbose>
                                <!-- example for setting jvm options if needed -->
                                <javaoptions>--enable-preview</javaoptions>
                            </configuration>
                        </execution>
                    </executions>
                    <dependencies>
                        <dependency>
                            <groupId>org.ow2.asm</groupId>
                            <artifactId>asm</artifactId>
                            <version>9.5</version>
                        </dependency>
                    </dependencies>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>3.2.1</version>
                </plugin>
            </plugins>
        </build>
    </project>
    

    Related