JAR 文件规范

介绍

JAR 文件是一种基于流行的 ZIP 文件格式的文件格式,用于将多个文件聚合为一个文件。 JAR 文件本质上是一个包含可选 META-INF 目录的 zip 文件。可以通过命令行 jar 工具或使用 Java 平台中的java.util.jar API 创建 JAR 文件。 JAR 文件的名称没有限制,可以是特定平台上的任何合法文件名。

模块化 JAR 文件

模块化 JAR 文件是在顶级目录(或根)目录中具有模块描述符 module-info.class 的 JAR 文件。模块描述符是模块声明的二进制形式。 (注意 多版本 JAR 文件 部分进一步细化了模块化 JAR 文件的定义。)

部署在模块路径(而不是类路径)上的模块化 JAR 文件是一个 explicit 模块。依赖项和服务提供者在模块描述符中声明。如果模块化 JAR 文件部署在类路径上,那么它的行为就好像非模块化 JAR 文件一样。

部署在模块路径上的非模块化 JAR 文件是 automatic module 。如果 JAR 文件有一个主属性 Automatic-Module-Name(请参阅 主要属性 ),那么该属性的值就是模块名称,否则模块名称是从 ModuleFinder.of(Path...) 中指定的 JAR 文件的名称派生的。

多版本 JAR 文件

多版本 JAR 文件允许单个 JAR 文件支持 Java 平台版本的多个主要版本。例如,一个多版本 JAR 文件可以同时依赖于 Java 8 和 Java 9 主要平台版本,其中一些类文件依赖于 Java 8 中的 API,而其他类文件依赖于 Java 9 中的 API。这使库和框架开发人员能够将 Java 平台版本的特定主要版本中 API 的使用与所有用户迁移到该主要版本的要求分离开来。库和框架开发人员可以逐步迁移到并支持新的 Java 功能,同时仍然支持旧功能。

多版本 JAR 文件由 main 属性标识:

Multi-Release: true 

JAR清单 的主要部分中声明。

依赖于 Java 平台版本 9 或更高版本的类和资源文件可能位于 versioned directory 而不是顶级(或根)目录下。版本化目录位于 META-INF 目录 下,格式如下:

META-INF/versions/N 

其中 N 是 Java 平台版本的主要版本号的字符串表示形式。具体N必须符合规范:

N: {1-9} {0-9} *

N 的值小于 9 的任何版本化目录都将被忽略,因为不符合上述规范的 N 的字符串表示形式也是如此。

多版本 JAR 中版本控制目录下的类文件,版本为 N 的类文件版本必须小于或等于与 Java 平台版本的第 N 主要版本关联的类文件版本。如果类文件的类是公共的或受保护的,则该类必须preside over具有相同完全限定名和访问修饰符的类,其类文件位于顶级目录下。通过逻辑扩展,这适用于版本低于 N 的版本化目录下的类文件的类(如果存在)。

如果多版本 JAR 文件部署在 Java 平台发布运行时的主要版本N 的类路径或模块路径(作为自动模块或显式多版本模块),则从该 JAR 文件加载类的类加载器将首先搜索对于 N 版本目录下的类文件,然后按降序排列的先前版本目录(如果存在),向下到 9 的较低主要版本界限,最后在顶级目录下。

由多版本 JAR 文件中的类导出的公共 API 在各个版本之间必须是exactly 相同的,因此至少为什么版本化目录下类文件的版本化公共类或受保护类必须主持顶级下类文件的类 -级目录。执行广泛的 API 验证检查既困难又昂贵,因为不需要 jar 工具等工具来执行广泛的验证,也不需要 Java 运行时来执行任何验证。此规范的未来版本可能会放宽完全相同的 API 约束以支持谨慎的演变。

META-INF 目录下的资源无法进行版本控制(例如服务配置)。

可以对多版本 JAR 文件进行签名。

Java 运行时的引导类加载器不支持多版本 JAR 文件。如果将多版本 JAR 文件附加到引导类路径(使用 -Xbootclasspath/a 选项),则将 JAR 视为普通 JAR 文件。

模块化多版本 JAR 文件

模块化多版本 JAR 文件是具有模块描述符 module-info.class 的多版本 JAR 文件,位于顶层目录(对于 模块化的 JAR 文件),或直接位于版本化目录中。

非导出包(未在模块描述符中声明为导出)中的公共类或受保护类不需要主持具有相同完全限定名称和访问修饰符的类,其类文件存在于顶级目录下。

