使用 jpackage 打包 Java 应用

Java应用的分发一直是一个比较麻烦的问题。这是因为Java应用的运行需要虚拟机的支持,仅有Java应用打包的JAR文件是不够的,目标机器还需要安装版本匹配的JDK或JRE。随着云原生和容器化技术的流行,Java应用可以选择以容器镜像的形式来打包和分发,极大地降低了分发难度。不过仍然有相当一部分的Java应用需要直接安装在客户的机器上。通常的解决方案是使用第三方安装工具,如install4j,创建应用的安装包。安装包负责打包应用和所依赖的Java运行环境。安装工具的问题在于过于繁琐,并且通常是收费的。很多时候我们只是需要简单的运行一个Java程序而已。比如,在客户的机器上运行Java编写的数据迁移工具。对于这样的需求,我们可以使用JDK 14中新增的Java打包工具jpackage。该工具在JDK 14和15中是预览功能,在JDK 16中已经成为正式功能。

jpackage 的基本用法

下面以JDK 16来进行说明。在JDK的bin目录下可以找到jpackage工具。jpackage可以生成平台相关的软件包:

  • Linux:deb和rpm
  • macOS:pkg和dmg
  • Windows:msi和exe

默认情况下,jpackage生成与当前运行环境相匹配的软件包。

下面用一个Spring Boot开发的REST服务来进行说明。该应用的代码由Spring Boot自动生成。Spring Boot会把应用代码和相关的第三方依赖打包成单个JAR文件。

./mvnw package

Spring Boot所产生的JAR文件是可以直接运行的。

$ java -jar target/simple-rest-service-0.0.1-SNAPSHOT.jar

对于生成的JAR文件,通过jpackage可以很容易的打包。下面是jpackage工具的用法。

$ jpackage --name simple-rest-service  \
    --input lib \
    --main-jar simple-rest-service-0.0.1-SNAPSHOT.jar

在给出的参数中:

  • --name:打包文件的名称
  • --input:包含全部JAR文件的目录
  • --main-jar:启动应用的JAR文件的名称

jpackage命令运行结束之后,会在当前目录中产生simple-rest-service-1.0.dmg文件。这是macOS上使用的应用安装文件。该文件的大小是68MB。运行该文件可以安装应用,就如同其他macOS应用一样。安装完成之后可以点击运行应用。

如果应用的JAR文件不是可执行的,可以使用参数--main-class来指定入口类的名称。

可以通过参数--arguments来传递启动参数给应用,还可以通过参数--java-options来传递Java系统属性。比如,下面的命令可以把Spring Boot默认的端口改成10080

$ jpackage --name simple-rest-service \
    --input lib \
    --main-jar simple-rest-service-0.0.1-SNAPSHOT.jar \
    --java-options "-Dserver.port=10080"

除了基本的参数之外,还可以设置应用的元数据。

  • --app-version: 应用的版本
  • --copyright:应用的版权信息
  • --description:应用的描述信息
  • --vendor:应用的提供者
  • --icon:应用的图标

下面的命令展示了这些参数的用法。

$ jpackage --name simple-rest-service \
    --input lib \
    --main-jar simple-rest-service-0.0.1-SNAPSHOT.jar \
    --app-version "1.0.0" \
    --vendor "vividcode" \
    --description "Simple REST service" \
    --icon icon.icns

自定义 JDK 镜像

默认生成的应用打包文件比较大,这是因为整个JDK中的模块都被打包了进去。可以对应用使用的JDK镜像进行定制,仅包含应用需要的模块。对于一个应用来说,整个打包过程一般分成三步来完成。

分析应用所依赖的模块

第一步是使用jdeps来分析应用所依赖的模块。这需要把应用所依赖的第三方库全部收集起来,再运行jdeps来输出结果。下面的代码使用Maven的maven-dependency-plugin插件把所有的依赖输出到lib目录。

<plugin>
  <artifactId>maven-dependency-plugin</artifactId>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>copy-dependencies</goal>
      </goals>
      <configuration>
        <outputDirectory>${project.build.directory}/lib</outputDirectory>
      </configuration>
    </execution>
  </executions>
</plugin>

需要注意的是,Spring Boot所打包的单个JAR文件是无法进行扫描的,这是因为该JAR文件使用了特殊的结构来组织应用所依赖的第三方库的JAR文件,无法被jdeps所识别。

接着使用jdeps来输出依赖的JDK模块的名称。参数--print-module-deps的作用是输出模块名称,--ignore-missing-deps的作用是忽略模块解析的错误。

$ jdeps -cp "lib/*" \
    --module-path "lib/*" \
    --multi-release 9 \
    --print-module-deps \
    --ignore-missing-deps \
    simple-rest-service-0.0.1-SNAPSHOT.jar

如果不添加参数--ignore-missing-deps,会产生很多错误,表示找不到依赖的类。这是由于Spring Boot中很多功能是可选的,这些缺失的类在运行时并不会被用到,因此不会影响应用的运行,但是会影响jdeps的检查结果。比如spring-beans会报告缺失Kotlin和Groovy的类,但是如果应用并不使用Kotlin或Groovy,运行时并不会产生影响。

上述命令所产生的结果如下所示:

java.base,java.desktop,java.instrument,java.management.rmi,java.naming,java.prefs,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.jfr,jdk.unsupported

这些就是应用所依赖的JDK模块。

创建自定义的 JDK 镜像

下一步是通过jlink来创建自定义的JDK镜像,如下面的代码所示。参数--add-modules中的模块列表来自jdeps命令的输出。产生的JDK镜像在目录custom-jre中。

$ jlink --add-modules java.base,java.desktop,java.instrument,java.management.rmi,java.naming,java.prefs,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.jfr,jdk.unsupported \
    --output custom-jre

创建应用的打包文件

最后使用jpackage来创建应用的打包文件。参数--runtime-image指向上一步创建的JDK镜像。

$ jpackage --name simple-rest-service \
    --input lib \
    --main-jar simple-rest-service-0.0.1-SNAPSHOT.jar \
    --runtime-image custom-jre

最后产生的安装包只有62.3MB,小于之前产生的68MB。

除了一些通用的参数之外,jpackage还可以使用平台相关的参数来定制安装包。比如,在Windows上,--win-menu可以把应用添加到启动菜单,--win-shortcut可以在桌面上创建快捷方式。macOS和Linux上也有相似的参数。

总得来说,jpackage在很大程度上解决了Java应用的分发问题。这种方式对很多应用来说已经足够好了。

版权所有 © 2024 灵动代码