Jar 包那些事


title: Jar 包那些事
date: 2020/11/23 15:55


引言

公司中有一个系统用的是 Dubbo + SpringBoot 的架构,但后来发现好像并没有必要用 dubbo 的架构所以准备直接让 web 层引用 service 层的 jar 包,有如下问题:

  1. Spring Boot 项目打包成的 jar ,被其他项目依赖之后,总是报找不到类的错误?
  2. 不使用 spring-boot-maven-plugin 插件打 jar 包,那么打进去的配置文件会不会生效? -> 测试后发现生效的。

可执行 jar 包 & 依赖 jar 包

一般使用 maven clean package 打包出来的 jar 包就是依赖 jar,引入一些插件我们就可以将其打成可执行 jar,一般而言两者的目录结构没有什么区别(见附1),唯一有区别的是 MANIFEST.MF(清单文件)。

依赖 jar 包的 MANIFEST.MF

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: x5456
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_181

可执行 jar 包的 MANIFEST.MF

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: x5456
Class-Path: lib/hutool-all-4.6.1.jar    # 依赖 jar 包的路径
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_181
Main-Class: cn.x5456.TestCanRunJar  # 使用命令(java -jar xxx.jar)运行时的主启动类

META-INF 文件夹是干啥的,META-INF文件夹能删吗?

如果你将 Jar 中的 META-INF 文件夹删除,那么 jar 文件里边就没有 MANIFEST.MF 文件。那么,java -jar 就找不到 main class.

没有 META-INF 你仍然可以创建一个 Jar 文件。但是,当你想要执行 jar 文件的时候,这个 jar 是需要具备 META-INF/MANIFEST.MF 的。

附1:可执行 jar 与依赖 jar 打包出来的文件结构

image

SpringBoot 打包的奥秘

这个是我们对一个 SpringBoot 项目执行 maven clean package 命令打包出来的结构:

image

我们发现这里有两个文件,第一个 spring-test-0.0.1-SNAPSHOT.jar 表示打包成的可执行 jar ,第二个 spring-test-0.0.1-SNAPSHOT.jar.original 则是在打包过程中 ,被重命名的 jar,这是一个不可执行 jar,但是可以被其他项目依赖的 jar。

original:原件

为什么会有两个文件呢?

在新建 SpringBoot 项目的时候,我们引入了一个插件:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
image

spring-boot-maven-plugin 项目存在于 spring-boot-tools 目录中,它的默认目标(goal)就是 repackage 功能,其他功能要使用,则需要开发者显式配置。

spring-boot-maven-plugin 的 repackage 能够将 mvn package 生成的软件包,再次打包为可执行的软件包,并将 mvn package 生成的软件包重命名为 *.original。

spring-boot-maven-plugin 的 repackage 在代码层面调用了 RepackageMojo的execute 方法,而在该方法中又调用了repackage 方法,repackage 方法代码及操作解析如下:

private void repackage() throws MojoExecutionException {
    // maven生成的jar,最终的命名将加上.original后缀
    Artifact source = getSourceArtifact();
    // 最终为可执行jar,即fat jar
    File target = getTargetFile();
    // 获取重新打包器,将maven生成的jar重新打包成可执行jar
    Repackager repackager = getRepackager(source.getFile());
    // 查找并过滤项目运行时依赖的jar
    Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
    // 将artifacts转换成libraries
    Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());
    try {
        // 获得Spring Boot启动脚本
        LaunchScript launchScript = getLaunchScript();
        // 执行重新打包,生成fat jar
        repackager.repackage(target, libraries, launchScript);
    } catch (IOException ex) {
        throw new MojoExecutionException(ex.getMessage(), ex);
    }
    // 将maven生成的jar更新成.original文件
    updateArtifact(source, target, repackager.getBackupFile());
}

两个 jar 包比较

可执行 jar spring-test-0.0.1-SNAPSHOT.jar 解压之后,目录结构如下:

├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   └── cn
│   │       └── x5456   # 应用的.class 文件目录
│   │           └── springtest
│   │               └── SpringTestApplication.class
│   └── lib # 这里存放的是应用的 Maven 依赖的jar包文件
│       ├── javax.annotation-api-1.3.2.jar
│       ├── spring-beans-5.1.8.RELEASE.jar
│       └── ...
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.x5456
│           └── springtest
│               ├── 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

我们的代码被放在了 BOOT-INF/classes/ 目录下。

META-INF 目录存放着当前 jar 包的清单文件(MANIFEST.MF)和当前 jar 包引入的 maven 依赖信息(pom.xml),我们看下 MANIFEST.MF 文件:

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: spring-test
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: cn.x5456.springtest.SpringTestApplication  # 这个是 Spring 自定义的属性,存放我们的启动类的全类路径
Spring-Boot-Classes: BOOT-INF/classes/  # 这个是 Spring 自定义的属性,表示我们自己代码编译后的位置
Spring-Boot-Lib: BOOT-INF/lib/  # 这个是 Spring 自定义的属性,表示项目依赖的 jar 的位置
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.2.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher # 当使用 java -jar xxx.jar 命令启动的时候会调用这个类的 main 方法。

