敏捷冲冲_

The Art of software design

一文读懂 Java Jar 包

在将 Java 应用程序进行打包时,你的软件使用者一定希望你仅提供给他一个单独的可执行文件,而不是一个含有大量类文件的目录。而 Java 归档(JAR)文件就是为此目的而设计的,一个 JAR 文件既可以包含类文件,也可以包含诸如图像和声音这些其它类型的文件。此外,JAR 文件是压缩的,它使用了大家熟悉的 ZIP 压缩格式。

然而在实际的项目开发过程中,我们通常会使用 maven 等项目管理工具来自动地管理 Jar 包及相关的依赖,这也导致有些小伙伴对 Jar 文件的运行原理一知半解,今天就让我们以原生指令的方式去探索 Jar 包的奥秘,希望大家学习完本篇文章后,可以对 Jar 文件有全新的认识。

什么是 Jar 包

Jar 包就是 Java Archive File,顾名思义,它是 Java 的一种文档格式,用于将许多文件聚合到一个文件中。JAR 文件本质上是一个 ZIP 文件,其中包含了一个可选的名为 META-INF 的特殊目录。注意,网上一些文章中说 META-INF 目录是必选的,其实并不是这样的,这点需要注意一下。

可以通过命令行 jar 工具或在 Java 平台中使用 java.util.jar API 来创建 JAR 文件。对于 JAR 文件的名称没有限制,它可以是特定平台上的任何合法文件名。

在许多情况下,JAR 文件并不仅仅是 Java 类文件或资源文件的简单存档。它们经常会被用作于应用程序的扩展。如果 Jar 包中存在 META-INF 目录,还可以在其中存储 Jar 包的配置信息,包括安全性,版本控制,启动配置信息和 SPI 服务等信息,这个会在后面的展开内容中详细讲解。

Jar 大体上上可以分为两种:

<<<CUSTOM

  • 非可执行 JAR:作为依赖库在宿主项目中使用,但是不能通过 java -jar 命令直接执行。
  • 可执行 JAR:可以通过 java -jar 命令直接运行程序,也可作为依赖库使用。

<<<END

查看 Jar 文件信息

使用如下命令格式,可以查看一个 .jar 文件的内容结构:

1
jar -t[vf] filename

