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

2021-12-07 16:14:53 浏览数 (1)

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

jar或war包中 META-INFMAINIFEST.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:

代码语言:javascript复制
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:

代码语言:javascript复制
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)

代码语言:javascript复制
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)

代码语言:javascript复制
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 的事件

0 人点赞