首页 文章详情

漫谈 JAR : JAR 快查手册

愿天堂没有BUG | 519 2021-06-12 11:15 0 0 0
UniSMS (合一短信)

总文档 :文章目录
Github : github.com/black-ant

每次文章都很尽量走纯技术路线 ,为什么就没人点赞呢?????

一 . 前言

本篇文章梳理一下一个 JAR 包 , 有什么涉及的要点

1.1  JAR 简述

JAR文件 是一种基于Zip的文件格式,用于将许多文件汇总到一个 , JAR 文件本质上是一个 zip 文件,其中包含一个可选的 META-INF 目录。

JAR 命令 是基于 ZIP 和 ZLIB 压缩格式的通用归档和压缩工具。最初,jar 命令用于打包 Java applet (自 JDK 11以来不受支持)或应用程序; 然而,从 JDK 9开始,用户可以使用 jar 命令创建模块化 jar

二 . 包结构

Java 里面想运行一个项目 , 通常有 war 和 jar 2种方式 , 过去最常见的就是通过 war 包部署项目 ,


包结构作用

.
|-- BOOT-INF
| |-- classes
| | `-- com
| | `-- gang
| | `-- study
| | `-- maven
| | |-- BootdependencyApplication.class
| | `-- service
| | `-- AopAdvice.class
| `-- lib
| |-- 省略 JAR
|-- META-INF
| |-- MANIFEST.MF
| `-- maven
| `-- com.gang.study.maven.bootdependency
| `-- com-gang-study-maven-bootdependency
| |-- pom.properties
| `-- pom.xml
`-- org
`-- springframework
`-- boot
`-- loader
|-- 省略 class

复制代码

来看一下正常包结构中各个目录的作用

// META-INF 目录 用于存储包和扩展配置数据,包括安全性、版本控制、扩展、配置应用程序、类加载器和服务 , META-INF 目录中的文件/目录由java2平台识别和解释

  • MANIFEST.MF : 用于定义扩展和包相关数据的清单文件

  • INDEX.LIST : 由 "-i" 命令生成 , 该文件包含应用程序或扩展中定义的包的位置信息。它是 JarIndex 实现的一部分,由类装入器用来加速其类装入过程

  • x.SF : JAR 文件的签名文件. ‘ x’代表基本文件名

  • x.DSA : 具有相同基文件名的签名文件关联的签名块文件。此文件存储相应签名文件的数字签名

  • maven : maven 的配置消息

BOOT-INF 目录 在后文详述

附录 一 : MANIFEST.MF 文件

// 规范的版本
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
// 多重发布 : 存在多个 JDK 版本
Multi-Release: true
//创建清单文件的用户的名称
Built-By: 10169
// SpringBoot 启动类
Start-Class: com.gang.study.maven.BootdependencyApplication
// SpringBoot 类路径
Spring-Boot-Classes: BOOT-INF/classes/
// Spring-Boot lib 文件路径
Spring-Boot-Lib: BOOT-INF/lib/
// Spring Boot 版本
Spring-Boot-Version: 2.2.6.RELEASE
// 创建清单文件的工具版本和供应商
Created-By: Apache Maven 3.6.1
// 此自定义头给出创建清单文件的用户的名称
Build-Jdk: 1.8.0_152
// Main 方法入口 , 这里更像一个加载器
Main-Class: org.springframework.boot.loader.JarLauncher
// 到库或资源的相对路径的空格分隔列表
Class-Path: core.jar lib/ properties/


//---------------- 其他的属性
Name: 包名
Implementation-Build-Date: 实施的构建日期
Implementation-Title: 执行的标题
Implementation-Vendor: 实施的供应商
Implementation-Version: 实施版本
Specification-Title: 规范的标题
Specification-Vendor: 规范的供应商
Specification-Version: 规格版本
Sealed: 那么包的所有类是否都来自同一个 JAR


// 一 : 格式
MANIFEST.MF 是一个标准的 key:value ( headers:attributes)
key1: value1
Key2: value2
// 有效的标题必须在冒号和值之间有空格。另一个重要的地方是在文件的末尾必须有一个新的行 (这个地方坑了很久 , 怎么都导不进去 , 需要换行才对) , 否则最后一行会被忽略



复制代码

附录二  : 包签名

JavaTM 平台允许您对 JAR 文件进行数字签名 , 在对 JAR 文件进行签名时,还可以选择对签名进行时间戳 , 时间戳可用于验证用于签名 JAR 文件的证书在签名时是否有效.

为此 , 还可以配置安全策略控制 , 可以将策略配置为向 applet 和应用程序授予安全特权 ,例如,您可以授予 applet 执行通常禁止的操作的权限,例如读写本地文件或运行本地可执行程序

一个签名主要由 4个元素完成 : 私钥 , 公钥与证书 , 摘要值

签名的使用方式 : Java 平台通过使用称为公钥和私钥的特殊号码来实现签名和验证

使用流程 :  签名者使用私钥为 JAR 文件签名 , 应用的公钥与证书一起放在 JAR 文件中,以便任何想要验证签名的人都可以使用它

摘要值 : 摘要值是文件内容的散列或编码表示形式,与签名时相同。当且仅当文件本身发生变化时,文件摘要才会发生变化。

证书 : 仅使用公钥和私钥还不足以真正验证签名 , 需要某种方法来确认公钥实际上来自它声称来自的签名者 , 这就用到了证书的概念 (添加一个附加元素 , 该元素是签名者在签名 JAR 文件中包含的证书)


// 当一个 JAR 文件被签名时,一个签名文件被自动生成并放置在 JAR 文件的 META-INF 目录中
// 这个目录包含了归档文件的清单 , 签名文件的文件名是.SF 扩展

Signature-Version: 1.0
SHA1-Digest-Manifest: h1yS+K9T7DyHtZrtI+LxvgqaMYM=
Created-By: 1.7.0_06 (Oracle Corporation)

Name: test/classes/ClassOne.class
SHA1-Digest: fcav7ShIG6i86xPepmitOVo4vWY=

Name: test/classes/ClassTwo.class
SHA1-Digest: xrQem9snnPhLySDiZyclMlsFdtM=

Name: test/images/ImageOne.gif
SHA1-Digest: kdHbE7kL9ZHLgK7akHttYV4XIa0=

Name: test/images/ImageTwo.gif
SHA1-Digest: mF0D5zpk68R4oaxEqoS9Q7nhm60=


复制代码

三 . 常用操作

3.1  获取  jar 包中类路径

// JAR 命令获取 
打印出 jar 中所有的class名 : jar tf com-gang-bootdependency-1.0-SNAPSHOT.jar
打印所有的 class : jar tf com-gang-bootdependency-1.0-SNAPSHOT.jar | grep '\.class$'

// Java 代码获取
public static Set<String> getClassNamesFromJarFile(File givenFile) throws IOException
{
Set<String> classNames = new HashSet<>();
try (JarFile jarFile = new JarFile(givenFile)) {
Enumeration<JarEntry> e = jarFile.entries();
while (e.hasMoreElements()) {
JarEntry jarEntry = e.nextElement();
if (jarEntry.getName().endsWith(".class")) {
String className = jarEntry.getName()
.replace("/", ".")
.replace(".class", "");
classNames.add(className);
}
}
return classNames;
}
}

// 运行时动态获取
public static Set<Class> getClassesFromJarFile(File jarFile) throws IOException, ClassNotFoundException {
Set<String> classNames = getClassNamesFromJarFile(jarFile);
Set<Class> classes = new HashSet<>(classNames.size());
try (URLClassLoader cl = URLClassLoader.newInstance(
new URL[] { new URL("jar:file:" + jarFile + "!/") })) {
for (String name : classNames) {
Class clazz = cl.loadClass(name); // Load the class by its name
classes.add(clazz);
}
}
return classes;
}

复制代码

3.2  生成tree树结构的几种方式

如果想要打印 tree 结构有以下几种方式 :


// 方式一 : tree 命令可以用于 Windows 生成目录数 (PS : 应该是系统本身就有该命令)
打印到当前文件 : tree /f >tree.txt


// 方式二 : tree for windows (PS : 这个和上面不是一个东西)
1. 下载 tree for windows 文件 : http://gnuwin32.sourceforge.net/packages/tree.htm (选择 Binaries.zip)
2. 解压获取 tree.exe , 放在 git 项目中 : Git\usr\bin (注意 , 外层的 bin , 需要到 usr 目录下)
3. 项目路径下反键 - git base here (即 git 控制台)
4. 运行 tree 命令

-A 使用ASNI绘图字符显示树状图而非以ASCII字符组合。
-C 在文件和目录清单加上色彩,便于区分各种类型。
-d 显示目录名称而非内容。
-D 列出文件或目录的更改时间。
-f 在每个文件或目录之前,显示完整的相对路径名称。
-F 在执行文件,目录,Socket,符号连接,管道名称名称,各自加上"*","/","
=","@","|“号。
-g 列出文件或目录的所属群组名称,没有对应的名称时,则显示群组识别码。
-i 不以阶梯状列出文件或目录名称。
-I 不显示符合范本样式的文件或目录名称。
-l 如遇到性质为符号连接的目录,直接列出该连接所指向的原始目录。
-n 不在文件和目录清单加上色彩。
-N 直接列出文件和目录名称,包括控制字符。
-p 列出权限标示。
-P 只显示符合范本样式的文件或目录名称。
-q 用”?"号取代控制字符,列出文件和目录名称。
-s 列出文件或目录大小。
-t 用文件和目录的更改时间排序。
-u 列出文件或目录的拥有者名称,没有对应的名称时,则显示用户识别码。
-x 将范围局限在现行的文件系统中,若指定目录下的某些子目录,其存放于另一个文件系统上,则将该子目录予以排除在寻找范围外。


$ tree
.
|-- classes
| `-- com
| `-- gang
| `-- study


