JAR File Specification

介绍

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

模块化JAR文件

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

在模块路径上部署的模块化JAR文件(而不是类路径)是一个显式模块。依赖关系和服务提供者在模块描述符中声明。如果模块化JAR文件部署在类路径上,则其行为类似于非模块化JAR文件。

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

多版本JAR文件

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

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

Multi-Release: true

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

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

META-INF/versions/N

其中N是Java平台发布的主要版本号的字符串表示。具体来说,N必须符合以下规范:

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

任何值为小于9N的版本化目录将被忽略,以及不符合上述规范的N的字符串表示。

在多版本JAR的版本化目录下的类文件,假设版本为N,必须具有小于或等于与Java平台发布的第N个主要版本相关联的类文件版本的类文件版本。如果类文件的类是公共的或受保护的,则该类必须主持具有相同完全限定名称和访问修饰符的类的类文件,该类文件存在于顶级目录下。逻辑上,这也适用于版本小于N的版本化目录下的类文件的类。

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

多版本JAR文件中的类导出的公共API必须在各个版本中完全相同,因此至少需要为版本化目录下的类文件的版本化公共或受保护类主持顶级目录下的类文件的类。进行广泛的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文件的签名文件。'x'代表基本文件名。

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

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

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

名称-值对和部分

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

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

任何形式的二进制数据都表示为base64。对于导致行长度超过72个字节的二进制数据,需要进行延续。二进制数据的示例包括摘要和签名。

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

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

规范:

section: 部分:
nonempty-section: 非空部分:
newline: CR LF | LF | CR不跟随 LF
header: 名称
name: 字母数字 *头部字符
value: 空格 *其他字符 换行 *继续
continuation: 空格 *其他字符 换行
alphanum: {A-Z} | {a-z} | {0-9}
headerchar: 字母数字 | - | _
otherchar: NUL, CR LF 之外的任何 UTF-8 字符

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

JAR清单

概述

JAR文件清单由一个主部分和一个由单独的JAR文件条目部分组成的列表组成,每个部分之间用换行符分隔。主部分和单独的部分都遵循上面指定的部分语法。它们各自具有自己的特定限制和规则。

清单规范:

manifest-file: 主要部分 换行 *单独部分
main-section: 版本信息 换行 *主属性
version-info: Manifest-Version : 版本号
version-number: 数字+{.数字+}*
main-attribute: (任何合法的主属性) 换行
individual-section: Name : 换行 *每个条目属性
perentry-attribute: (任何合法的每个条目属性) 换行
newline: CR LF | LF | CR不跟随 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文件进行签名。如果使用 jarsigner 工具对JAR文件进行签名,则包括 META-INF 目录中的非签名相关文件在内的每个文件条目都将被签名。与签名相关的文件包括:

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

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

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

签名文件

每个签名者由扩展名为 .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. 对于清单中的每个条目,将清单文件中的摘要值与“Name:”属性中引用的实际数据上计算的摘要值进行比较,该属性指定相对文件路径或URL。如果任何摘要值不匹配,则JAR文件验证失败。

示例清单文件:

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

    Name: common/class1.class
    SHA-256-Digest: (SHA-256摘要的base64表示)

    Name: common/class2.class
    SHA1-Digest: (SHA1摘要的base64表示)
    SHA-256-Digest: (SHA-256摘要的base64表示)

相应的签名文件将是:

    Signature-Version: 1.0
    SHA-256-Digest-Manifest: (SHA-256摘要的base64表示)
    SHA-256-Digest-Manifest-Main-Attributes: (SHA-256摘要的base64表示)

    Name: common/class1.class
    SHA-256-Digest: (SHA-256摘要的base64表示)

    Name: common/class2.class
    SHA-256-Digest: (SHA-256摘要的base64表示)

Magic 属性

验证给定清单条目上的签名的另一个要求是验证程序理解该条目的清单条目中的 Magic 键值的值或值。

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

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

以下是清单文件中 Magic 属性潜在用途的两个示例:

        Name: http://www.example-scripts.com/index#script1
        SHA-256-Digest: (SHA-256哈希的base64表示)
        Magic: JavaScript, Dynamic

        Name: http://www.example-tourist.com/guide.html
        SHA-256-Digest: (SHA-256哈希的base64表示)
        SHA-256-Digest-French: (SHA-256哈希的base64表示)
        SHA-256-Digest-German: (SHA-256哈希的base64表示)
        Magic: Multilingual

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

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

数字签名

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

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

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

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

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

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

文件名扩展名可以是1到3个字母数字字符。未识别的扩展名将被忽略。

关于清单和签名文件的注意事项

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

Class-Path属性

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

应用程序(或更一般地说,JAR文件)通过清单属性Class-Path指定其所需库的相对URL。该属性列出了要搜索其他库的实现的URL,如果它们在主机Java虚拟机上找不到。这些相对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文件和包可以选择性地进行封装,以便包可以在一个版本内强制执行一致性。

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

封装的JAR指定该JAR定义的所有包都是封装的,除非为特定包明确覆盖。

通过清单属性Sealed指定封装的包,其值为truefalse(大小写不敏感)。例如,

    Name: javax/servlet/internal/
    Sealed: true

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

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

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

    Sealed: true

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

如果缺少此属性,则假定JAR文件未封装,以保持向后兼容性。然后系统默认检查包头以获取封装信息。

包封装对于安全性也很重要,因为它限制对仅限于同一JAR文件中源自的包中定义的成员的访问。

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

API详情

java.util.jar

另请参阅

java.security
java.util.zip