0%

如何把Java文件打包为Jar文件

前言

通过本文了解一下从 Java 文件生成可执行的 jar 文件的过程。有时候我们在使用第三方 SDK 的时候,如果其内部实现有 bug 或接口定义不够灵活时会一时间束手无策。我们了解了 jar 文件的实现机制,就可以对 SDK 进行二次加工。 在不做破坏性改动的前提下,给自己的开发工作带来遍历。

java 到 jar

所有文件在一个包下

在同一个包 src 下有下面两个类

  • People
1
2
3
4
5
6
7
8
9
10
11
12
13
public class People {
private String name;
private int age;

public People(String name,int age) {
this.name = name;
this.age = age;
}

public void print() {
System.out.println("people : "+this.name+","+this.age);
}
}
  • Main
1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
System.out.println("hello world");
People people = new People("mike",22);
people.print();
}
}

命令行执行 javac xxx.java 后会生成对应的 .class 文件,然后执行 java Main 输出:

1
2
hello world
people : mike,22

为了保持代码的整洁,可以使用 javac -d xxx

-d 参数指定生成的 class 文件的位置。

为了方便,可以把 class 打包成 jar 文件,方便运行。

jar 文件生成

准备一个 manifest.txt 文件,需要以下内容

1
2
Main-Class: Main

Main 即包含 main 方法的那个类

Main 和冒号之前一定要有空格,Main 后面一定要有换行

manifest.txt 最好和 Main 这个 class 文件在同一目录下

然后执行命令:

1
jar -cvmf mainfest.txt app.jar *.class

可以简单看一下 jar 这个命令的帮助文档

jar --help
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
用法: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
选项:
-c 创建新档案
-t 列出档案目录
-x 从档案中提取指定的 (或所有) 文件
-u 更新现有档案
-v 在标准输出中生成详细输出
-f 指定档案文件名
-m 包含指定清单文件中的清单信息
-n 创建新档案后执行 Pack200 规范化
-e 为捆绑到可执行 jar 文件的独立应用程序
指定应用程序入口点
-0 仅存储; 不使用任何 ZIP 压缩
-P 保留文件名中的前导 '/' (绝对路径) 和 ".." (父目录) 组件
-M 不创建条目的清单文件
-i 为指定的 jar 文件生成索引信息
-C 更改为指定的目录并包含以下文件
如果任何文件为目录, 则对其进行递归处理。
清单文件名, 档案文件名和入口点名称的指定顺序
与 'm', 'f' 和 'e' 标记的指定顺序相同。

示例 1: 将两个类文件归档到一个名为 classes.jar 的档案中:
jar cvf classes.jar Foo.class Bar.class
示例 2: 使用现有的清单文件 'mymanifest' 并
将 foo/ 目录中的所有文件归档到 'classes.jar' 中:
jar cvfm classes.jar mymanifest -C foo/ .

这里有一点非常重要的是 : m f 这两个指令的顺序要和后面 清单文件名和归档文件名的顺序保持一致。切记,切记。

即可生成一个可执行的 jar 文件,执行命令 java -jar app.jar 输出结果:

1
2
hello world
people : mike,22

jar -tf xxx.jar 可以查看 jar 文件内容

jar -xf xxx.jar 可以解压 jar 文件

java 文件在不同的包中

上面是理想的情况,所有类在一个文件夹下(包)下,但是这是很少见的,为了代码规范及防止包名冲突等原因,一般情况下我们都会分包,根据业务或代码架构的层级会把 java 文件放到不同的包里。因此生成的 class 文件也在不同的包里。

如上图,在 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 目录下(这个目录是默认的,可以手动配置更改) 生成相关的编译产物。

因此,刚才上面执行的命令,在这里的 out/production/JavaArt 目录下执行即可。

到这一步,其实和普通的 jar 包生成就非常相似了

  1. manifext.txt 文件的准备

    1
    2
    Main-Class: com.ui.App

  2. 打包命令

    1
    jar -cvmf manifest.txt app.jar com/ui

就是就这么简单,Main-Class 需要根据包名指定要 main 方法所在的类,打包的时候指定具体要打包的目录即可。

最终结果,生产了 app.jar 文件。

可以通过 jar -tf app.jar 命令查看一下生成的 jar 文件包含些什么。

可以看到包含了 ui 包下所有的 class文件。

这样,这个 app.jar 就可以当做一个小小的可执行程序,之后需要文件批量重命名的场景,java jar app.jar 就可以打开一个简单的 UI 视图进行操作了。

javac -d

除了使用 IDE 指定的默认路径之外,我们也可以使用 javac -d 在编译的时候指定自定义的路径,来规定 class 文件生成的路径。这个路径确定好之后,后续步骤和 对 IDE 生成的 class 的处理没有区别了。

总结

简单了解一下 jar 文件的生成,通过不带包名的实现入门,了解了对于包含包名的类(毕竟这才是常态)的处理。

开篇提到的对第三方 SDK 的二次加工就不展开了,简单来说就是生成同名的类,替换原来的 class 二次打包的过程。但是具体实施过程中,还需要很多细节需要考虑,比如原文件包含混淆时如何处理,要生成的同名类,如果包名类包含对其他类的依赖又该如何处理。

加个鸡腿呗.