// 方式三 : 在线 tree 转换
http://dir.yardtea.cc/

// 方式四 : 使用软件显示如上图所示的数结构
下载软件 DirPrintOK
复制代码

3.3 JAR 反编译方法

// 方案一 :  工具查看

1. ZIP 解压后 , 直接通过 Java Decompiler 查看
?- 该方案对于部分 class 有局限性 , 看出来的如下所示 : // INTERNAL ERROR //
2. JD Project : 最常用的最好的 java 脱机反编译器之一
3. Cavaj Java Decompiler
4. DJ Java Decompiler
5. JBVD
6. androidchef
7. Procyon
8. CFR Decompiler
9. FernFlower
10. Krakatau
11. Luyten

// 编译器大同小异 , 无非就是转码 , 以上任意一种都可以试着用一下 , 一般我自己使用 JD-GUI / Jar Explorer
https://java-decompiler.github.io/


// 方案二 : 命令行反编译
java -jar JDCommandLine.jar $
{TARGET_JAR_NAME}.jar ./classes
复制代码

3.4 如何预防反编译问题

Java 字节码保留有关字段、方法返回值和参数的类型信息,但是它不包含本地变量的类型信息

Java 类文件中的类型信息使得字节码的反编译任务比机器代码的反编译更加容易

因此,反编译 Java 字节码需要分析大多数局部变量类型,平坦堆栈指令和结构化循环和条件。