filename 代表我们要查看的 jar 文件的名称,比如查看 servlet 官方 jar 文件的信息:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
C:\Users\PF>jar -tf javax.servlet-api-4.0.1.jar
META-INF/
META-INF/MANIFEST.MF
javax/
javax/servlet/
javax/servlet/http/
javax/servlet/annotation/
javax/servlet/descriptor/
META-INF/LICENSE.txt
javax/servlet/ServletRequestEvent.class
javax/servlet/Registration.class
javax/servlet/LocalStrings.properties
javax/servlet/MultipartConfigElement.class
javax/servlet/SingleThreadModel.class
javax/servlet/AsyncContext.class
javax/servlet/FilterRegistration$Dynamic.class
javax/servlet/DispatcherType.class
javax/servlet/GenericFilter.class
javax/servlet/GenericServlet.class
javax/servlet/ServletOutputStream.class
javax/servlet/HttpMethodConstraintElement.class
javax/servlet/ServletInputStream.class
javax/servlet/FilterConfig.class
javax/servlet/ServletRequestAttributeEvent.class
javax/servlet/http/HttpServletResponseWrapper.class
javax/servlet/http/HttpServletRequestWrapper.class
javax/servlet/http/LocalStrings.properties
javax/servlet/http/NoBodyResponse.class
javax/servlet/http/HttpFilter.class
javax/servlet/http/WebConnection.class
javax/servlet/http/HttpServletMapping.class
javax/servlet/http/HttpSessionBindingListener.class
javax/servlet/http/HttpSessionContext.class
javax/servlet/http/package.html
javax/servlet/http/HttpServlet.class
javax/servlet/http/HttpServletRequest.class
javax/servlet/http/HttpServletRequest$1.class
javax/servlet/http/PushBuilder.class
javax/servlet/http/LocalStrings_es.properties
javax/servlet/http/Cookie.class
javax/servlet/http/LocalStrings_ja.properties
javax/servlet/http/Part.class
javax/servlet/http/HttpSessionListener.class
javax/servlet/http/HttpSessionBindingEvent.class
javax/servlet/http/HttpServletResponse.class
javax/servlet/http/HttpSessionEvent.class
javax/servlet/http/HttpUpgradeHandler.class
javax/servlet/http/HttpSessionIdListener.class
javax/servlet/http/HttpSessionActivationListener.class
javax/servlet/http/MappingMatch.class
javax/servlet/http/HttpUtils.class
javax/servlet/http/HttpSession.class
javax/servlet/http/LocalStrings_fr.properties
javax/servlet/http/HttpSessionAttributeListener.class
javax/servlet/http/NoBodyOutputStream.class
javax/servlet/Filter.class
javax/servlet/ServletConfig.class
javax/servlet/ServletContextEvent.class
javax/servlet/UnavailableException.class
javax/servlet/ServletRegistration$Dynamic.class
javax/servlet/package.html
javax/servlet/RequestDispatcher.class
javax/servlet/ServletRequestWrapper.class
javax/servlet/Servlet.class
javax/servlet/FilterChain.class
javax/servlet/annotation/MultipartConfig.class
javax/servlet/annotation/WebListener.class
javax/servlet/annotation/WebFilter.class
javax/servlet/annotation/HttpConstraint.class
javax/servlet/annotation/package.html
javax/servlet/annotation/WebInitParam.class
javax/servlet/annotation/ServletSecurity$EmptyRoleSemantic.class
javax/servlet/annotation/WebServlet.class
javax/servlet/annotation/ServletSecurity$TransportGuarantee.class
javax/servlet/annotation/HttpMethodConstraint.class
javax/servlet/annotation/ServletSecurity.class
javax/servlet/annotation/HandlesTypes.class
javax/servlet/ServletResponseWrapper.class
javax/servlet/ServletSecurityElement.class
javax/servlet/LocalStrings_ja.properties
javax/servlet/ServletContextListener.class
javax/servlet/ServletRequestAttributeListener.class
javax/servlet/ServletException.class
javax/servlet/AsyncListener.class
javax/servlet/descriptor/TaglibDescriptor.class
javax/servlet/descriptor/package.html
javax/servlet/descriptor/JspPropertyGroupDescriptor.class
javax/servlet/descriptor/JspConfigDescriptor.class
javax/servlet/ServletContext.class
javax/servlet/SessionCookieConfig.class
javax/servlet/ServletRequestListener.class
javax/servlet/ServletResponse.class
javax/servlet/SessionTrackingMode.class
javax/servlet/HttpConstraintElement.class
javax/servlet/FilterRegistration.class
javax/servlet/Registration$Dynamic.class
javax/servlet/WriteListener.class
javax/servlet/AsyncEvent.class
javax/servlet/ServletContextAttributeEvent.class
javax/servlet/ReadListener.class
javax/servlet/ServletContainerInitializer.class
javax/servlet/ServletRegistration.class
javax/servlet/LocalStrings_fr.properties
javax/servlet/ServletContextAttributeListener.class
javax/servlet/ServletRequest.class
META-INF/maven/
META-INF/maven/javax.servlet/
META-INF/maven/javax.servlet/javax.servlet-api/
META-INF/maven/javax.servlet/javax.servlet-api/pom.xml
META-INF/maven/javax.servlet/javax.servlet-api/pom.properties

可以看到,除了 .class 文件以外,jar 还能打包静态资源文件,比如 .xml、.properties、.html 等项目所需的所有文件。

MANIFEST.MF 文件

从上面的命令执行结果中可以看到,除了类文件、图像和其他资源外,每个 Jar 文件还包含了一个用于描述归档信息的清单文件 MANIFEST.MF

清单文件的名称固定为 MANIFEST.MF,它位于 Jar 文件的一个特殊 META-INF 子目录中。最小的符合标准的清单文件是很简单的:

1
Manifest-Version: 1.0

复杂的清单文件会包含更多的属性条目,这些清单条目被分成多个节。第一节被称为主节(main section),它作用于整个 JAR 文件。随后的条目用来指定已命名条目的属性,这些已命名的条目可以是某个文件、包或者 URL。它们都必须起始于名为 Name 的条目,节与节之间用空行分开。

一般属性

<<<CUSTOM

  • Manifest-Version:定义清单文件的版本。该值是合法的版本号,例如:Manifest-Version: 1.0。
  • Created-By:声明该文件的生成者,一般该属性是由 jar 命令行工具自动生成的,例如:Created-By: Maven Jar Plugin 3.2.0。
  • Signature-Version:定义 jar 文件的签名版本,该值应该是有效的版本号字符串。
  • Class-Path:此属性的值指定此应用程序所需依赖库的相对路径,如果有多个依赖库,路径之间用一个或多个空格分隔,此应用程序的类加载器会使用此属性的值,来构造依赖库的搜索路径。

这里重点介绍一下 Class-Path 属性,在我们开发 Jar 包应用的时候,往往还会依赖其它的 .jar 文件。这时,打包当前应用为 .jar 文件时,就需要考虑当前应用与第三方 .jar 文件的依赖关系。否则我们打包的 jar 就无法独立运行。而 Class-Path 属性就是用来解决依赖的 .jar 文件路径问题的。

例如:

<<<END

1
Class-Path: ./lib/mysql-connector-java-8.0.22.jar ./lib/util.jar

代表当前 jar 文件依赖同目录下的 lib/mysql-connector-java-8.0.22.jar 和 lib/util.jar 文件,如果这两个 jar 文件不存在,可能会导致 java.lang.ClassNotFoundException 异常。

注意:Class-Path 指定的是外部依赖文件路径,而不是 Jar 包内的路径。

应用程序相关属性

<<<CUSTOM

  • Main-Class:定义 jar 文件的入口类,该类必须是一个可执行的类,一旦定义了该属性即可通过 java -jar x.jar 来直接运行该 jar 文件。

<<<END

包扩展属性

<<<CUSTOM

  • Implementation-Title:定义了扩展实现的标题

  • Implementation-Version:定义扩展实现的版本

  • Implementation-Vendor:定义扩展实现的组织

  • Implementation-Vendor-Id:定义扩展实现的组织的标识

  • Implementation-URL:定义该扩展包的下载地址(URL)

  • Specification-Title:定义扩展规范的标题

  • Specification-Version:定义扩展规范的版本

  • Specification-Vendor:声明了维护该规范的组织

  • Sealed:定义jar文件是否封存,值可以是true或者false

<<<END

自定义属性

除了前面提到的一些属性外,你也可以在 MANIFEST.MF 中增加自己的属性以及响应的值:

1
2
3
4
5
Spring-Boot-Version: 2.4.0
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx

读取 MANIFEST.MF 信息

JDK 中提供了可以直接读取 MANIFEST.MF 文件信息的工具,可以通过 java.util.jar 这个类库来读取。下面的代码展示了如何读取 servlet 官方 jar 包信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws IOException {
JarFile jar = new JarFile(new File("C:\\Users\\Pf\\Downloads\\javax.servlet-api-4.0.1.jar"));
Manifest manifest = jar.getManifest();

Attributes mainAttributes = manifest.getMainAttributes();
for(Map.Entry<Object, Object> attrEntry : mainAttributes.entrySet()){
System.out.println("main\t"+attrEntry.getKey()+": "+attrEntry.getValue());
}

Map<String, Attributes> entries = manifest.getEntries();
for(Map.Entry<String, Attributes> entry : entries.entrySet()) {
Attributes values = entry.getValue();
for (Map.Entry<Object, Object> attrEntry : values.entrySet()) {
System.out.println(attrEntry.getKey() + ": " + attrEntry.getValue());
}
}
}

输出执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main	Manifest-Version: 1.0
main Bundle-Description: Java(TM) Servlet 4.0 API Design Specification
main Bundle-License: https://oss.oracle.com/licenses/CDDL+GPL-1.1
main Bundle-SymbolicName: javax.servlet-api
main Implementation-Version: 4.0.1
main Archiver-Version: Plexus Archiver
main Built-By: vinay
main Bundle-ManifestVersion: 2
main Bnd-LastModified: 1524208739354
main Specification-Vendor: Oracle Corporation
main Implementation-Vendor-Id: org.glassfish
main Bundle-DocURL: https://javaee.github.io
main Bundle-Vendor: GlassFish Community
main Import-Package: javax.servlet;version="4.0.0",javax.servlet.annotation;version="4.0.0",javax.servlet.descriptor;version="4.0.0",javax.servlet.http;version="4.0.0"
main Tool: Bnd-0.0.255
main Implementation-Vendor: GlassFish Community
main Export-Package: javax.servlet;uses:="javax.servlet.annotation,javax.servlet.descriptor";version="4.0.0",javax.servlet.annotation;uses:="javax.servlet";version="4.0.0",javax.servlet.http;uses:="javax.servlet";version="4.0.0",javax.servlet.descriptor;version="4.0.0"
main Bundle-Version: 4.0.0
main Bundle-Name: Java Servlet API
main Extension-Name: javax.servlet
main Created-By: 1.8.0_131 (Oracle Corporation)
main Build-Jdk: 1.8.0_131
main Specification-Version: 4.0

META-INF 目录

