java程序打包成exe文件系列一(捆绑JRE/jdk)

2,317 阅读6分钟

前言

尽管 Java 程序的最优运行环境为 Linux ,然而鉴于部分客户的硬件环境状况,我们的部分应用不得不部署于 Windows 操作系统之下。在 JVM 的架构模式中,Java 程序虽具备跨平台运行的能力,然而实际操作起来却颇具复杂性,需先行安装 jdk ,继而配置各类系统变量等。倘若客户对技术领域缺乏了解,那么这一系列操作对其而言着实不够友好。那么,能否将我们的程序制作成一个技术包,使用户仅需点击即可运行呢?接下来,我将为诸位分享借助 maven 来打包 Windows 环境下 Java 程序的多种方式。

众多老铁留言反馈,在操作过程中遭遇诸多问题。在此,我已完整整理相关代码及资源,诸位可通过以下链接进行下载:

一、准备开发环境和项目

我们所运行的各类软件,若就其安装及运行方式而言,大致可归为两类:一类为控制台程序,另一类为包含 gui 的界面交互程序。若采用 Java 进行开发,这两类程序均可实现。我们分别准备两个案例来演示打包步骤,其一为控制台运行,其二为图形化程序。诸位可通过文章头部的资源进行下载。同时,开发环境选用 Apache Maven 3.6.3 与 JDK8 。对于控制台程序,我们采用 springboot ,对于 gui 程序,则使用 javafx 分别进行演示。

二、将 springboot 程序打包成 exe 并绑定 jdk

springboot 打包 jar

我们创建一个 springboot 程序,对 pom.xml 文件的 build 部分进行如下修改,添加一个 pkg-sb 以方便后续的名称管理:

<build>
    <finalName>pkg-sb</finalName>
    <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>

而后运行打包命令:

mvn clean -DskipTests package

如此,在 target 目录下将生成一个 pkg-sb.jar 的文件。实际上,此 jar 文件即可直接运行。倘若在 Windows 环境下已安装 jdk8 ,则可直接运行:

java -jar pkg-sb.jar

但此过程需在控制台中进行,且对于用户而言颇为不便。尽管也可编写一个 bat 脚本,然而并非十分优雅。当下,我们将此程序打包成 exe 文件。

springboot 打包 exe 并绑定 jdk

maven 拥有众多插件可用于打包 EXE ,此处我们选用 launch4j-maven-plugin 来达成目的。将此插件添加至 pom.xml ,先看如下代码:

<plugin>
    <groupId>com.akathist.maven.plugins.launch4j</groupId>
    <artifactId>launch4j-maven-plugin</artifactId>
    <version>1.7.25</version>
    <executions>
        <execution>
            <id>l4j-clui</id>
            <phase>package</phase>
            <goals>
                <goal>launch4j</goal>
            </goals>
            <configuration>
                <!-- 打包类型,可使用值:console、gui  分别代表控制台和图形界面程序-->
                <headerType>console</headerType>
                <!-- jar文件位置 -->
                <jar>target/pkg-sb.jar</jar>
                <!-- 生成exe文件的名称 -->
                <outfile>target/pkg-sb.exe</outfile>
                <errTitle>pkg-sb-error</errTitle>
                <classPath>
                    <!--
                    这里务必要留意,springboot 默认打包后的启动类是此类别,而非我们程序中的 Application 文件,
                    若不清楚,可将打包的 jar 文件解压并查找:META-INF/MANIFEST.MF 文件查看其中的:Main-Class
                    属性值
                    -->
                    <mainClass>org.springframework.boot.loader.JarLauncher</mainClass>
                    <addDependencies>true</addDependencies>
                    <preCp>anything</preCp>
                </classPath>
                <jre>
                    <jdkPreference>jdkOnly</jdkPreference>
                    <!-- jdk运行目录,这里可使用绝对路径,也可使用相对路径,不建议使用绝对路径,此处我们直接采用相对路径,下面此配置表明 jdk 的目录与 exe 的文件处于同级 -->
                    <path>./runtime</path>
                    <jdkPreference>preferJre</jdkPreference>
                    <opts>
                        <opt>-server</opt>
                        <opt>-Xss256k</opt>
                        <opt>-XX:MetaspaceSize=256m</opt>
                        <opt>-XX:MaxMetaspaceSize=256m</opt>
                        <opt>-Xms512m</opt>
                        <opt>-Xmx512m</opt>
                        <opt>-Xmn512m</opt>
                        <opt>-XX:SurvivorRatio=8</opt>
                        <opt>-XX:+AggressiveOpts</opt>
                        <opt>-XX:+UseBiasedLocking</opt>
                        <opt>-XX:+DisableExplicitGC</opt>
                        <opt>-XX:MaxTenuringThreshold=12</opt>
                        <opt>-XX:+UseConcMarkSweepGC</opt>
                        <opt>-XX:+UseParNewGC</opt>
                        <opt>-XX:+CMSParallelRemarkEnabled</opt>
                        <opt>-XX:LargePageSizeInBytes=128m</opt>
                        <opt>-XX:+UseFastAccessorMethods</opt>
                        <opt>-XX:+UseCMSInitiatingOccupancyOnly</opt>
                        <opt>-Djava.awt.headless=true</opt>
                        <opt>-Duser.timezone=GMT+08</opt>
                        <opt>-Dvar1=var1-value-001</opt>
                        <opt>-Dconfig.file=./config.properties</opt>
                    </opts>
                </jre>
                <versionInfo>
                    <fileVersion>1.2.3.4</fileVersion>
                    <txtFileVersion>txt file version?</txtFileVersion>
                    <fileDescription>a description</fileDescription>
                    <copyright>my copyright</copyright>
                    <productVersion>4.3.2.1</productVersion>
                    <txtProductVersion>txt product version</txtProductVersion>
                    <productName>E-N-C-C</productName>
                    <internalName>ccne</internalName>
                    <originalFilename>pkg-sb.exe</originalFilename>
                </versionInfo>
            </configuration>
        </execution>
    </executions>
