转载

聊一聊 JAR 文件和 MANIFEST.MF

在 JAVA 语言这个圈子里面摸爬滚打,除了对于语言层面和框架层面的学习之外,有一些东西它一直存在,但是确没有对它们有足够的重视,因为都觉得它是理所当然,比如 JAR 是个什么?

提到 JAR,最先可能想到的就是依赖,比如 fastjson.jar ,它可以作为依赖在项目中来引用,但是不能通过 java -jar 来执行,这种就是非可执行的 JAR。另外一种,比如我们项目打包之后生成的 JAR (当然也可能是 war),我们可以通过 java -jar 来运行程序,我们把它称之为可执行的 JAR。

JAR 作用大体可以分为以下几种:

  • 用于发布和使用类库
  • 作为应用程序和扩展的构建单元
  • 作为组件、applet 或者插件程序的部署单位
  • 用于打包与组件相关联的辅助资源

基本概念

JAR 文件是一种归档文件,以 ZIP 格式构建,以 .jar 为文件扩展名。用户可以使用 JDK 自带的 jar 命令创建或提取 JAR 文件。也可以使用其他 zip 压缩工具,不过压缩时 zip 文件头里的条目顺序很重要,因为 MANIFEST 文件常需放在首位。JAR 文件内的文件名是 Unicode 文本。

JAR 文件(Java 归档,英语:Java Archive)是一种软件包文件格式,通常用于聚合大量的 Java 类文件、相关的元数据和资源(文本、图片等)文件到一个文件,以便分发 Java 平台应用软件或库。

以上来自维基百科

JAR 文件格式提供了许多优势和功能,其中很多是传统的压缩格式如 ZIP 或者 TAR 所没有提供的。它们包括:

  • 安全性:可以对 JAR 文件内容加上数字化签名。这样,能够识别签名的工具就可以有选择地为您授予软件安全特权,这是其他文件做不到的,它还可以检测代码是否被篡改过。
  • 减少下载时间:如果一个 applet 捆绑到一个 JAR 文件中,那么浏览器就可以在一个 HTTP 事务中下载这个 applet 的类文件和相关的资源,而不是对每一个文件打开一个新连接。
  • 压缩:JAR 格式允许您压缩文件以提高存储效率。
  • 传输平台扩展。Java 扩展框架 (Java Extensions Framework) 提供了向 Java 核心平台添加功能的方法,这些扩展是用 JAR 文件打包的 (Java 3D 和 JavaMail 就是由 Sun 开发的扩展例子 )。
  • 包密封:存储在 JAR 文件中的包可以选择进行 密封,以增强版本一致性和安全性。密封一个包意味着包中的所有类都必须在同一 JAR 文件中找到。
  • 包版本控制:一个 JAR 文件可以包含有关它所包含的文件的数据,如厂商和版本信息。
  • 可移植性:处理 JAR 文件的机制是 Java 平台核心 API 的标准部分。

JAR 文件格式

这里分别给出两个 JAR 的解压之后的示例

普通的 JAR 解压之后的文件目录

以 fastjson 为例:

.
├── META-INF
│   ├── LICENSE.txt
│   ├── MANIFEST.MF
│   ├── NOTICE.txt
│   ├── maven
│   │   └── com.alibaba
│   │       └── fastjson
│   │           ├── pom.properties
│   │           └── pom.xml
│   └── services
│       ├── javax.ws.rs.ext.MessageBodyReader
│       ├── javax.ws.rs.ext.MessageBodyWriter
│       ├── javax.ws.rs.ext.Providers
│       └── org.glassfish.jersey.internal.spi.AutoDiscoverable
└── com
    └── alibaba
        └── fastjson
            ├── JSON.class
            ├── JSONArray.class
            ├── JSONAware.class
            ├── JSONException.class
            ├── JSONObject.class
            ....省略
复制代码

可执行的 jar (以 SpringBoot 的 FAT JAR 为例)

这个 jar 是从 start.spring.io 上下载下来的一个最简单的 demo 打包来的

├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   └── com
│   │       └── example   # 应用的.class 文件目录
│   │           └── demo
│   │               └── DemoApplication.class
│   └── lib # 这里存放的是应用的 Maven 依赖的jar包文件
│       ├── javax.annotation-api-1.3.2.jar
│       ├── jul-to-slf4j-1.7.26.jar
│       ├── log4j-api-2.11.2.jar
│       ├── log4j-to-slf4j-2.11.2.jar
│       ├── logback-classic-1.2.3.jar
│       ├── logback-core-1.2.3.jar
│       ├── slf4j-api-1.7.26.jar
│       ├── snakeyaml-1.23.jar
│       ├── spring-aop-5.1.8.RELEASE.jar
│       ├── spring-beans-5.1.8.RELEASE.jar
│       ├── spring-boot-2.1.6.RELEASE.jar
│       ├── spring-boot-autoconfigure-2.1.6.RELEASE.jar
│       ├── spring-boot-starter-2.1.6.RELEASE.jar
│       ├── spring-boot-starter-logging-2.1.6.RELEASE.jar
│       ├── spring-context-5.1.8.RELEASE.jar
│       ├── spring-core-5.1.8.RELEASE.jar
│       ├── spring-expression-5.1.8.RELEASE.jar
│       └── spring-jcl-5.1.8.RELEASE.jar
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.example
│           └── demo
│               ├── pom.properties
│               └── pom.xml
└── org
    └── springframework
        └── boot
            └── loader #存放的是 Spring boot loader 的 class 文件
                ├── ExecutableArchiveLauncher.class
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
                ├── LaunchedURLClassLoader.class
                ├── Launcher.class
                ├── MainMethodRunner.class
                ├── PropertiesLauncher$1.class
                ├── PropertiesLauncher$ArchiveEntryFilter.class
                ├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
                ├── PropertiesLauncher.class
                ├── WarLauncher.class
                ├── archive
                │   ├── Archive$Entry.class
                │   ├── ...
                ├── data
                │   ├── RandomAccessData.class
                │   ├── ...
                ├── jar
                │   ├── AsciiBytes.class
                │   ├── ...
                └── util
                    └── SystemPropertyUtils.class
复制代码

META-INF

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

  • MANIFEST.MF:这个 manifest 文件定义了与扩展和包相关的数据。
  • 通过 MAVEN 插件打包进来的文件比如:
    • maven
    • services : 存储所有服务提供程序配置文件
  • 其他的还有一些不常看到的:
    • INDEX.LIST :这个文件由 jar工具的新选项 -i生成,它包含在应用程序或者扩展中定义的包的位置信息。它是 JarIndex 实现的一部分,并由类装载器用于加速类装载过程。
    • .SF:这是 JAR 文件的签名文件
    • .DSA:与签名文件相关联的签名程序块文件,它存储了用于签名 JAR 文件的公共签名。
    • LICENSE.txt :证书信息
    • NOTICE.txt : 公告信息

可执行的 JAR

可以执行的 JAR 与 普通的 JAR 最直接的区别就是能否通过 java -jar 来执行。

一个 可执行的 jar文件是一个自包含的 Java 应用程序,它存储在特别配置的 JAR 文件中,可以由 JVM 直接执行它而无需事先提取文件或者设置类路径。要运行存储在非可执行的 JAR 中的应用程序,必须将它加入到您的类路径中,并用名字调用应用程序的主类。但是使用可执行的 JAR 文件,我们可以不用提取它或者知道主要入口点就可以运行一个应用程序。可执行 JAR 有助于方便发布和执行 Java 应用程序

一个可执行的 JAR 必须通过 menifest 文件的头引用它所需要的所有其他从属 JAR。如果使用了 -jar选项,那么环境变量 CLASSPATH 和在命令行中指定的所有类路径都被 JVM 所忽略。

MANIFEST.MF 文件

当我们用 JAR 命令打完包后,会在根目录下面创建 META-INF 目录,该目录下面会有一些对该 JAR 包信息的描述,其中肯定会有一个 MANIFEST.MF 文件,该文件包含了该 JAR 包的版本、创建人和类搜索路径等信息。

  • FASTJSON jar 中的 MANIFEST.MF 文件

    Manifest-Version: 1.0              # 用来定义manifest文件的版本
    Archiver-Version: Plexus Archiver  # 详见 http://codehaus-plexus.github.io/plexus-archiver/
    Built-By: wenshao                  # 构建者
    Created-By: Apache Maven 3.5.0  #  # 声明该文件的生成者,一般该属性是由 jar 命令行工具生成的
    Build-Jdk: 1.8.0_162               # 基于构建的 JDK 版本
    复制代码
  • SpringBoot demo 的 MANIFEST.MF 文件

    Manifest-Version: 1.0
    Implementation-Title: demo                     # 定义了扩展实现的标题
    Implementation-Version: 0.0.1-SNAPSHOT         # 定义扩展实现的版本
    Start-Class: com.example.demo.DemoApplication  # 启动类
    Spring-Boot-Classes: BOOT-INF/classes/         # 编译之后的 class 文件目录
    Spring-Boot-Lib: BOOT-INF/lib/                 # 当前工程依赖的 jar 包目录
    Build-Jdk-Spec: 1.8                            # 指定的 JDK 版本
    Spring-Boot-Version: 2.1.6.RELEASE             # SpringBoot 版本
    Created-By: Maven Archiver 3.4.0             
    Main-Class: org.springframework.boot.loader.JarLauncher  # Main 函数
    复制代码

在 Java 平台中, MANIFEST 文件是 JAR 归档中所包含的特殊文件,MANIFEST 文件被用来定义扩展或文件打包相关数据。

MANIFEST 文件作为一个元数据文件,它包含了不同部分中的 k-v 对数据。

如果一个 JAR 文件被当作可执行文件,则其中的 MANIFEST 文件需要指出该程序的主类文件,如上面案例中的 SpringBoot demo 的那个 jar 中的MANIFEST 文件所示

MANIFEST 作用

从 MANIFEST 文件中提供的信息大概可以了解到其基本作用

  • JAR 包基本信息描述
  • Main-Class 指定程序的入口,这样可以直接用java -jar xxx.jar来运行程序
  • Class-Path 指定jar包的依赖关系,class loader会依据这个路径来搜索class

获取 MANIFEST.MF

JDK 中提供了可以获取 jar 包中 MANIFEST.MF 文件信息的工具,可以通过 java.util.jar 这个类库来获取。

JarFile jar = new JarFile(new File("/Users/glmapper/Documents/test/demo/target/demo-0.0.1-SNAPSHOT.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());
    }
}
复制代码

执行结果为:

main	Implementation-Title:demo
main	Implementation-Version:0.0.1-SNAPSHOT
main	Start-Class:com.example.demo.DemoApplication
main	Spring-Boot-Classes:BOOT-INF/classes/
main	Spring-Boot-Lib:BOOT-INF/lib/
main	Build-Jdk-Spec:1.8
main	Spring-Boot-Version:2.1.6.RELEASE
main	Created-By:Maven Archiver 3.4.0
main	Manifest-Version:1.0
main	Main-Class:org.springframework.boot.loader.JarLauncher

复制代码

Jar 文件和 Manifest 在 java 中的定义

下面为 JarFile 的定义,从代码就可以看出,前面我们所介绍的 Jar 是以 ZIP 格式构建一种归档文件,因为它是 ZipFile 的子类。

public class JarFile extends ZipFile {
    private SoftReference<Manifest> manRef;
    private JarEntry manEntry;
    private JarVerifier jv;
    private boolean jvInitialized;
    private boolean verify;
    //指示是否存在Class-Path属性(仅当hasCheckedSpecialAttributes为true时才有效)
    private boolean hasClassPathAttribute;
    // 如果清单检查特殊属性,则为 true
    private volatile boolean hasCheckedSpecialAttributes;
    // 在SharedSecrets中设置JavaUtilJarAccess
    static {
        SharedSecrets.setJavaUtilJarAccess(new JavaUtilJarAccessImpl());
    }
    /**
     * The JAR manifest file name.(JAR清单文件名)
     */
    public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
    // 省略其他
}
复制代码

下面是 Manifest 类的定义,用来描述 JAR 的 清单文件。从其属性中也很好的观察到,其存储的就是 K-V 键值对数据。

public class Manifest implements Cloneable {
    // manifest main attributes
    private Attributes attr = new Attributes();
    // manifest entries
    private Map<String, Attributes> entries = new HashMap<>();
    // 省略其他
}
复制代码

小结

JAR 格式远远超出了一种压缩格式,它有许多可以改进效率、安全性和组织 Java 应用程序的功能。因为这些功能已经建立在核心平台 -- 包括编译器和类装载器 -- 中了,所以开发人员可以利用 JAR 文件格式的能力简化和改进开发和部署过程。

原文  https://juejin.im/post/5d16cc8cf265da1b8d163237
正文到此结束
Loading...