在将 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 文件的内容结构:
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 子目录中。最小的符合标准的清单文件是很简单的:
复杂的清单文件会包含更多的属性条目,这些清单条目被分成多个节。第一节被称为主节(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
大多数 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