</plugin>

在此,主要关注 configuration 配置,需留意的几个要点说明如下:

  1.  headerType :打包类型,可使用值:console 、gui 分别代表控制台和图形界面程序。
  2.  jar :jar 文件位置。
  3.  outfile :生成 exe 文件的名称。
  4.  classPath.mainClass :此处务必留意,springboot 默认打包后的启动类是此类别,而非我们程序中的 Application 文件。若不清楚,可将打包的 jar 文件解压并查找:META-INF/MANIFEST.MF 文件查看其中的:Main-Class 属性值。
  5.  jre.path :jdk 运行目录,这里可使用绝对路径,也可使用相对路径,不建议使用绝对路径,此处我们直接采用相对路径,下面此配置表明 jdk 的目录与 exe 的文件处于同级。
  6.  jre.opts :此处可传入运行时的参数设置以及系统变量传递,我们添加了两个环境变量的测试。

接下来,我们再次运行 maven 命令进行打包:

mvn clean -DskipTests package

可以看到已生成 exe 文件:

1.png

完善打包结构并制作发布包

方才完成了 exe 文件的生成,但点击运行时毫无反应,此乃因我们配置的 jdk 运行目录未添加进去。

生成 jdk 运行时

生成 jdk 运行时的方法众多,其一为直接前往官方下载 jre 包,若您的 jdk 版本大于 8 ,若为 jdk8 以下,则直接前往官方下载 jre 。我在此处使用 jlink 来生成,因为 jlink 能够定制所需的模块,生成的运行时相对较小。在 cmd 控制台中使用以下命令生成:

jlink --add-modules java.base,java.compiler,java.desktop,java.net.http,java.management,java.scripting,java.naming,java.sql,java.logging,jdk.unsupported,jdk.httpserver,jdk.internal.jvmstat,jdk.jfr --output runtime

生成的目录结构如下:

2.png

我们运行时的目录名称为 runtime ,而后将方才生成的 EXE 文件拷贝至 runtime 同级的目录下。倘若您是直接下载的 jre ,则需将 jre 的目录文件更改为 runtime ,如下图所示:

3.png

点击运行:

4.png 此处已运行成功,此为一个控制台程序。

制作 zip 发布包

方才我们均为手动操作,将所需资源手动拷贝,接下来我们使用 assembly 插件来对我们所需的资源进行统一管理。首先修改我们的项目结构:

5.png

编写 assembly 配置文件:

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
    <id>package</id>
    <formats>
        <format>zip</format>
    </formats>
    <includeBaseDirectory>true</includeBaseDirectory>
    <fileSets>
        <fileSet>
            <directory>runtime</directory>
            <outputDirectory>\runtime</outputDirectory>
        </fileSet>
        <fileSet>
            <directory>conf</directory>
            <outputDirectory></outputDirectory>
            <includes>
                <include>*.properties</include>
            </includes>
        </fileSet>
        <fileSet>
            <directory>${project.build.directory}</directory>
            <outputDirectory></outputDirectory>
            <includes>
                <include>pkg-sb.exe</include>
            </includes>
        </fileSet>
    </fileSets>
</assembly>

而后在 pom.xml 中添加插件 assembly :

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.4.2</version>
    <configuration>
        <appendAssemblyId>false</appendAssemblyId>
        <finalName>${build.finalName}</finalName>
        <descriptors>
            <descriptor>assembly/package.xml</descriptor>
        </descriptors>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

最后运行 maven 打包命令:

mvn clean -DskipTests package

完成后此处生成了一个 zip 文件:

6.png

接下来,我们可直接将此 zip 发送给用户,解压后的目录格式如下:

7.png

直接双击 exe 运行。下面我们测试一下在打包时传递的系统参数,我们在代码中添加如下代码:

@RequestMapping("/test")
public Object test(){
    String var1=System.getProperty("var1");
    String config_file=System.getProperty("config.file");
    StringBuffer buffer=new StringBuffer();
    buffer.append("获取到系统变量 var1 的值:").append(var1).append("\n");
    if(!StringUtils.isEmpty(config_file)){
        File file=new File(config_file);
        if(file.exists()){
            try (FileInputStream inputStream=new FileInputStream(file)){
                Properties properties=new Properties();
                properties.load(inputStream);
                buffer.append("获取到配置文件信息:").append(properties.toString());
            }catch (Exception e){
                buffer.append("读取配置文件错误:"+e.getMessage());

            }
        }else {
            buffer.append("配置文件不存在");
        }
    }else {
        buffer.append("未获取到配置文件变量信息");
    }
    return  ResponseEntity
           .ok()
           .body(buffer.toString());
}

程序启动成功后,我们访问:http://localhost:12345/test

8.png

表明我们在打包时传递的系统变量在程序中已成功获取。至此,控制台程序打包已完成。

总结

上述我们完成了 springboot 程序打包 EXE 的流程,并且自带 jdk 运行时,如此一来,便不依赖目标机上的 jdk 环境。即便目标机上未曾安装 jdk ,我们的程序亦能正常运行,且依照我们所需的版本进行运行。然而,此种方式仍非尽善尽美,毕竟始终需启动控制台,倘若不慎关闭了控制台,整个程序亦将停止。后续,我将为诸位分享如何将此 exe 制作成 Windows 的系统服务,以使程序于后台运行。因篇幅与时间所限,有意学习的同学请关注我后续的更新。