模块描述符通常与任何其他类或资源文件没有区别。模块描述符可能出现在版本化区域下,但不出现在顶级目录下。这确保了,例如,只有 Java 8 版本的类可以出现在顶级目录下,而 Java 9 版本的类(包括,或者可能只包括模块描述符)可以出现在 9 版本目录下。

任何控制版本较低的模块描述符或顶层模块描述符的版本化模块描述符,例如 M 必须与 M 相同,但有两个例外:

  1. 主要版本描述符可以有不同的非transitive requires java.*jdk.* 模块的子句;和
  2. 主持版本描述符可以有不同的uses子句,即使是在java.*jdk.*模块之外定义的服务类型。

诸如 jar 工具之类的工具应该对版本化模块描述符执行此类验证,但不需要 Java 运行时来执行任何验证。

META-INF 目录

META-INF 目录中的以下文件/目录被 Java 平台识别和解释以配置应用程序、类加载器和服务:

用于定义包相关数据的清单文件。

此文件由 jar 工具的新“-i" 选项生成,它包含应用程序中定义的包的位置信息。它是 JarIndex 实现的一部分,由类加载器用来加速它们的类加载过程。

JAR 文件的签名文件。 'x' 代表基本文件名。

与具有相同基本文件名的签名文件关联的签名块文件。该文件以 PKCS #7 结构存储相应签名文件的数字签名。

该目录存储了部署在类路径上的 JAR 文件或作为自动模块部署在模块路径上的 JAR 文件的所有服务提供者配置文件。有关详细信息,请参阅 服务商发展 的规范。

此目录下包含 多版本 JAR 文件的版本化类和资源文件。

名称-值对和部分

在我们详细了解各个配置文件的内容之前,需要定义一些格式约定。在大多数情况下,清单文件和签名文件中包含的信息表示为受 RFC822 标准启发的所谓“名称:值”对。我们也称这些对为标题或属性。

名称-值对组称为“部分”。节与其他节之间用空行分隔。

任何形式的二进制数据都表示为 base64。导致行长度超过 72 字节的二进制数据需要继续。二进制数据的示例是摘要和签名。

实现应支持最多 65535 字节的标头值。

本文档中的所有规范使用相同的语法,其中终端符号以固定宽度字体显示,非终端符号以斜体显示。

规格:

section: *header +newline
nonempty-section: +header +newline
newline: CR LF | LF | CR (not followed by LF )
header: name : value
name: alphanum *headerchar
value: 空间 *otherchar newline *continuation
continuation: 空间*otherchar newline
alphanum: {A-Z} | {a-z } | {0-9 }
headerchar: alphanum | - | _
otherchar: any UTF-8 character except NUL, CR and LF

上述规范中定义的非终结符号将在以下规范中引用。

JAR清单

概述

JAR 文件清单包含一个主要部分,后面是各个 JAR 文件条目的部分列表,每个部分由换行符分隔。主要部分和各个部分都遵循上面指定的部分语法。他们每个人都有自己特定的限制和规则。

清单规格:

manifest-file: main-section newline *individual-section
main-section: version-info newline *main-attribute
version-info: Manifest-Version : version-number
version-number: digit+{ . digit+}*
main-attribute: (any legitimate main attribute) newline
individual-section: Name : value newline *perentry-attribute
perentry-attribute: (any legitimate perentry attribute) newline
newline: CR LF | LF | CR (not followed by LF )
digit: {0-9}

在上面的规范中,可以出现在主要部分的属性被称为主要属性,而可以出现在单独部分的属性被称为每条目属性。某些属性可以同时出现在主要部分和各个部分中,在这种情况下,每个条目的属性值会重写指定条目的主要属性值。两种类型的属性定义如下。

主要属性

主要属性是清单的主要部分中存在的属性。他们分为以下不同的组:

每个条目的属性

每个条目属性仅适用于与清单条目相关联的单个 JAR 文件条目。如果相同的属性也出现在主要部分,则每个条目属性的值将重写主要属性的值。例如,如果 JAR 文件 a.jar 具有以下清单内容:

  Manifest-Version: 1.0
  Created-By: 1.8 (Oracle Inc.)
  Sealed: true
  Name: foo/bar/
  Sealed: false 

这意味着归档在 a.jar 中的所有包都是密封的,除了包 foo.bar 不是。

每个条目的属性分为以下几组:

签名的 JAR 文件

概述

可以使用命令行 jarsigner 工具或直接通过 java.security API 对 JAR 文件进行签名。如果 JAR 文件由 jarsigner 工具签名,则每个文件条目(包括 META-INF 目录中的非签名相关文件)都将被签名。签名相关文件为:

请注意,如果此类文件位于 META-INF 子目录中,则它们不被视为与签名相关。这些文件名的不区分大小写的版本是保留的,也不会被签名。

可以使用 java.security API 对 JAR 文件的子集进行签名。已签名的 JAR 文件与原始 JAR 文件完全相同,只是其清单已更新并且在 META-INF 目录中添加了两个附加文件:签名文件和签名块文件。当不使用 jarsigner 时,签名程序必须同时构造签名文件和签名块文件。

对于在已签名 JAR 文件中签名的每个文件条目,只要它不存在于清单中,就会为其创建一个单独的清单条目。每个清单条目都列出一个或多个摘要属性和一个可选的 魔法属性

签名文件

每个签名者都由扩展名为 .SF 的签名文件表示。该文件的主要部分类似于清单文件。它由一个主要部分组成,其中包括签名者提供的信息,但不特定于任何特定的 jar 文件条目。除了 Signature-VersionCreated-By 属性(参见 主要属性 ),主要部分还可以包括以下安全属性:

主要部分之后是单个条目的列表,这些条目的名称也必须出现在清单文件中。每个单独的条目必须至少包含清单文件中相应条目的摘要。

出现在清单文件中但未出现在签名文件中的路径或 URL 不用于计算。

签名验证

如果签名有效,则 JAR 文件验证成功,并且生成签名时 JAR 文件中的所有文件自那时起都没有更改。 JAR 文件验证涉及以下步骤:

  1. 首次解析清单时,通过签名文件验证签名。为了效率,这个验证可以被记住。请注意,此验证仅验证签名说明本身,而不是实际的存档文件。

  2. 如果签名文件中存在 x-Digest-Manifest 属性,请根据对整个清单计算的摘要验证该值。如果签名文件中存在多个x-Digest-Manifest属性,请验证其中至少一个与计算出的摘要值匹配。

  3. 如果签名文件中不存在 x-Digest-Manifest 属性,或者上一步计算的摘要值都不匹配,则执行优化程度较低的验证:

    1. 如果签名文件中存在 x-Digest-Manifest-Main-Attributes 条目,请根据清单文件中主要属性计算出的摘要验证该值。如果此计算失败,则 JAR 文件验证失败。这个决定可以被记住以提高效率。如果签名文件中不存在 x-Digest-Manifest-Main-Attributes 条目,则其不存在不会影响 JAR 文件验证,并且不会验证清单主要属性。

    2. 根据根据清单文件中的相应条目计算的摘要值,验证签名文件中每个源文件信息部分中的摘要值。如果任何摘要值不匹配,则 JAR 文件验证失败。

    存储在 x-Digest-Manifest 属性中的清单文件的摘要值可能不等于当前清单文件的摘要值的原因之一是它可能包含文件签名后新添加文件的部分。例如,假设在生成签名(以及签名文件)之后,一个或多个文件被添加到 JAR 文件中(使用 jar 工具)。如果 JAR 文件由不同的签名者再次签名,则清单文件会更改(jarsigner 工具会为新文件添加部分)并创建一个新的签名文件,但原始签名文件不会改变。如果生成签名时 JAR 文件中的所有文件自那时起都没有更改,则对原始签名的验证仍然被认为是成功的,如果签名文件的非标头部分中的摘要值就是这种情况等于清单文件中相应部分的摘要值。

  4. 对于清单中的每个条目,根据“名称:”属性中引用的实际数据计算的摘要验证清单文件中的摘要值,该属性指定相对文件路径或 URL。如果任何摘要值不匹配,则 JAR 文件验证失败。

示例清单文件:

  Manifest-Version: 1.0
  Created-By: 1.8.0 (Oracle Inc.)

  Name: common/class1.class
  SHA-256-Digest: (base64 representation of SHA-256 digest)

  Name: common/class2.class
  SHA1-Digest: (base64 representation of SHA1 digest)
  SHA-256-Digest: (base64 representation of SHA-256 digest) 

对应的签名文件为:

  Signature-Version: 1.0
  SHA-256-Digest-Manifest: (base64 representation of SHA-256 digest)
  SHA-256-Digest-Manifest-Main-Attributes: (base64 representation of SHA-256 digest)

  Name: common/class1.class
  SHA-256-Digest: (base64 representation of SHA-256 digest)

  Name: common/class2.class
  SHA-256-Digest: (base64 representation of SHA-256 digest) 

魔法属性

验证给定清单条目上的签名的另一个要求是验证者了解该条目的清单条目中的 Magic 密钥对值的一个或多个值。

Magic 属性是可选的,但要求解析器在验证条目的签名时了解条目的 Magic 键的值。

Magic 属性的一个或多个值是一组以逗号分隔的特定于上下文的字符串。逗号前后的空格将被忽略。忽略大小写。魔法属性的确切含义是特定于应用程序的。这些值指示如何计算清单条目中包含的哈希值,因此对于正确验证签名至关重要。关键字可用于动态或嵌入内容,多语言文档的多个哈希等。

以下是清单文件中可能使用 Magic 属性的两个示例:

    Name: http://www.example-scripts.com/index#script1
    SHA-256-Digest: (base64 representation of SHA-256 hash)
    Magic: JavaScript, Dynamic

    Name: http://www.example-tourist.com/guide.html
    SHA-256-Digest: (base64 representation of SHA-256 hash)
    SHA-256-Digest-French: (base64 representation of SHA-256 hash)
    SHA-256-Digest-German: (base64 representation of SHA-256 hash)
    Magic: Multilingual 

在第一个示例中,这些 Magic 值可能表明 http 查询的结果是嵌入在文档中的脚本,而不是文档本身,并且脚本是动态生成的。这两条信息指示如何计算哈希值以与清单的摘要值进行比较,从而比较有效的签名。

在第二个示例中,Magic 值表示检索到的文档可能已经针对特定语言进行了内容协商,并且要验证的摘要取决于检索到的文档所用的语言。

数字签名

数字签名是 .SF 签名文件的签名版本。这些是不打算由人类解释的二进制文件。

数字签名文件与 .SF 文件具有相同的文件名,但扩展名不同。扩展名因签名者私钥的算法而异。

上面未列出的签名算法的数字签名文件必须位于 META-INF 目录中并具有前缀“SIG-”。对应的签名文件(.SF文件)也必须有相同的前缀。

对于那些不支持外部签名数据的格式,文件应包含 .SF 文件的签名副本。因此,一些数据可能会重复,验证者应该比较这两个文件。

支持外部数据的格式要么引用 .SF 文件,要么使用隐式引用对其执行计算。

每个 .SF 文件可能有多个数字签名,但这些签名应由同一法律实体生成。

文件扩展名可以是 1 到 3 个 alphanum 个字符。无法识别的扩展名将被忽略。

关于清单和签名文件的说明

以下是适用于清单和签名文件的附加限制和规则的列表。

JAR索引

概述

从1.3开始,引入了JarIndex来优化网络应用类加载器的类搜索过程,尤其是小程序。最初,applet 类加载器使用简单的线性搜索算法在其内部搜索路径上搜索每个元素,该搜索路径由“ARCHIVE”标记或“Class-Path”主属性构建。类加载器下载并打开其搜索路径中的每个元素,直到找到类或资源。如果类加载器试图找到不存在的资源,则必须下载应用程序或小程序中的所有 jar 文件。对于大型网络应用程序和小程序,这可能会导致启动缓慢、响应迟缓和网络带宽浪费。 JarIndex 机制收集一个applet 中定义的所有jar 文件的内容,并将信息存储在applet 类路径上第一个jar 文件的索引文件中。第一个jar文件下载完成后,applet类加载器将使用收集到的内容信息进行jar文件的高效下载。

现有的 jar 工具得到了增强,能够检查 jar 文件列表并生成关于哪些类和资源驻留在哪个 jar 文件中的目录信息。此目录信息存储在根 jar 文件的 META-INF 目录中名为 INDEX.LIST 的简单文本文件中。当类加载器加载根 jar 文件时,它会读取 INDEX.LIST 文件并使用它来构建从文件和包名称到 jar 文件名列表的映射哈希表。为了找到一个类或资源,类加载器查询哈希表以找到合适的 jar 文件,然后在必要时下载它。

一旦类加载器在特定的 jar 文件中找到了一个 INDEX.LIST 文件,它就会始终信任其中列出的信息。如果找到特定类的映射,但类加载器无法通过链接找到它,则会抛出未指定的错误或 RuntimeException。发生这种情况时,应用程序开发人员应该在扩展上重新运行 jar 工具,以便将正确的信息放入索引文件中。

为了防止给应用程序增加过多的空间开销并加快内存中哈希表的构建,INDEX.LIST 文件保持尽可能小。对于具有非空包名称的类,映射记录在包级别。通常一个包名映射到一个 jar 文件,但是如果一个特定的包跨越多个 jar 文件,那么这个包的映射值将是一个 jar 文件列表。对于非空目录前缀的资源文件,映射也记录在目录级别。只有包名为空的类和位于根目录中的资源文件才会在单个文件级别记录映射。

