Java应用程序的分发

Java应用程序的分发

说到给一些比较外行的人发Java程序,让他们能够运行起程序来,这是关于Java程序分发的问题。

可以从最直白的思路想,Java程序是如何运行起来的。

从源码的.java编译成.class再打包成.jar形式,然后就可以通过命令java -jar xxx.jar来执行程序了。

这一过程中编译打包的部分使用到了JDK,后面的部分执行时使用到了JRE。

就是说我们自己需要JDK来编写编译打包,执行的人需要JRE就可以了。

那么这里就有一个直白的做法了。

编译源码打包+JRE+shell脚本

我们可以创建一个文件夹,这个文件夹里面有打包好的Java程序以及JRE,再写一个对应操作系统的shell脚本。

这样做使用我们程序的人就可以直接通过双击脚本来执行程序了。

但这里面可能要注意一下JRE的提取问题以及这个方案的缺点。

JRE的提取

JDK8以及之前

这些版本的JDK是直接包含JRE的,就在JDK的目录下有一个jre的目录,直接复制文件夹即可。

或者你可以直接去对应的官网下载对应的jre,下面给个Oracle的链接:

Java Archive Downloads - Java SE 8u211 and later | Oracle 中国

JDK9以及之后

JDK8之后引入了模块化的设计,不再像之前一样直接带有完整功能的JRE,一般Java程序不会什么都用到,所以这样设计所需的JRE部分体积会变小。

关于这方面的介绍,这里我贴两个链接,在这里就不过多描述了。

了解 Java 9 模块 | Oracle 中国

模块 - Java教程 - 廖雪峰的官方网站

这里还讲讲实际操作的大概过程,最方便直接的做法是打包成一个.jar后,开始分析这个jar所依赖的Java模块(当然最好是开始时候就要写这些东西了)

使用jdeps来分析一个jar大致所所依赖的Java模块,示例命令:jdeps.exe --print-module-deps --recursive --ignore-missing-deps <jar file>

--print-module-deps:打印 jar 文件所需的 JDK 模块。

--recursive:递归分析 jar 文件中所有类的依赖。

--ignore-missing-deps:忽略无法找到的依赖模块。

演示过程:

1
2
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> jdeps --print-module-deps --recursive --ignore-missing-deps .\gugu_typora_image_magic-1.0.0-jar-with-dependencies.jar
java.base,java.compiler,java.instrument,jdk.unsupported

上面通过命令分析输出所需的Java模块java.base,java.compiler,java.instrument,jdk.unsupported,接下来就是提取jre,使用命令jlink,示例命令:jlink --add-modules <modules> --output <path>

演示过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> jlink --add-modules "java.base,java.compiler,java.instrument,jdk.unsupported" --output "./jre"
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> dir


目录: E:\Temp\Example\GuGu_Typora_Image_Magic\target


Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2024/8/25 12:01 archive-tmp
d----- 2024/8/25 12:38 classes
d----- 2024/8/25 12:38 generated-sources
d----- 2024/8/25 12:52 jre
d----- 2024/8/25 12:01 maven-archiver
d----- 2024/8/25 12:38 maven-status
-a---- 2024/8/25 12:01 4392012 gugu_typora_image_magic-1.0.0-jar-with-dependencies.jar
-a---- 2024/8/25 12:01 16623 gugu_typora_image_magic-1.0.0.jar

PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> tree jre
Data 的文件夹 PATH 列表
E:\TEMP\EXAMPLE\GUGU_TYPORA_IMAGE_MAGIC\TARGET\JRE
├─bin
│ └─server
├─conf
│ └─security
│ └─policy
│ ├─limited
│ └─unlimited
├─include
│ └─win32
├─legal
│ ├─java.base
│ ├─java.compiler
│ ├─java.instrument
│ └─jdk.unsupported
└─lib
├─security
└─server