依赖 jar spring-test-0.0.1-SNAPSHOT.jar.original 解压之后,目录结构如下:

image

依赖 jar 并没有 BOOT-INF/classes 目录,而且也没有把它所依赖的 jar 包打进来,当其他项目引用这个 jar 包的时候(通过 maven 坐标),他会去找 META-INF/maven/pom.xml 文件,将当前 jar 包所依赖的其他 jar 包从 maven 仓库拉下来。

我们顺道看下它的 META-INF/MANIFEST.MF 文件,其中没有定义启动类等。

Manifest-Version: 1.0
Implementation-Title: spring-test
Implementation-Version: 0.0.1-SNAPSHOT
Build-Jdk-Spec: 1.8
Created-By: Maven Jar Plugin 3.2.0

为什么 SpringBoot 不直接把我们的代码放到根路径,而要自定义一个 BOOT-INF 目录呢?

其他的可执行 jar 都是直接把他放在了打包的类路径下,这样就可以做到既可以执行又可以引用,那么 SpringBoot 为什么这样做呢,他的奥秘就在于 MANIFEST.MF 文件中的 Main-Class 配置的并不是我们的主启动类,而是 JarLauncher 这个类,没有放在根路径的原因可能是害怕我们创建了一个相同类路径的类将其覆盖。

Main-Class: org.springframework.boot.loader.JarLauncher # 当使用 java -jar xxx.jar 命令启动的时候会调用这个类的 main 方法。

那么 JarLauncher 是做什么的呢?

Spring-Boot启动之前做了哪些事?

其他项目引用 SpringBoot 打成的 jar 包

我们对 spring-test 这个项目执行 maven clean install 命令,将其打包到 maven 仓库。

image
image

其他项目,通过 maven 引用这个 jar 包,调用它里面的 JsonUtils 工具类:

image

我们发现根本就调用不了这个方法(因为 SpringBoot 打包出来的 jar 包结构和正常的不一样,他把我们的代码放到了 BOOT-INF 目录下了)。

解决:一次打包两个 jar

一般来说,Spring Boot 直接打包成可执行 jar 就可以了,不建议将 Spring Boot 作为普通的 jar 被其他的项目所依赖。如果有这种需求,建议将被依赖的部分,单独抽出来做一个普通的 Maven 项目,然后在 Spring Boot 中引用这个 Maven 项目。

如果非要将 Spring Boot 打包成一个普通 jar 被其他项目依赖,技术上来说,也是可以的,给 spring-boot-maven-plugin 插件添加如下配置:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <classifier>exec</classifier>
    </configuration>
</plugin>

配置的 classifier 表示可执行 jar 的名字,配置了这个之后,在插件执行 repackage 命令时,就不会给 mvn package 所打成的 jar 重命名了,所以,打包后的 jar 如下:

image

我们执行 maven clean install 将其打包到仓库:

image

这个时候其他项目引用他就可以调用它里面的方法了:

image

为啥 MAIN-CLASS 是 XXXLauncher?

XXXLauncher 在调用我们自己的主类之前做了以下三件事:

  1. 扩展JDK默认的支持JAR对应的协议,因为Spring Boot启动不仅仅需要JDK的JAR文件,还需要BOOT-INF/lib这个目录下的文件。默认实现无法将BOOT-INF/lib这个目录当作ClassPath,故需要替换实现。
  2. 判断当前的介质,是java -jar启动,还是java org.springframework.boot.loader.JarLauncher启动。以便获取对应的ClassLoader。
  3. 获取MANIFEST.MF文件中的Start-Class属性,也就是我们自定义的主类。通过第二步获取的ClassLoader加载获取到Class文件,通过反射调用main方法,启动应用。

项目打成 war 包的结构

image

我们发现他打出来的结构中多了一个 org 目录,通过上面的学习我们知道这个目录里放的是启动器(Launcher)相关的类,而启动器又是在 MANIFEST.MF 文件中配置的,所以我们看下:

Manifest-Version: 1.0
Implementation-Title: spring-test
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: cn.x5456.springtest.SpringTestApplication
Spring-Boot-Classes: WEB-INF/classes/
Spring-Boot-Lib: WEB-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.2.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.WarLauncher # 与打成 jar 包的相比,这里换成了 WarLauncher

Spring 官方解释这样做的目的是:打包一个又能发布于 tomcat 又能 java -jar 直接跑的war。

image

JarLauncher 与 WarLauncher 区别

差别仅在于,JarLauncher在构建LauncherURLClassLoader时,会搜索BOOT-INF/classes目录及BOOT-INF/lib目录下jar,WarLauncher在构建LauncherURLClassLoader时,则会搜索WEB-INFO/classes目录及WEB-INFO/lib和WEB-INFO/lib-provided两个目录下的jar

参考文章

Spring Boot 打包成的可执行 jar ,为什么不能被其他项目依赖?

聊一聊 JAR 文件和 MANIFEST.MF

Spring-Boot启动之前做了哪些事?

下面的这篇文章,等学懂了 JVM 类加载机制再回来看

springboot应用启动原理(二) 扩展URLClassLoader实现嵌套jar加载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容