索引文件规格

INDEX.LIST 文件包含一个或多个部分,每个部分由一个空行分隔。每个部分都定义了一个特定 jar 文件的内容,其中一个标题定义了 jar 文件路径名,后跟包或文件名列表,每行一个。所有 jar 文件路径都相对于根 jar 文件的代码库。这些路径名的解析方式与当前扩展机制对捆绑扩展的解析方式相同。

UTF-8 编码用于支持索引文件中文件或包名称中的非 ASCII 字符。

规格

index file: version-info blankline section*
version-info: JarIndex-Version: version-number
version-number: digit+{.digit+}*
section: body blankline
body: header name*
header: char+ .jar newline
name: char+ newline
char: any valid Unicode character except NULL, CR and LF
blankline: newline newline
newline: CR LF | LF | CR (not followed by LF )
digit: {0-9 }

INDEX.LIST 文件是通过运行 jar -i. 生成的。有关详细信息,请参阅 jar 手册页。

向后兼容性

新的类加载方案完全向后兼容在当前扩展机制之上开发的应用程序。当类加载器加载第一个 jar 文件并在 META-INF 目录中找到一个 INDEX.LIST 文件时,它将构建索引哈希表并使用新的扩展加载方案。否则,类加载器将简单地使用原始的线性搜索算法。

类路径属性

应用程序的清单可以指定一个或多个引用 JAR 文件的相对 URL 和它需要的其他库的目录。这些相对 URL 将相对于包含应用程序从中加载的代码库(“context JAR”)进行处理。

应用程序(或者更一般地,JAR 文件)通过清单属性 Class-Path 指定它需要的库的相对 URL。如果在主机 Java 虚拟机上找不到其他库的实现,此属性会列出用于搜索其他库实现的 URL。这些相对 URL 可能包括应用程序所需的任何库或资源的 JAR 文件和目录。不以“/”结尾的相对 URL 被假定为引用 JAR 文件。例如,

Class-Path: servlet.jar infobus.jar acme/beans.jar images/ 

在 JAR 文件的清单中最多可以指定一个 Class-Path 标头。

如果满足以下条件,则 Class-Path 条目有效:

无效条目将被忽略。有效条目根据上下文 JAR 解析。如果生成的 URL 无效或引用无法找到的资源,则会将其忽略。重复的 URL 将被忽略。

生成的 URL 被插入到类路径中,紧跟在上下文 JAR 的 URL 之后。例如,给定以下类路径:

a.jar b.jar 

如果 b.jar 包含以下 Class-Path 清单属性:

Class-Path: lib/x.jar a.jar 

那么这样一个URLClassLoader实例的有效搜索路径将是:

a.jar b.jar lib/x.jar 

当然,如果 x.jar 有自己的依赖项,那么这些依赖项将根据相同的规则添加,对于每个后续 URL 依此类推。在实际实现中,JAR 文件依赖项被延迟处理,以便 JAR 文件在需要时才真正打开。

包装密封

JAR 文件和包可以是可选的 sealed ,这样包就可以在一个版本中强制执行一致性。

密封在 JAR 中的包指定该包中定义的所有类必须来自同一个 JAR。否则,将抛出 SecurityException

密封的 JAR 指定由该 JAR 定义的所有包都是密封的,除非专门为包重写。

密封包通过清单属性 Sealed 指定,其值为 truefalse(不区分大小写)。例如,

  Name: javax/servlet/internal/
  Sealed: true 

指定 javax.servlet.internal 包是密封的,并且该包中的所有类都必须从同一个 JAR 文件加载。

如果缺少此属性,则包密封属性是包含 JAR 文件的密封属性。

密封的 JAR 通过相同的清单标头 Sealed 指定,其值再次为 truefalse 。例如,

  Sealed: true 

指定此存档中的所有包都是密封的,除非在清单条目中使用 Sealed 属性明确覆盖特定包。

如果缺少此属性,则假定 JAR 文件not 是密封的,以实现向后兼容性。然后系统默认检查包头以获取密封信息。

包密封对于安全性也很重要,因为它将对受包保护的成员的访问限制为仅限于包中定义的那些源自同一 JAR 文件的类。

未命名的包是不可密封的,因此要密封的类必须放在它们自己的包中。

API 详情

java.util.jar

参见

java.security
java.util.zip