Spring boot 项目打出来的包启动过程

3
(1)

spring boot 的工程支持打包为jar和war,打包成 jar 或 war 可以直接用 java -jar xxx.jar 来启动,war包也可以放入tomcat等容器中运行。

jar或war包中 META-INF\MAINIFEST.MF 中定义的Main-Class指定的类为启动类。

在spring boot项目中,spring boot 提供 为 maven 和 gradle 分别提供了插件增加 repackage 的goal,用于打出 executable 的 fat jar,这个jar包除了包含了我们的项目编译后的代码和所需的依赖包以外,还有spring-boot-loader 的一些类用于提供类加载器和启动我们自己的main方法,内嵌的依赖jar不需要解压缩和将所有的类都读入内存。

因为 application.properties 、application.yml中使用${...}这样的占位符引用,所以为了避免与maven的变量冲突,maven打包的参数占位符为改为了@..@

包结构

jar:

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

war:

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

例子 jar:

war:

MANIFEST.MF 内容(JAR)

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: demoJar
Implementation-Version: 0.0.1-full-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.example.demojar.DemoJarApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.5.5
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

Main-Class 是 spring 的org.springframework.boot.loader.JarLauncher 类,Start-Class我们自己的启动类。

MAIIFEST.MF (WAR)

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: WEB-INF/classpath.idx
Implementation-Title: demoWar
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: WEB-INF/layers.idx
Start-Class: com.example.demowar.DemoWarApplication
Spring-Boot-Classes: WEB-INF/classes/
Spring-Boot-Lib: WEB-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.5.5
Created-By: Maven WAR Plugin 3.3.2
Main-Class: org.springframework.boot.loader.WarLauncher

Main-Class 是 spring 的org.springframework.boot.loader.WarLauncher 类,Start-Class 我们自己的启动类。

war 结构的 fat jar 下面的WEB-INF下多出来一个 lib-provided 目录用来防 embed tomcat 的 jar

Spring-Boot-Classpath-Index 的作用:这个文件记录了我们的依赖包的路径,但是这个配置,只有在以展开后的运行方式中才会使用。


Spring-Boot-Layers-Index 的作用: 用于创建 OCI(Open Container Initiative)Image的时候,分层用,想了解的同学,可以去研究下 mvn spring-boot:build-image 内部实现


那么还有一个Spring-Boot-Layers-Index 是做什么的呢,它指定的路径是 BOOT-INF/layers.idx ,这个也捎带的说一下,这个文件是在将 spring boot 的应用 使用 man spring-boot:build-image 打包容器镜像的时候的层级定义文件,因为容器中文件系统是多层级的,docker 从 registry 中 pull image 的时候也是按层获取,分成多层以后,就可以避免最基本的那些文件占用多份磁盘空间,更重要的是可以加快部署的速度,因为只需要从registry拉取变动的层的文件。
默认构建docker镜像不会将我们的fat jar 分成多层,要分成多层需要在spring-boot-maven-plugin 插件里开启 configuration > layers > enabled=true
不分层的时候就是一个fat jar 放到容器中,如果是分层后,就会将fat jar 中的文件根据此 layers.idx 中 定义,提取各层的文件,然后从底层到高层分四次加入到 Image 镜像


提取的命令 java -Djarmode=layertools -jar application.jar extract


可以使用dive命令分析Image每一层加入了哪些文件: dive docker.io/library/demojar:0.0.1-SNAPSHOT

dive 可以 使用 brew 或者 apt 、yum 等工具安装


如果对分层镜像这部分内容感兴趣可以看这个文章: https://reflectoring.io/spring-boot-docker/

Launcher 类层级:

JarLauncher默认构造函数实现是空的,它父类ExecutableArchiveLauncher构造函数会调用再上一级父类Launcher的 createArchive方法创建了demojar.jar的一个JarFileArchive实例。
ExecutableArchiveLauncher的launch方法,调用 getClassPathArchivesIterator() 方法扫描zip包entries,创建内部 archive,包括BOOT-INF/class和和BOOT-INF/lib下的jar包对应的archive对象。
这些 JarArchive 有一个 getUrl() 的方法,返回了 URI 对象,这个URI对象创建的时候给了 Handler,所以当 LaunchedURIClassLoader加载这些URI指定的类的时候,就会通过spring扩展的 URLStreamHandler 的 Handler 来进行类的加载,当然这个扩展的Handler 会使用spring boot loader 扩展的 JarUrlConnection来从jar中获取输入流。

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。这些ClassLoader在加载类的时候,首先会询问父级有没有找到这个类,如果父级有自己就不找了。
JarLauncher 的执行main方法,是 AppClassLoader 作为ClassLoader 执行的,默认情况下,ClassLoader会使用调用者所使用的的ClassLoader 去加载使用到的类,但是我们自己的Application类并不在默认的AppClassLoader范围内,所以在调用我们的Application的main方法的时候,需要用能够读取到那些类的LaunchedURLClassLoader,所以执行前,我们会看到,有一个切换ClassLoader 的动作。
spring 的 LaunchedURLClassLoader 继承的是 URLClassLoader,本身是一种基于URL的类加载器,每个类都有一个URL,原理就是使用URL去加载类的byte数组,然后转换成类对象。那我们看一下 spring boot loader 为我们扩展了哪些与类加载有关的功能,来支持jar中jar的类加载。
URLClassLoader 中有一个 URLClassPath对象,里面保存了每一个jar的loader对象。

