Version: Java SE 18
目录
JAR文件是基于流行的ZIP文件格式的文件格式,用于将多个文件聚合为一个文件。JAR文件本质上是一个zip文件,包含一个可选的META-INF目录。JAR文件可以通过命令行JAR工具创建,也可以使用Java平台中的Java .util. JAR API创建。JAR文件的名称没有限制,可以是特定平台上的任何合法文件名。
模块化JAR文件是一个在顶级目录(或根目录)中具有模块描述符module-info.class的JAR文件。
模块描述符是模块声明的二进制形式。
(请注意,关于多版本JAR文件的部分进一步细化了模块化JAR文件的定义。)
部署在模块路径(与类路径相反)上的模块化JAR文件是显式模块。
依赖项和服务提供者在模块描述符中声明。
如果模块化的JAR文件部署在类路径上,那么它的行为就像一个非模块化的JAR文件。
部署在模块路径上的非模块JAR文件是一个自动模块。如果JAR文件有一个主属性Automatic-Module-Name(参见main Attributes),那么该属性的值就是模块名,否则模块名来自于 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 Manifest的主要部分中声明。
依赖于主要版本(9或更高版本)的Java平台版本的类和资源文件可能位于版本控制目录下,而不是顶层(或根)目录下。版本目录位于META-INF目录下,其形式如下:
META-INF/versions/N
其中N是Java平台版本的主要版本号的字符串表示。具体来说N必须符合规范:
N: | {1-9} {0-9} * |
任何N值小于9的版本目录将被忽略,因为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文件,有一个模块描述符 module-info.class ,在顶层目录中(就像一个模块化JAR文件一样),或者直接在一个版本目录中。
非导出包中的公共类或受保护类(未在模块描述符中声明为导出)不需要管理具有相同完全限定名称和访问修饰符的类,该类文件存在于顶级目录下。
模块描述符通常与任何其他类或资源文件没有区别对待。模块描述符可能出现在版本控制区域下,但不会出现在顶级目录下。例如,这确保了只有Java 8版本的类可以出现在顶层目录下,而Java 9版本的类(包括,或者可能只包括,模块描述符)可以出现在9版本目录下。
任何版本化的模块描述符,在低版本化的模块描述符或在顶层的模块描述符上,比如M,必须与M相同,有两个例外:
工具,比如jar工具,应该对版本化的模块描述符执行这样的验证,但是Java运行时不需要执行任何验证。
Java平台识别并解释META-INF目录下的下列文件/目录,以配置应用程序、类加载器和服务:
MANIFEST.MF
用于定义包相关数据的manifest文件。
INDEX.LIST
这个文件是由jar工具的新 -i 选项生成的,它包含应用程序中定义的包的位置信息。
它是JarIndex实现的一部分,类装入器使用它来加快类装入过程。
x.SF
JAR文件的签名文件。'x'表示基本文件名。
x.DSA
, x.RSA
, or x.EC
与具有相同基本文件名的签名文件相关联的签名块文件。该文件将相应签名文件的数字签名存储在PKCS #7结构中。
services/
这个目录存储部署在类路径上的JAR文件或作为自动模块部署在模块路径上的JAR文件的所有服务提供者配置文件。有关更多细节,请阅读服务提供者开发规范。
versions/
这个目录下面包含多版本JAR文件的版本化类和资源文件。
在详细讨论各个配置文件的内容之前,需要定义一些格式约定。在大多数情况下,清单文件和签名文件中包含的信息表示为受RFC822标准启发的所谓的 "name: value" 对。我们也称这些成对的为标题或属性。
一组name-value 对被称为“section”。各section之间用空行分隔。
任何形式的二进制数据都表示为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: | SPACE *otherchar newline *continuation |
continuation: | SPACE *otherchar newline |
alphanum: | {A-Z } | {a-z } | {0-9 } |
headerchar: | alphanum | - | _ |
otherchar: | any UTF-8 character except NUL, CR and LF |
在上述规范中定义的非终端符号将在以下规范中引用。
一个JAR文件清单由一个主要部分和一个单独JAR文件条目的部分列表组成,每个部分由换行符分隔。主节和各个节都遵循上面指定的节语法。它们都有自己特定的限制和规则。
主部分包含关于JAR文件本身以及应用程序的安全性和配置信息。它还定义了应用于每个单独清单条目的主要属性。本节中任何属性的名称都不能等于“name”。本节以空行结束。
单独部分定义了这个JAR文件中包含的包或文件的各种属性。JAR文件中的所有文件都不需要作为条目列在清单中,但是必须列出要签名的所有文件。清单文件本身不能被列出。每个部分必须以名称为“name”的属性开始,并且值必须是文件的相对路径,或者是引用存档之外数据的绝对URL。
如果同一个文件条目有多个单独的部分,则合并这些部分中的属性。如果某个属性在不同的部分有不同的值,那么最后一个属性将被识别。
不被理解的属性会被忽略。这些属性可能包括应用程序使用的特定于实现的信息。
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} |
在上面的规范中,可以出现在主要部分的属性被称为主要属性,而可以出现在单独部分的属性被称为每个条目属性。某些属性可以同时出现在主节和各个节中,在这种情况下,每个条目的属性值将覆盖指定条目的主属性值。这两种属性的定义如下。
Main attributes是出现在清单的主要部分中的属性。它们分为以下不同的组:
agentmain
。额外的属性(如 Can-Retransform-Classes
) 可用于指示代理所需的功能。每个条目属性只应用于与清单条目相关联的单个JAR文件条目。如果相同的属性也出现在主部分中,那么per-entry属性的值将覆盖主属性的值。例如,JAR文件a.jar包含如下清单内容:
Manifest-Version: 1.0
Created-By: 1.8 (Oracle Inc.)
Sealed: true
Name: foo/bar/
Sealed: false
这意味着归档在a.jar中的所有包都是封闭的,除了包foo.bar不是。
每个条目的属性分为以下几组:
可以使用命令行 jarsigner 工具或直接通过 java.security 对JAR文件进行签名。
如果JAR文件是由 jarsigner工具签名的,那么每个文件条目,包括META-INF目录中没有签名的相关文件,都将被签名。相关签名文件如下:
META-INF/MANIFEST.MF
META-INF/*.SF
META-INF/*.DSA
META-INF/*.RSA
META-INF/*.EC
META-INF/SIG-*
注意,如果这样的文件位于META-INF子目录中,它们就不被认为与签名相关。这些文件名的大小写不敏感版本将被保留,也不会被签名。
可以使用 java.security 对JAR文件的子集进行签名。签名的JAR文件与原始JAR文件完全相同,不同之处在于它的清单被更新,另外两个文件被添加到META-INF目录:签名文件和签名块文件。当不使用jarsigner时,签名程序必须构造签名文件和签名块文件。
对于签名JAR文件中签名的每个文件条目,只要它在清单中不存在,就会为它创建一个单独的清单条目。每个清单条目列出一个或多个摘要属性和一个可选的Magic属性。
每个签名者由一个扩展名为 .SF
. 的签名文件表示。该文件的主要部分类似于清单文件。
它由一个主要部分组成,其中包括由签名者提供的信息,但不特定于任何特定的jar文件条目。
除了Signature-Version和Created-By属性(参见主属性),主部分还可以包括以下安全属性:
java.security.MessageDigest
算法的标准名称): 此属性的值是清单的主要属性的摘要值。java.security.MessageDigest
算法的标准名称): 此属性的值是整个清单的摘要值。主部分后面是一个单独条目的列表,这些条目的名称也必须出现在清单文件中。
每个单独的条目必须至少包含清单文件中相应条目的摘要。
在manifest文件中出现但在签名文件中没有出现的路径或url不会在计算中使用。
如果签名有效,并且在生成签名时JAR文件中的文件从那时起没有任何更改,那么JAR文件验证就成功了。验证JAR文件的步骤如下:
在第一次解析清单时,验证签名文件上的签名。为了提高效率,可以记住这种验证。注意,这种验证只验证签名方向本身,而不是实际的存档文件。
如果签名文件中存在 x-Digest-Manifest
属性,则根据对整个清单计算的摘要验证该值。如果签名文件中存在多个 x-Digest-Manifest
属性,请验证其中至少有一个与计算的摘要值匹配。
如果 x-Digest-Manifest
属性在签名文件中不存在,或者上一步计算的digest值不匹配,则执行一个优化程度较低的验证:
如果签名文件中存在 x-Digest-Manifest-Main-Attributes
条目,则根据manifest文件中主要属性计算的摘要验证该值。如果这个计算失败,那么JAR文件验证就会失败。这个决定会因为效率而被记住。如果 x-Digest-Manifest-Main-Attributes
条目在签名文件中不存在,它的不存在不会影响JAR文件验证,manifest主属性也不会被验证。
根据根据清单文件中的相应条目计算的摘要值验证签名文件中每个源文件信息部分中的摘要值。如果任何摘要值不匹配,那么JAR文件验证就会失败。
存储在 x-Digest-Manifest
属性中的清单文件的摘要值可能不等于当前清单文件的摘要值的一个原因是,它可能包含文件签名后新添加的文件的部分。
例如,假设一个或多个文件(使用JAR工具)被添加到JAR文件中(因此生成了签名文件)。
如果JAR文件由另一个签名者再次签名,那么manifest文件将被更改(jarsigner工具为新文件向其添加部分),并创建一个新的签名文件,但原始签名文件不变。
如果在生成签名时JAR文件中的文件从那时起没有任何更改,那么对原始签名的验证仍然被认为是成功的;
如果签名文件的非头部分中的摘要值等于manifest文件中相应部分的摘要值,就会出现这种情况。
对于清单中的每个条目,根据根据 "Name:" 属性中引用的实际数据计算的摘要来验证清单文件中的摘要值,该属性指定了一个相对文件路径或URL。如果任何摘要值不匹配,那么JAR文件验证就会失败。
示例 manifest 文件:
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属性的值是一组以逗号分隔的上下文特定字符串。逗号前后的空格会被忽略。实例是被忽略的。魔术属性的确切含义是特定于应用程序的。这些值指示如何计算清单项中包含的散列值,因此对于正确验证签名至关重要。关键字可以用于动态或嵌入内容,多语言文档的多个哈希值,等等。
下面是在manifest文件中使用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文件相同的文件名,但扩展名不同。扩展取决于签名者私钥的算法。
.RSA
((PKCS7签名,用于RSA或RSA - pss密钥).DSA
(PKCS7签名,用于DSA密钥).EC
(PKCS7签名,用于EC或EdDSA密钥)上面没有列出的签名算法的数字签名文件必须位于META-INF目录中,并且具有“SIG-”前缀。对应的签名文件(.SF文件)也必须有相同的前缀。
对于那些不支持外部签名数据的格式,该文件应由. sf文件的签名副本组成。因此,有些数据可能会被复制,验证者应该比较这两个文件。
支持外部数据的格式要么引用 .SF文件,要么用隐式引用对其执行计算。
每个 .SF 文件可以有多个数字签名,但这些签名应该由相同的法律实体生成。
文件扩展名可以是1 ~ 3个字母字符。未识别的扩展将被忽略。
下面是应用于清单文件和签名文件的其他限制和规则的列表。
从1.3开始,引入了JarIndex来优化网络应用程序(尤其是applet)的类加载器的类搜索过程。最初,applet类加载器使用简单的线性搜索算法来搜索其内部搜索路径上的每个元素,该搜索路径是由“ARCHIVE”标记或“Class-Path”主属性构造的。类装入器下载并打开其搜索路径中的每个元素,直到找到类或资源。如果类加载器试图找到一个不存在的资源,那么必须下载应用程序或applet中的所有jar文件。对于大型网络应用程序和小程序,这会导致启动缓慢、响应缓慢和浪费网络带宽。JarIndex机制收集applet中定义的所有jar文件的内容,并将这些信息存储在applet类路径的第一个jar文件中的索引文件中。下载第一个jar文件后,applet类加载器将使用收集到的内容信息来高效地下载jar文件。
现有的jar工具得到了增强,它能够检查 jar文件列表,并生成关于哪些类和资源驻留在哪个jar文件中的目录信息。这个目录信息存储在一个名为 INDEX.LIST 的简单文本文件中,这个文件在jar文件的META-INF目录里。当类加载器加载根jar文件时,它读取 INDEX.LIST 文件,并使用它构造一个从文件和包名到jar文件名列表的映射散列表。为了找到类或资源,类装入器查询哈希表以找到合适的jar文件,然后在必要时下载它。
一旦类装入器找到一个INDEX。在特定jar文件中的LIST文件中,它总是信任其中列出的信息。
如果找到了特定类的映射,但是类装入器没有通过链接找到它,就会抛出未指定的Error或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 andLF |
blankline: | newline newline |
newline: | CR LF | LF | CR (not followed by LF ) |
digit: | {0-9 } |
INDEX.LIST
文件由运行 jar -i 生成。有关更多详细信息,请参阅jar手册页。
新的类加载方案完全向后兼容在当前扩展机制之上开发的应用程序。当类装入器装入第一个jar文件和一个INDEX.LIST
文件位于META-INF目录中,它将构造索引哈希表并对扩展使用新的加载方案。否则,类装入器将简单地使用原始的线性搜索算法。
应用程序的清单可以为它需要的其他库指定一个或多个相对url,这些url指向JAR文件和目录。这些相对url将被处理为相对于从其中加载应用程序的代码库(“上下文JAR”)。
应用程序(或者更一般地说,JAR文件)通过清单属性Class-Path指定所需库的相对url。如果在主机Java Virtual Machine上找不到其他库的实现,这个属性将列出用于搜索它们的url。这些相对url可能包括应用程序所需的库或资源的JAR文件和目录。不以'/'结尾的相对url被假定指向JAR文件。例如,
Class-Path: servlet.jar infobus.jar acme/beans.jar images/
在JAR文件的清单中最多可以指定一个类路径头。
如果以下条件为 true,Class-Path条目是有效的:
通过根据上下文JAR的URL解析它,可以使用它来创建URL。
它是相对的,而不是绝对的,即它不包含方案组件,除了上下文JAR从文件系统加载的情况,在这种情况下,由于兼容性原因,文件方案是允许的。
这个条目所表示的JAR文件或目录的位置包含在上下文JAR的包含目录中。使用“. .不允许导航到父目录,除了从文件系统加载上下文JAR的情况。
无效的条目将被忽略。
根据上下文JAR解析有效条目。如果生成的URL无效或引用了无法找到的资源,那么它将被忽略。
重复的url也会被忽略。
生成的URL被插入到类路径中,紧跟在上下文JAR的URL之后。例如,给定以下类路径:
a.jar b.jar
如果b.jar包含以下Class-Path manifest属性:
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定义的所有包都是封闭的,除非专门为一个包覆盖。
封闭的包是通过manifest属性sealed指定的,其值为true或false(与大小写无关)。例如,
Name: javax/servlet/internal/
Sealed: true
指定javax.servlet.internal包是封闭的,并且包中的所有类都必须从同一个JAR文件加载。
如果缺少这个属性,则包封闭属性就是包含JAR文件的属性。
封闭的JAR通过相同的清单头sealed指定,其值同样为true或false。例如,
Sealed: true
指定此存档中的所有包都是封闭的,除非在清单项中使用sealed属性对特定包显式覆盖。
如果缺少此属性,则假定JAR文件不封闭,以便向后兼容。然后系统默认检查包头以封闭信息。
包封闭对于安全性也很重要,因为它将对包保护成员的访问限制为只能访问来自同一个JAR文件的包中定义的类。
未命名的包是不可封闭的,因此要封闭的类必须放在它们自己的包中。
java.security 包
java.util.zip 包