方案一 : 使用 YGuard 混淆类文件 (类似的还有 ProGuard 等 )

可以将已编译的代码转换为人类难以理解的代码 , 同时有助于减少应用程序的启动时间.

最重要的是 , 这是一个完全开源的程序. YGuard 的混淆方式 :

  • 通过使用不可表达的字符替换包、类、方法和字段名,从逆向工程中删除类文件

  • 收缩引擎分析所有输入 Jar 文件的字节码,以确定从一组给定的代码入口点无法到达哪些代码实体。然后 yGuard 将删除这些过时的代码片段(整个类或单个方法和字段)。

YGuard 模糊的是 class 文件  , YGuard 是一个免费的开源 Java 字节码混淆器和 shrinker , 通过模糊处理,参考地址 :

@ github.com/yWorks/yGua…
@ github.com/yWorks/yGua…

<dependency>
<groupId>com.yworks</groupId>
<artifactId>yguard</artifactId>
<version>3.0.0</version>
<scope>compile</scope>
</dependency>

<!-- 配置规则详见官方文档 -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<id>obfuscate</id>
<configuration>
<tasks>
<property name="runtime_classpath" refid="maven.runtime.classpath"/>
<taskdef name="yguard" classname="com.yworks.yguard.YGuardTask" />
<yguard>
<!-- see the yGuard task documentation for information about the <yguard> element-->
</yguard>
</tasks>
</configuration>
</execution>
</executions>
</plugin>




