调试spring-boot-loader-2.7.15.jar学习SpringBoot Jar为什么可以直接启动

1、spring boot jar 直接启动的秘密

最近对springboot jar为什么可以直接运行很好奇,所以找了一些文章并下载了springbootloader相关的源码进行debug,观察spring 是如何在java jar标准之上,进行扩展,以使一个jar包可以直接运行的。简而言之。spring做了以下的工作:

  1. 在遵循java jar规范的前提下,定义了spring boot 可运行的jar包格式规范
  2. 因为spring boot jar 可以直接启动,所以它就必须要把所有的东西(启动类和依赖)打包在一个盒子里(fatjar)
  3. 为了支持解析jar中嵌套jar,就必须还要支持这种方式的解析-找到jar中jar里的class

下面分别对上诉的进行简单说明

1.1、 在遵循java jar规范的前提下,定义了spring boot 可运行的jar包格式规范

简而言之 spring boot 定义了自己的fat jar 的格式 如下

其中jar的原信息文件MANIFEST.MF内容如下:

makefile 复制代码
Manifest-Version: 1.0
Implementation-Title: spring-learn
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.vincent.learn.SpringLearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.5.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher

Java Jar规范是可以直接启动 Main-Class 对应的类的main方法,可以看到是 org.springframework.boot.loader.JarLauncher 这个类在 spring-boot- loader-2.xx.jar文件里(下面会详细介绍这个jar相关的内容)。

另外可以看到我们自己写的springboot项目的主启动类其实是Start-Class对应的类也就是com.vincent.learn.SpringLearnApplication 另外还有 Spring-Boot-Classes 以及 Spring-Boot-Lib 用来存放项目class 和 依赖的jar 文件。

所以可以看到springboot 的fat jar 需要有专门打包逻辑以及解包并寻找class的逻辑

1.2、 spring boot 打包fat jar逻辑

首先springboot提供了相应的maven 打包工具

执行maven clean package之后,会生成两个文件:

复制代码
spring-learn-0.0.1-SNAPSHOT.jar
spring-learn-0.0.1-SNAPSHOT.jar.original

spring-boot-maven-plugin项目存在于spring-boot-tools目录中。 spring-boot-maven-plugin默认有5个goals: repackage、 run、 start、 stop、 build-info。在打包的时候默认使用的是repackage。

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

1.3、对这种fat jar寻找class文件进行支持

我们知道原始的java的三种classLoader进行class文件装载时,通过UrlClassPath下的URL,如果是jar协议的话,是只支持单层jar内寻找的,如 java.net.URL#URL(java.lang.String, java.lang.String, int, java.lang.String, java.net.URLStreamHandler) 中,遇到多层会直接抛出异常,所以

springboot专门针对形如 jar:file:/Users/vincent/github_project/spring-boot-debug/target/spring-boot-debug-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-2.7.15.jar!/ 格式定制了专用的URLStreamHandler解析器,也就是 org.springframework.boot.loader.jar.Handler , 同时为这种专用的解析器设计了专用的类加载器org.springframework.boot.loader.LaunchedURLClassLoader 用于加载 BOOT-INF 问价内的class和jar , 到此所有为启动Main方法的Class以准备好了环境。

  • 大家有没有想过一个问题,为什么不可以直接把Jar的Main-Class 设置成我们的业务启动主类,依赖的相关jar依然打成嵌入jar的方式,这样不就可以省了通过 org.springframework.boot.loader.JarLauncher#main 引导我们的业类进行启动了吗 ?
arduino 复制代码
public static void main(String[] args) throws Exception {  
new JarLauncher().launch(args);  
}

1.4 启动流程简单梳理

1、启动入口ork.boot.loader.JarLauncher#main

arduino 复制代码
public static void main(String[] args) throws Exception {
		new JarLauncher().launch(args);
	}      

2、在JarLauncher的父类org.springframework.boot.loader.ExecutableArchiveLauncher#ExecutableArchiveLauncher() 构造函数中对获取了当前启动jar包的Archive-其实就是我们的package之后的Fat Jar

csharp 复制代码
public ExecutableArchiveLauncher() {  
try {  
this.archive = createArchive();  
this.classPathIndex = getClassPathIndex(this.archive);  
}  
catch (Exception ex) {  
throw new IllegalStateException(ex);  
}  
}