Archive 是对 spring boot jar 中资源的封装的接口,有两个实现类:

org.springframework.boot.loader.archive.ExplodedArchive 表示目录资源时使用org.springframework.boot.loader.archive.JarFileArchive 表示Jar文件资源时使用

jar in jar 路径识别:
org.springframework.boot.loader.jar.JarURLConnection 支持jar中jar 和其中类的URL获取输入流
org.springframework.boot.loader.jar.Handler 用URL获取到JarURLConnection

jar in jar 文件读取:
org.springframework.boot.loader.jar.JarFile JarFile扩展支持获取内部NestedArchive
org.springframework.boot.loader.data.RandomAccessDataFile 从指定位置读取文件

类加载器:
org.springframework.boot.loader.LaunchedURLClassLoader 加载第一层jar中类和嵌套jar的类加载的ClassLoader

普通 JAR 中资源的URL格式:

A Class in jar
jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class


A Jar file
jar: http://www.foo.com/bar/baz.jar!/


A Jar directory
jarhttp://www.foo.com/bar/baz.jar!/COM/foo

spring boot jar 资源URL格式:

URL for a class in jar
file:/demojar-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/com/example/demojar/DemojarApplication.class

zip 文件尾部有 Central Directory ,里面记录了entry的名称和起点位置(偏移量), Entry 的 Local Header 区域保存了entry的大小。这样就可以定位到需要读取的字节。
zip 文件的 Central Directory 放到尾部,是为了zip文件修改的情况下,减少对zip文件改动成本。

如果想要查看类加载时,的细节可以调试 java.net.URLClassLoader#findClass
通过遍历loaders,也就是遍历每一个jar中是否存在 对应的 .class 文件,如果找到了返回Resource对象,Resource 中的 getByteBuffer() 会 调用的就是 spring boot loader 的 JarURLConnection的 getInputStream方法,然后JarURlConnection 会调用JarFile的getInputStream方法时传入jarEntry,用来获取class文件的输入流。看下代码的调用链

附件结构看,用都用了这部分信息:

获取 JarFileEntry 的 LocalHeaderOffset,也就拿到了 类文件数据的起始偏移量 和 类文件的大小,然后使用随机访问接口,获取到inputStream,因为 jar 中的 jar 没有压缩的,但是jar中jar里的类是压缩存储的,所以内部实现的时候给随机访问的inputStream又套了一层ZipInflatorInputStream。

最终读取 bytes 的代码可以 调试 sun.misc.Resource#getBytes ,有了byte[]之后,使用java.lang.ClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.ProtectionDomain) 来生成一个Class对象。

启动成功

fat jar 启动流程

1、new JarLauncher()的父构造函数中创建了 JarFileArchive 和 classPathIndex

2、筛选出Archive中的 Archive(BOOT-INF/classes目录和 BOOT-INF/lib下的每个jar ,如果是war包,则是WEB-INF/classes目录和WEB-INF/lib和WEB-INF/lib-provided下的每个jar):

3、创建LaunchedURLClassLoader:

4、设置当前线程的类加载器为上面的LaunchedURLClassLoader类加载器(以前是AppClassLoader),然后执行我们的main方法

SpringApplication.run() 执行了些什么?

1、构造函数中,判断出应用类型和main方法的类、加载spring.factories文件,创建bootstrapRegistryInitializers(启动扩展点),初始化器和监听器实例,排序(Ordered接口)后分别放入list

2、SpringApplication.run 方法做的事情:

1、构造一个 DefaultBootstrapContext

2、创建 spring.factories中所有的 SpringApplicationRunListener

  • EventPublishingRunListener ,用来在各个阶段发送ApplicationEvent,这些event会被listeners处理。
  • 例如在contextLoaded的发生时,将会给实现了ApplicationContextAware接口的listeners设置context

3、向listerns发出 starting 的事件

4、创建和处理环境(profile、propertySource),向listeners 发出 environmentPrepared 的事件

4、输出Banner、创建 ApplicationContext 、准备应用上下文、刷新应用上下文 (spring 容器核心)

5、向 listeners 发出 started 的事件

6、调用所有的ApplicationRunner的实现和CommandLineRunner的实现

7、向 listeners 发出 running 的事件

8、如果发生异常向 listeners 发出 failed 的事件

这篇文章有用吗?

平均评分 3 / 5. 投票数: 1

到目前为止还没有投票!成为第一位评论此文章。

很抱歉,这篇文章对您没有用!

让我们改善这篇文章!

告诉我们我们如何改善这篇文章?


了解 工作生活心情记忆 的更多信息

Subscribe to get the latest posts sent to your email.