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