接下来运行程序看看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> .\jre\bin\java.exe -jar .\gugu_typora_image_magic-1.0.0-jar-with-dependencies.jar
Exception in thread "main" java.lang.NoClassDefFoundError: java/util/logging/Logger
at org.yaml.snakeyaml.internal.Logger.<init>(Logger.java:30)
at org.yaml.snakeyaml.internal.Logger.getLogger(Logger.java:34)
at org.yaml.snakeyaml.TypeDescription.<clinit>(TypeDescription.java:43)
at org.yaml.snakeyaml.constructor.Constructor.<init>(Constructor.java:60)
at org.yaml.snakeyaml.constructor.Constructor.<init>(Constructor.java:50)
at org.yaml.snakeyaml.Yaml.<init>(Yaml.java:64)
at com.gugumin.tim.pojo.Config.filling(Config.java:32)
at com.gugumin.tim.pojo.Config.load(Config.java:27)
at com.gugumin.tim.App.main(App.java:26)
Caused by: java.lang.ClassNotFoundException: java.util.logging.Logger
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)
... 9 more

哦呀,报错了,看起来是找不到类,说明我们导出的jre还是有问题,既然缺少了那就继续加入引入就好了。

我们要先确定java.util.logging.Logger是属于哪个模块下的类,我们可以使用IDEA定位类去找到对应的模块

image-20240825125902030

我们知道了模块名java.logging,再把模块加到命令中继续提取jre就好,演示过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> jlink --add-modules "java.base,java.compiler,java.instrument,jdk.unsupported,java.logging" --output "./jre"
错误: directory already exists: .\jre
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> remove-item jre -Recurse -Force
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> jlink --add-modules "java.base,java.compiler,java.instrument,jdk.unsupported,java.logging" --output "./jre"
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> .\jre\bin\java.exe -jar .\gugu_typora_image_magic-1.0.0-jar-with-dependencies.jar
Exception in thread "main" java.lang.NoClassDefFoundError: java/beans/IntrospectionException
at org.yaml.snakeyaml.constructor.BaseConstructor.getPropertyUtils(BaseConstructor.java:640)
at org.yaml.snakeyaml.constructor.BaseConstructor.addTypeDescription(BaseConstructor.java:659)
at org.yaml.snakeyaml.constructor.Constructor.<init>(Constructor.java:107)
at org.yaml.snakeyaml.constructor.Constructor.<init>(Constructor.java:60)
at org.yaml.snakeyaml.constructor.Constructor.<init>(Constructor.java:50)
at org.yaml.snakeyaml.Yaml.<init>(Yaml.java:64)
at com.gugumin.tim.pojo.Config.filling(Config.java:32)
at com.gugumin.tim.pojo.Config.load(Config.java:27)
at com.gugumin.tim.App.main(App.java:26)
Caused by: java.lang.ClassNotFoundException: java.beans.IntrospectionException
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)
... 9 more
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> remove-item jre -Recurse -Force;jlink --add-modules "java.base,java.compiler,java.instrument,jdk.unsupported,java.logging,java.desktop" --output "./jre"
PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> .\jre\bin\java.exe -jar .\gugu_typora_image_magic-1.0.0-jar-with-dependencies.jar
Exception in thread "main" java.lang.RuntimeException: Please provide image path
at com.gugumin.tim.utils.ValidUtil.checkPath(ValidUtil.java:18)
at com.gugumin.tim.App.main(App.java:27)

上面的过程中还出现了缺少的一个模块,我们再定位类再引入它,至此程序已经能够启动了,抛出的异常是属于自己编写的代码逻辑,后续还出现类似的错误可以根据上面的思路进行排查。

缺点

对比其他其他语言,不能直接一步到位编译成二进制可执行文件,而且就算源码通过编译打包,对于Java来说这些都是可逆向的,源码安全性差。

exe4j

上面的方案有说到不能一步到位生成二进制文件,这里再介绍一下生成二进制可执行文件的方法。

使用exe4j这个工具,我们可以让Java编译打包的jar生成伪二进制可执行文件。

为什么会带伪呢?因为这个东西的原理是压缩成exe执行的时候再解压成jar,通过执行命令来运行…

下面直接贴一些链接来了解它:

使用 exe4j 将 jar 包生成 .exe 文件(敲详细)

exe4j生成的exe反编译成java代码

Native Image

Java难道就不可以编译成二进制文件了吗?是可以,但是得使用到GraalVM的17版本及其以上。