3、接着JarLauncher#launch 方法进行

  • 设置Jar protocol Handler 覆盖 java 默认提供的 handler
  • 遍历Fat Jar中 BOOT-INT/classes 和 BOOT-INF/lib 下所有的资源 分别构造 JarFileive , 并把所有的JarFileArchive的URL 收集起来作为自定义ClassLoader的UrlClassPath ,然后创建自定义的ClassLoader
  • 在RootArchive - 也就是我们最初打的Fat Jar的MANIFEST.MF 文件里找到Start-Class 对应的业务主启动类字符串
  • 通过第二步创建的自定义ClassLoader来加载主业务启动类,并进行反射执行其main方法,完整jar的启动
ini 复制代码
protected void launch(String[] args) throws Exception {  
if (!isExploded()) {  
JarFile.registerUrlProtocolHandler();  
}  
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());  
String jarMode = System.getProperty("jarmode");  
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();  
launch(args, launchClass, classLoader);  
}

好了以上就是整个spring boot fat jar 启动的流程和设计思路了,当然上面只是一个比较粗线条的框架的梳理,其实整个设计过程中,涉及了很多的知识点,包括 ClassLoader 类加载 和URL 的协议解析,还有标准Jar文件 以及 Spring Jar 文件的流读取机制 等,这些都是很考验对于Java语言的底层机制的理解,有很深挖的意义和价值 。

1.4、如何验证上面Jar的启动流程 - debug SpringBot Fat Jar

网上有很多解析源码的文章,但是代码真正运行起来到底是什么样的,如果能debug一下整个运行流程 那就更能加深对于一些底层运行机制的理解了。Idea提供了Debug SpringBoot Fat Jar的机制。如下图配置就ok,主要就是设置打包好的Fat Jar的路径以及源码项目路径

遇到的问题

下面就是我在debug的过程中遇到一个当时觉得很疑惑的问题

在遍历获取fat jar 内部的 archive (先拿到JarEntry再构造成JarFile再构造成JarFileArchive)的过程中,发现构造JarFile 时候其内部 url属性是null, 但是执行完return new JarFileArchive(jarFile); 返回之后 JarFile里的url就被赋值了, 但是一直没有找到触发这个赋值url属性的地方,打的各种断点都没有进去, 为什么对这个URL的赋值这么在意,是因为这个URL就是最终要作为自定义ClassLoader的ClassPath传入的,而且这个URL中包含了解析用的Handler,我要验证下对于嵌入Jar类的URL的Handler是如何使用SpringBoot自定义的Handler 调用路径如下

bash 复制代码
org.springframework.boot.loader.archive.JarFileArchive.
NestedArchiveIterator#adapt
    ->  org.springframework.boot.loader.archive.JarFileArchive
    #getNestedArchive 
        -> 

一个很明显的JarFile#url赋值逻辑是在 org.springframework.boot.loader.jar.JarFile#getUrl

但是这个断点处的if分支一直没有进去,每次都是直接return一个已经初始化好的url,但是url怎么赋值的一直没找到(其实赋值逻辑就是if分支的逻辑)但是一直不进入而是直接返回

经过不断来回debug,在过程中idea弹出了一条提示消息

Skipped breakpoint at org.springframework.boot.loader.archive.JarFileArchive:192 because it happened inside debugger evaluation 看起来idea 在debug的过程中跳过了一些断点,看起来很可能是这个原因导致没有走上面的if语句,所以只要能关于idea的相关的跳过一些断点应该就能解决了。在stackoverflow有相关的解决方案

原因是idea在调试的时候,在debug控制台中如果点击相应的Class并展示其信息的时候其实是会调用其对应的toString()方法,而JarFileArchive#toString方法如下

JarFileArchive的toString()导致了JarFile#getUrl()方法的if逻辑的执行 算是学到了一个idea debug的知识点吧

参考

mp.weixin.qq.com/s/ikx8Ix93f... juejin.cn/post/684490...

相关推荐
uzong15 分钟前
后端系统设计文档模板
后端
幽络源小助理19 分钟前
SpringBoot+Vue车票管理系统源码下载 – 幽络源免费项目实战代码
vue.js·spring boot·后端
uzong1 小时前
软件架构指南 Software Architecture Guide
后端
又是忙碌的一天1 小时前
SpringBoot 创建及登录、拦截器
java·spring boot·后端
勇哥java实战分享2 小时前
短信平台 Pro 版本 ,比开源版本更强大
后端
学历真的很重要2 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
计算机毕设VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
上进小菜猪2 小时前
基于 YOLOv8 的智能杂草检测识别实战 [目标检测完整源码]
后端
韩师傅3 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端
栈与堆4 小时前
LeetCode-1-两数之和
java·数据结构·后端·python·算法·leetcode·rust