复制代码

方案二 : 如果想要隐藏某个字符串 , 可以考虑对属性加密

方案三 :很多反编译器没有适应新特性和某些工具框架 . 例如 : lambdas

方案四 :定制一个 ClassLoader 用于专门的解密

方案五 : 使用 protector4j 对 jar 文件加密 , 将 JAR 文件转换为私有的 JARX 格式,保护类文件和应用程序结构

@ protector4j.com/
(PS : 个人没用过 , 不评价 , 类似的还有 JarProtector , 不过 protector4j 比较新 )

3.5 Maven 打包  JAR

<!-- Maven pom.xml 文件构建 jar-->
<modelVersion>4.0.0</modelVersion>
<version>0.1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<!-- Plugin Java-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>
<mainClass>
com.baeldung.executable.ExecutableMavenJar
</mainClass>
</manifest>
</archive>
</configuration>
</plugin>


<!-- Plugin Spring -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<classifier>spring-boot</classifier>
<mainClass>
com.baeldung.executable.ExecutableMavenJar
</mainClass>
</configuration>
</execution>
</executions>
</plugin>

<!-- 通常也可以直接使用 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

复制代码

3.6 把文件打包到 JAR 中

可以使用 jar -u 命令更新 jar 中的文件 , 常常用于系统的定制等操作 , 通常的格式如下 :



jar uf JarExample.jar com/baeldung/jar/JarExample.class


// 案例 : 需要替换 parent.jar 内部的 child-1.3.4.jar 依赖

// Step 1 : 准备好相关的路径 (例如需要放在该路径下 , 先在外部创建一个相同的路径)
构建目录 :\new_version\BOOT-INF\classes\bundles

// Step 2 : 将 bundles jar 移动到 该路径

// Step 3 : 执行命令执行
jar -uvf0 parent.jar BOOT-INF/classes/bundles/child-1.3.4.jar


复制代码

四 . 其他操作

4.1 手动创建一个 JAR 文件

在这个案例里面 , 我会试着手动编译 .java 并且打包成一个 jar 文件并且执行

// Step 1 : 准备一个 .java 方法
public class DefaultMain {
public static void main(String[] args){
System.out.println("Test Default Main Success!");
}
}

// Step 2 : 打包为 .class 文件 (运行后生成一个 DefaultMain.class)
javac -d . DefaultMain.java


// Step 3 : (此处可以省略 , 参考后文设置清单文件)

// Step 4 : 打包到 JAR 文件中 (运行后生成一个 example.jar)
jar cfe example.jar com/gang/DefaultMain com/gang/DefaultMain.class

// Step 5 : 执行 jar
java -jar example.jar


// 附录 : JAR 包结构
.
|-- META-INF
| `-- MANIFEST.MF
|-- com
| `-- gang
| `-- DefaultMain.class
`-- tree.txt

3 directories, 3 files


复制代码

4.2 Maven 添加 MANIFEST.MF 文件

这种方式是通过 Maven 编辑  MANIFEST.MF 文件

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifest>
<packageName>com.baeldung.java</packageName>
</manifest>
<manifestEntries>
<Created-By>baeldung</Created-By>
</manifestEntries>
</archive>
</configuration>
</plugin>
复制代码

4.3 JAR 添加 MANIFEST.MF 文件