JVM其实是用很多不同的版本,JVM是有一套规范,实现了这套规范的都可以作为JVM。

我们常用的应该是HotSpot的JVM吧,这里要介绍的JVM是GraalVM,它与HotSpot的侧重点不同,它能够实现编译二进制可执行文件。

它能够编译成二进制可执行文件的功能称之为Native Image,这里先贴官网文档链接来提供快速了解:

https://www.graalvm.org/22.0/reference-manual/native-image/

它的实际操作与上面的提取JDK9以及之后的jre有些类似,但也要稍微复杂一些。

一开始还是要分析程序所涉及的依赖与操作(反射等),这些最好还是一开始编写的时候就顺带编写,当然事后也是可以通过工具来分析。

官方的文档也已经描述了安装Native Image的过程:https://www.graalvm.org/latest/docs/getting-started/windows/,也可以自行搜索其他文章来完成环境的安装。

下面列举个编译示例演示。

  1. 分析jar运行时动态配置等信息

    这一部分可以通过GraalVM的工具生成或者编写代码时留意顺带编写。这里演示是先用工具分析jar,如有缺失再手动补齐。

    1
    2
    3
    4
    5
    6
    7
    8
    PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> java --version
    java 21.0.2 2024-01-16 LTS
    Java(TM) SE Runtime Environment Oracle GraalVM 21.0.2+13.1 (build 21.0.2+13-LTS-jvmci-23.1-b30)
    Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 21.0.2+13.1 (build 21.0.2+13-LTS-jvmci-23.1-b30, mixed mode, sharing)
    PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> java -agentlib:native-image-agent=config-output-dir=./native-image-config -jar .\gugu_typora_image_magic-1.0.0-jar-with-dependencies.jar
    Exception in thread "main" java.lang.RuntimeException: Please provide image path
    at com.gugumin.tim.utils.ValidUtil.checkPath(ValidUtil.java:18)
    at com.gugumin.tim.App.main(App.java:27)

    上面命令的参数,这里贴一个官网文档链接:https://www.graalvm.org/latest/reference-manual/native-image/metadata/AutomaticMetadataCollection/

    执行玩上述的命令后,当前目录下生成了native-image-config文件夹,该文件夹内容结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    PS E:\Temp\Example\GuGu_Typora_Image_Magic\target> tree /F .\native-image-config\
    卷 Data 的文件夹 PATH 列表
    卷序列号为 xxxx-xxxx
    E:\TEMP\EXAMPLE\GUGU_TYPORA_IMAGE_MAGIC\TARGET\NATIVE-IMAGE-CONFIG
    jni-config.json
    predefined-classes-config.json
    proxy-config.json
    reflect-config.json
    resource-config.json
    serialization-config.json

    └─agent-extracted-predefined-classes

    上面的配置文件以.json格式存储,这部分的内容可以参考官方文档:https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/

  2. 编译

    上面我们可以得到程序的动态配置,接下来就需要使用到C++环境来执行native-image命令来编译(这一部分是之前安装visual stodio的C++组件获得的)。

    Snipaste-2024-08-27-13-28-13.png

    接下来我们就使用native-image来编译二进制可执行文件:

    image-20240827130040676

    上面出了点小问题,但这里先解释一下参数:

    -H:ConfigurationFileDirectories:指定编译配置文件夹路径,就是我们在上面通过工具生成的配置文件夹。

    --no-fallback:生成独立的可执行文件夹(不包括其他配置文件夹如DLL等)

    上面的小问题,按它提示说的,再加个参数忽略检查即可。

    image-20240827130533092.png

    至此已经生成了一个可执行二进制文件,我们直接将它丢入虚拟机中执行试一试:

    Snipaste-2024-08-27-13-21-14.png

    可以看到,在没有运行时环境下也能正常运行,而且编译成了二进制后源码安全性提高了,使得更不容易得到Java源代码。

总结

Java不太适合处理有分发需求的程序,整个过程比较繁琐,建议还是使用其他语言编写分发吧…


Java应用程序的分发
https://blog.gugu.dev/2024-08-27/Java应用程序的分发/
作者
MinMin
发布于
2024年8月27日
许可协议