大多数 JAR 文件包含一个 META-INF 目录,它用于存储 JAR 包和扩展的配置数据,如安全性和版本信息。Java 2 平台(标准版【J2SE】)识别并解释 META-INF 目录中的下述文件和目录,以便配置应用程序、扩展和类装载器:

<<<CUSTOM

  • MANIFEST.MF:这个清单文件前面已经详细介绍过。
  • services 目录:此目录下存储所有 SPI 服务配置文件,SPI 全称为 Service Provider Interface,是 JDK 内置的一种服务提供发现机制。
  • INDEX.LIST:这个文件由 jar 工具的新选项 -i 自动生成。从1.3 开始,引入 JarIndex 来优化网络应用程序(尤其是小应用程序)的类加载器的类搜索过程。一旦类加载器在特定的 jar 文件中找到INDEX.LIST 文件,它就始终信任其中列出的信息。如果找到特定类的映射,但类加载器无法通过跟踪链接找到它,则抛出 InvalidJarIndexException。发生这种情况时,应用程序开发人员应在扩展名上重新运行 jar 工具,以将正确的信息获取到索引文件中。
  • .SF:这是 JAR 文件的签名文件
  • .DSA:与签名文件相关联的签名程序块文件,它存储了用于签名 JAR 文件的公共签名。
  • LICENSE:版权信息

<<<END

创建 Jar 文件

上面已经基本介绍完了 Jar 包的相关概念和文件组织结构信息,如果我们要手工打包一个 .jar 文件,可以使用 JDK 提供的 jar 命令行工具来完成。

具体用法如下:

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
39
40
41
42
43
44
45
46
主操作模式:

-c, --create 创建档案
-i, --generate-index=FILE 为指定的 jar 档案生成
索引信息
-t, --list 列出档案的目录
-u, --update 更新现有 jar 档案
-x, --extract 从档案中提取指定的 (或全部) 文件
-d, --describe-module 输出模块描述符或自动模块名称

在任意模式下有效的操作修饰符:

-C DIR 更改为指定的目录并包含
以下文件
-f, --file=FILE 档案文件名。省略时, 基于操作
使用 stdin 或 stdout
--release VERSION 将下面的所有文件都放在
jar 的版本化目录中 (即 META-INF/versions/VERSION/)
-v, --verbose 在标准输出中生成详细输出

在创建和更新模式下有效的操作修饰符:

-e, --main-class=CLASSNAME 捆绑到模块化或可执行
jar 档案的独立应用程序
的应用程序入口点
-m, --manifest=FILE 包含指定清单文件中的
清单信息
-M, --no-manifest 不为条目创建清单文件
--module-version=VERSION 创建模块化 jar 或更新
非模块化 jar 时的模块版本
--hash-modules=PATTERN 计算和记录模块的散列,
这些模块按指定模式匹配并直接或
间接依赖于所创建的模块化 jar 或
所更新的非模块化 jar
-p, --module-path 模块被依赖对象的位置, 用于生成
散列

只在创建, 更新和生成索引模式下有效的操作修饰符:

-0, --no-compress 仅存储; 不使用 ZIP 压缩

其他选项:

-?, -h, --help[:compat] 提供此帮助,也可以选择性地提供兼容性帮助
--help-extra 提供额外选项的帮助
--version 输出程序版本

我们先创建一个 Java 项目,编写一个非常简单的类 MainClass.java,整个项目结构如下图:

MainClass.java

1
2
3
4
5
6
7
package design.laravel;

public class MainClass {
public static void main(String[] args) {
System.out.println("hello laravel.design");
}
}

META-INF/MANIFEST.MF

1
2
Manifest-Version: 1.0
Main-Class: design.laravel.MainClass

切换至 src 目录下,执行打包命令:

1
2
3
4
5
jar -cvfm study.jar META-INF/MANIFEST.MF design/laravel/MainClass.class

输出:
已添加清单
正在添加: design/laravel/MainClass.class(输入 = 447) (输出 = 302)(压缩了 32%)

打包命令执行成功,会在 src 目录下生成一个 study.jar 文件,因为我们在 MANIFEST.MF 里面设置了 Mani-Class 属性,所以我们可以直接通过 java -jar 命令执行 jar 包:

1
2
3
4
java -jar study.jar

输出:
hello laravel.design

用 jar 命令查看我们打包好的 study.jar 文件结构:

1
2
3
4
5
6
jar -tf study.jar

输出:
META-INF/
META-INF/MANIFEST.MF
design/laravel/MainClass.class

有关于 jar 命令更多的高级用法,请查看官方帮助文档

参考文档

https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html

https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jar.html