通过 JAR 命令 手动添加 MANIFEST.MF , 其中最重要的一点 : 使用换行符结束清单文件 , 否者默认会忽略

// Step 1 : 准备一个任意的文本文件 , 添加属性(记得回车)
Built-By: gang

// Step 2 : 使用命令打包
jar cvfm example.jar manifest.txt com/gang/DefaultMain.class

../test>jar cvfm example.jar manifest.txt com/gang/DefaultMain.class
已添加清单
正在添加: com/gang/DefaultMain.class(输入 = 442) (输出 = 299)(压缩了 32%)

// 解压后最终结果 :
Manifest-Version: 1.0
Built-By: gang
Created-By: 1.8.0_152 (Oracle Corporation)

复制代码

4.4  JAR  中定义 JDK 多版本

当期望  JAR 包能在多种版本的 JDK 运行时 , 可以使用如下方式构建 :

├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── gang
│ │ │ └── test
│ │ │ ├── DefaultVersion.java
│ │ │ └── App.java
│ │ └── java9
│ │ └── com
│ │ └── gang
│ │ └── test
│ │ └── DefaultVersion.java



<!-- Step 1 : 通过 Maven 构建 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java-8</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</execution>
<execution>
<id>compile-java-9</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>9</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
</compileSourceRoots>
<!-- > 3.7.1 时替换下句 : <multiReleaseOutput>true</multiReleaseOutput> -->
<outputDirectory>${project.build.outputDirectory}/META-INF/versions/9</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

<!-- Step 2 : MANIFEST 文件中将 Multi-Release 条目设置为 true -->
<!--使用这种配置,Java 运行时将在 JAR 文件的 META-INF/versions 文件夹中查找特定于版本的类; 否则,只使用基类-->

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
</plugin>



复制代码

4.5  Fat JAR 打包

什么是 Fat jat ?

Fat jar 同样是一个 jar , 与普通 jar 的区别是 : 它包含来自所有库的类,您的项目依赖于这些库,当然还有当前项目的类.

以 Spring 的打包方式为例 : Spring 将应用程序代码打包到BOOT-INF.classes,将依赖包打包到BOOT-INF.lib目录Spring 这种应该就是 Fat JAR " 将 dependencies 的 jar 复制到主 jar,然后使用特殊的类加载器加载

 




<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>source-release-assembly</id>
<phase>none</phase>
</execution>
<execution>
<id>source-release-assembly-no-eclipse-libs</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<runOnlyAtExecutionRoot>true</runOnlyAtExecutionRoot>
<descriptors>
<descriptor>src/assemble/${sourceReleaseAssemblyDescriptor}.xml</descriptor>
</descriptors>
<tarLongFileMode>gnu</tarLongFileMode>
</configuration>
</execution>
</executions>
</plugin>


<properties>
<start-class>org.baeldung.boot.Application</start-class>
</properties>
复制代码

五 . JAR 命令及常见操作

5.1 JAR 命令模板

jar {ctxu}[vfm0M] [jar-文件] [manifest-文件] [-C 目录] 文件名 ...

其中 {ctxu} 是 jar 命令的子命令,每次 jar 命令只能包含 ctxu 中的一个,它们分别表示

