前言
通过本文了解一下从 Java 文件生成可执行的 jar 文件的过程。有时候我们在使用第三方 SDK 的时候,如果其内部实现有 bug 或接口定义不够灵活时会一时间束手无策。我们了解了 jar 文件的实现机制,就可以对 SDK 进行二次加工。 在不做破坏性改动的前提下,给自己的开发工作带来遍历。
java 到 jar
所有文件在一个包下
在同一个包 src 下有下面两个类
- People
1 | public class People { |
- Main
1 | public class Main { |
命令行执行 javac xxx.java 后会生成对应的 .class 文件,然后执行 java Main 输出:
1 | hello world |
为了保持代码的整洁,可以使用 javac -d xxx
-d 参数指定生成的 class 文件的位置。
为了方便,可以把 class 打包成 jar 文件,方便运行。
jar 文件生成
准备一个 manifest.txt 文件,需要以下内容
1 | Main-Class: Main |
Main 即包含 main 方法的那个类
Main 和冒号之前一定要有空格,Main 后面一定要有换行
manifest.txt 最好和 Main 这个 class 文件在同一目录下
然后执行命令:
1 | jar -cvmf mainfest.txt app.jar *.class |
可以简单看一下 jar 这个命令的帮助文档
jar --help
1 | 用法: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ... |
这里有一点非常重要的是 : m f 这两个指令的顺序要和后面 清单文件名和归档文件名的顺序保持一致。切记,切记。
即可生成一个可执行的 jar 文件,执行命令 java -jar app.jar 输出结果:
1 | hello world |
jar -tf xxx.jar 可以查看 jar 文件内容
jar -xf xxx.jar 可以解压 jar 文件
java 文件在不同的包中
上面是理想的情况,所有类在一个文件夹下(包)下,但是这是很少见的,为了代码规范及防止包名冲突等原因,一般情况下我们都会分包,根据业务或代码架构的层级会把 java 文件放到不同的包里。因此生成的 class 文件也在不同的包里。
![](/2019/09/12/%E5%A6%82%E4%BD%95%E6%8A%8AJava%E6%96%87%E4%BB%B6%E6%89%93%E5%8C%85%E4%B8%BAJar%E6%96%87%E4%BB%B6/ui.jpg)
如上图,在 com.ui 包下,包含一个文件批量重命名的功能,并且包含一个简单的 java Swing 实现的UI 选择器。对于文件批量重命名的功能,可以用各种各样的脚步语言实现,但是这样的一个 java Swing 实现方案,可以用鼠标 灵活的选择路径和各种配置,因此生成 jar文件之后,可以有很强的复用性。
下面就来看看,对于一个在包 内的 java 类如何打包成 jar 文件。
其实具体实现和上面没有 包是类似的,我们稍微想一下,对于多个包名的 java 是如何执行的就可以找到规律了。例如,对于上图中 App 类,出于 com.ui package下,那么相比于普通的类,在执行的时候包含包名就可以了
1 | java com.ui.App |
你可能会好奇了,那么这个命令应该在哪个路径下执行呢?在项目根目录执行似乎是不可以的。这个就要回归到 javac 命令的执行逻辑了。
大部分情况下,我们都是使用诸如 Eclipse/IntelliJ 系列/vscode… 等这些 IDE 进行编码工作,当我们写完代码执行一个 run 或者是 build 功能的按钮或快捷键,所有的关于编译的工作和运行的工作(当然这里以熟悉的 Java 和 Kotlin 为例,C/C++ 可能还涉及更多的步骤,Python/js 可能只需要最终的解释执行,但这不是重点)都会由 IDE 默默为我们完成,让我们把精力集中在编码中,去解决更多业务相关的问题。
比如上图的 Java 项目,在 IntelliJ IDEA 中,运行后会在 out 目录下(这个目录是默认的,可以手动配置更改) 生成相关的编译产物。
![](/2019/09/12/%E5%A6%82%E4%BD%95%E6%8A%8AJava%E6%96%87%E4%BB%B6%E6%89%93%E5%8C%85%E4%B8%BAJar%E6%96%87%E4%BB%B6/class-dir.jpg)
因此,刚才上面执行的命令,在这里的 out/production/JavaArt 目录下执行即可。
到这一步,其实和普通的 jar 包生成就非常相似了
manifext.txt 文件的准备
1
2Main-Class: com.ui.App
打包命令
1
jar -cvmf manifest.txt app.jar com/ui
就是就这么简单,Main-Class 需要根据包名指定要 main 方法所在的类,打包的时候指定具体要打包的目录即可。
最终结果,生产了 app.jar 文件。
![](/2019/09/12/%E5%A6%82%E4%BD%95%E6%8A%8AJava%E6%96%87%E4%BB%B6%E6%89%93%E5%8C%85%E4%B8%BAJar%E6%96%87%E4%BB%B6/result.jpg)
可以通过 jar -tf app.jar 命令查看一下生成的 jar 文件包含些什么。
![](/2019/09/12/%E5%A6%82%E4%BD%95%E6%8A%8AJava%E6%96%87%E4%BB%B6%E6%89%93%E5%8C%85%E4%B8%BAJar%E6%96%87%E4%BB%B6/info.jpg)
可以看到包含了 ui 包下所有的 class文件。
这样,这个 app.jar 就可以当做一个小小的可执行程序,之后需要文件批量重命名的场景,java jar app.jar 就可以打开一个简单的 UI 视图进行操作了。
javac -d
除了使用 IDE 指定的默认路径之外,我们也可以使用 javac -d 在编译的时候指定自定义的路径,来规定 class 文件生成的路径。这个路径确定好之后,后续步骤和 对 IDE 生成的 class 的处理没有区别了。
总结
简单了解一下 jar 文件的生成,通过不带包名的实现入门,了解了对于包含包名的类(毕竟这才是常态)的处理。
开篇提到的对第三方 SDK 的二次加工就不展开了,简单来说就是生成同名的类,替换原来的 class 二次打包的过程。但是具体实施过程中,还需要很多细节需要考虑,比如原文件包含混淆时如何处理,要生成的同名类,如果包名类包含对其他类的依赖又该如何处理。