-c  : 创建新的 JAR 文件包  
-t  : 列出 JAR 文件包的内容列表  
-x  :  展开 JAR 文件包的指定文件或者所有文件  
-u  :  更新已存在的 JAR 文件包 (添加文件到 JAR 文件包中)  
[vfm0M]  :  中的选项可以任选,也可以不选,它们是 jar 命令的选项参数  
-v  :  生成详细报告并打印到标准输出  
-f  :  指定 JAR 文件名,通常这个参数是必须的  
-m  :  指定需要包含的 MANIFEST 清单文件  
-0  :  只存储,不压缩,这样产生的 JAR 文件包会比不用该参数产生的体积大,但速度更快  
-M  :  不产生所有项的清单(MANIFEST〕文件,此参数会忽略 -m 参数  
[jar-文件]  :  即需要生成、查看、更新或者解开的 JAR 文件包,它是 -f 参数的附属参数  
[manifest-文件]  :  即 MANIFEST 清单文件,它是 -m 参数的附属参数  
[-C 目录]  :  表示转到指定目录下去执行这个 jar 命令的操作。它相当于先使用 cd 命令转该目录下再执行不带 -C 参数的 jar 命令,它只能在创建和更新 JAR 文件包的时候可用。 
文件名 ...  :  指定一个文件/目录列表,这些文件/目录就是要添加到 JAR 文件包中的文件/目录。如果指定了目录,那么 jar 命令打包的时候会自动把该目录中的所有文件和子目录打入包中。 

5.2 JAR 常用命令

@ jar-The Java Archive Tool (oracle.com)

一 : JAR 命令设置 Class 主类

jar cfe example.jar com.gang.DefaultMain com/gang/*.class

// 生成的 MANIFEST 文件
Manifest-Version: 1.0
Created-By: 1.8.0_152 (Oracle Corporation)
Main-Class: com.gang.DefaultMain
复制代码

二 : 运行 jar

java -jar test.jar
复制代码

三 : jar 内容管理

// 提取 JAR 内容
jar xf jar-file [archived-file(s)]

// 查看 JAR 内容
jar tf jar-file

// 更新 JAR 内容
jar uf jar-file input-file(s)

复制代码

四 : 清单管理

// 命令行修改清单文件
jar cfm jar-file manifest-addition input-file(s)

// 设置默认入口
jar cfm MyJar.jar Manifest.txt MyPackage/*.class

// 添加 class
jar cfm MyJar.jar Manifest.txt MyPackage/*.class

复制代码

其他检索

// 指定主类 : 
> java -cp JarExample.jar com.baeldung.jar.JarExample
> java -cp JarExample.jar com/baeldung/jar/JarExample

// 列出 Jar 的内容 :
> jar tf JarExample.jar

// 查看清单文件
> unzip -p JarExample.jar META-INF/MANIFEST.MF

复制代码

六 . 其他

6.1 WAR 与 JAR 的区别

JAR 包

简单地说,JAR 或 Java Archive 是一种包文件格式 , JAR 文件具有 .Jar 扩展名 , 可能包含库、资源和元数据文件 , 实际上,它是一个压缩文件,包含. 类文件的压缩版本以及编译的 Java 库和应用程序的资源

WAR 包

WAR 是 Web Application Archive 或 Web Application Resource 的缩写 , 这些归档文件具有 .War 扩展,用于打包 web 应用程序,这些应用程序可以部署在任何 Servlet/JSP 容器上。

// WAR 包格式
META-INF/
MANIFEST.MF
WEB-INF/
web.xml
jsp/
helloWorld.jsp
classes/
static/
templates/
application.properties
lib/
// *.jar files as libs

复制代码

6.2  java.util.jar  api 使用

官方 API 文档

6.3 对比 BOOT-INF

前面看到还有一个 BOOT-INF , 这个文件夹的目的又是什么 ?

参考地址

BOOT-INF 结构的特性 :

  • Spring Boot 应用程序加载表单 BOOT-INF 文件夹

  • 应用程序类应该放在嵌套的 BOOT-INF/classes 目录中

  • 依赖项应该放在一个嵌套的 BOOT-INF/lib 目录中

BOOT-INF 解决的问题 :

Java 没有提供任何加载嵌套 jar 文件的标准方法(即加载本身包含在 jar 中的 jar 文件)。当需要分发一个可以从命令行运行而不需要解压缩的自包含应用程序时 , 会出现问题

PS : 其实这里就是前文说的 FAT JAR

为此 , Spring 采用了如下的目录结构 , 将 应用程序类放在一个嵌套的 BOOT-INF/classes 目录中

example.jar
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-BOOT-INF
+-classes
| +-mycompany
| +-project
| +-YourClasses.class
+-lib
+-dependency1.jar
+-dependency2.jar

// SpringBoot 对于 War 包的目录格式 (依赖项应该放在嵌套的 WEB-INF/lib 目录中) :
example.war
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-WEB-INF
+-classes
| +-com
| +-mycompany
| +-project
| +-YourClasses.class
+-lib
| +-dependency1.jar
| +-dependency2.jar
+-lib-provided
+-servlet-api.jar
+-dependency3.jar


复制代码

BOOT-INF 的索引

前面说了 , BOOT-INF 说到底是为了聚合包和应用程序 , 他有多种索引文件 :

  • Classpath Index (classpath.idx) : 提供了将 jars 添加到类路径的顺序

  • Layer  Index (layers.idx) : 提供了一个 Layer 和 JAR 中应该包含的部分的列表

// classpath.idx
- "BOOT-INF/lib/dependency2.jar"
- "BOOT-INF/lib/dependency1.jar"

// layers.idx
- "dependencies":
- "BOOT-INF/lib/dependency1.jar"
- "BOOT-INF/lib/dependency2.jar"
- "application":
- "BOOT-INF/classes/"
- "META-INF/"


复制代码

BOOT-INF 的加载

BOOT-INF 基于 org.springframework.boot.loader.jar 进行加载  , 该类允许从标准 jar 文件或嵌套的子 jar 数据中加载 jar 内容

首次加载时,每个 JarEntry 的位置映射到外部 jar 的物理文件偏移量

myapp.jar
+-------------------+-------------------------+
| /BOOT-INF/classes | /BOOT-INF/lib/mylib.jar |
|+-----------------+||+-----------+----------+|
|| A.class ||| B.class | C.class ||
|+-----------------+||+-----------+----------+|
+-------------------+-------------------------+
^ ^ ^
0063 3452 3980

// 可以看到 :
1. 在 myapp.jar 的位置0063的/BOOT-INF/classes 中找到 A.class
2. 在 myapp.jar 中位于3452的位置找到来自嵌套 jar 的 B.class
3. C.class 位于3980的位置

复制代码

有了这些信息,我们就可以通过寻找外部 jar 的适当部分来加载特定的嵌套条目 , 这就是 BOOT-INF 解决外部以来的主要方式

同时 , Spring 提供了 多个启动器装载资源 :

  • Springframework.boot.loader.Launcher

    • 该类是一个特殊的引导类,用作可执行 jar 的主入口点。它是 jar 文件中的实际 Main-Class,用于设置适当的 URLClassLoader 并最终调用 main ()方法

  • JarLauncher : 子启动器 , 在 BOOT-INF/lib/中查找

  • WarLauncher : 子启动器 ,  在 WEB-INF/lib/和 WEB-INF/lib-proved/中查找

  • PropertiesLauncher : 子启动器 , 默认在应用程序归档中查找 BOOT-INF/lib/ (允许添加额外路径)

// 配置方式 :
Main-Class: org.springframework.boot.loader.WarLauncher
Start-Class: com.mycompany.project.MyApplication

复制代码

总结

不知道怎么写总结了 , 本来就是一篇总结文档 , 所以没什么需要总结的..

只是想吐槽一下 , 一直以来都在走技术路线 , 但是深深的感觉长篇幅的技术文档 , 受众面很小. 虽然写文档本身还是为了服务自己 , 但是没人看确实影响心态 , 一度怀疑是不是总结的不好 , 以后自己用来回顾的时候会不会也看不懂了~~

参考

官方参考文档

  • JAR File Specification (oracle.com)

  • The Java™ Tutorials

其他参考文档

  • www.baeldung.com/jar-file-ge…

  • www.baeldung.com/java-view-j…

  • www.baeldung.com/java-view-j…

  • www.baeldung.com/install-loc…

  • www.baeldung.com/executable-…

  • www.baeldung.com/deployable-…

  • www.baeldung.com/java-jar-ex…

  • www.baeldung.com/java-create…

  • www.baeldung.com/java-jar-wa…

  • www.baeldung.com/java-jar-ma…

  • www.baeldung.com/maven-multi…


作者:AntBlack
链接:https://juejin.cn/post/6971822360810749966
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter