Spring Boot jar包启动分析
首先,为了了解Spring Boot Jar包的启动情况,我们需要构建一个Spring的FAT jar 看看其中都有哪些东西。
解压Spring Boot Maven Plugin 打包的jar
Spring Boot项目打包后,通过mvn package方式,可以看到在代码的target目录下,生成了一个jar文件和一个jar.original文件。这两个文件有什么区别呢?
- jar文件:这个是通过SpringBootMavenPlugin插件重新包装出来的,他里面包含了我们项目中所依赖的所有jar包文件
- jar.original文件:这个是通过maven打包后最原始的文件,里面没有项目中所以来的jar包,因此这个文件通常比较小
我们通过比较解压后的文件结构来了解下两种jar的区别。
jar 文件
├─BOOT-INF
│ │ classpath.idx
│ │
│ ├─classes
│ │ │ application.yml
│ │ │
│ │ └─com
│ │ └─chillax
│ │ │ Application.class
│ │ │
│ │ └─controller
│ │ HelloController.class
│ │
│ └─lib
│ hutool-all-5.3.8.jar
│ .............jar
│
├─META-INF
│ │ MANIFEST.MF
│ │
│ └─maven
│ └─com.chillax
│ └─spring-boot-loader
│ pom.properties
│ pom.xml
│
└─org
└─springframework
└─boot
└─loader
│ ClassPathIndexFile.class
│ ...........class
│
├─archive
│ Archive$Entry.class
│ ..........class
├─data
│ ..........class
│
├─jar
│ AsciiBytes.class
│ .......class
│
├─jarmode
│ JarMode.class
│ ............class
│
└─util
SystemPropertyUtils.class
- BOOT-INF/classes 目录存放应用编译后的class文件
- BOOT-INF/lib 目录存放应用以来的jar包
- MEA-INF目录存放应用的相关元信息。
- org目录存放Spring Boot相关classs
jar.original 文件
│ application.yml
│
├─com
│ └─chillax
│ │ Application.class
│ │
│ └─controller
│ HelloController.class
│
└─META-INF
│ MANIFEST.MF
│
└─maven
└─com.chillax
└─spring-boot-loader
pom.properties
pom.xml
我们可以看到内容比较少,主要是少了,依赖的jar和Spring Boot相关classes。因为这个源文件会被Spring Boot的Maven插件重新打包,形成可运行的Spring Boot FatJar
FAT JAR执行模块-spring-boot-loader
当使用java -jar 命令时,按照官方文档提供的标准,默认的启动类必须要在/META-INF/MANIFEST.MF文件中指明启动的类。
jar 文件官方规范 参考这个链接 https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html
我们来看一下,Spring Boot项目中生成的清单文件是什么样子的
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: chillax
Start-Class: com.chillax.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.3.1.RELEASE
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_251
Main-Class: org.springframework.boot.loader.JarLauncher
我们可以看到项目的相关元信息。可以看到Main-Class被插件包装为了JarLauncher。并且指定的Start-class为我们的最常见的SpringApplication写的入口类。那我们所得到的信息是,Spring Boot 通过打包插件,讲项目的元信息进行再次封装后,通过spring-boot-loader#JarLauncher
类启动我们的Spring Boot的入口类,从而实现spring boot 项目的jar启动。
spring-boot-loader查看
这里面类的层级关系,我们可以通过idea查看到
一般情况下,我们的FatJAR再我们执行java -jar 后,会执行JarLauncher#main
方法,然后通过JarLauncher的launch
方法启动我们的应用。
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
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);
}
launch方法主要是执行了三部操作
-
注册自己的Jar协议解析器
-
加载自己的类加载器,用于加载自身classes和依赖的classes
Archive
SpringBoot定义了一个描述资源的接口
org.springframework.boot.loader.archive.Archive
。org.springframework.boot.loader.archive.ExplodedArchive
- 用于在文件夹目录下寻找资源
org.springframework.boot.loader.archive.JarFileArchive
。- 用于在jar包环境下寻找资源。
而在SpringBoot打包的fatJar中,则是使用后者。
这里主要是解析Spring自己抽象的资源类,将所有关联的class加载
-
launch方法利用反射,启动MANIFEST.MF文件中指定的start-class的Main方法
为什么要注册自己的Jar协议解析器呢?
因为,默认情况下,JDK提供的ClassLoader只能识别Jar中的class文件以及加载classpath下的其他jar包中的class文件。对于在jar包中的jar包是无法加载的。
Spring Boot是如何重写协议解析器
首先,在java中描述一种资源,通常使用URL,与URL关联的协议通常有一个URLStreamHandler
的实现类,
在JDK中可以找到file、http、jar的内置协议,这些实现类在sum.net.www.protocol
下且类名必须为Handler
。
这些处理器的读取规则有如下几种:
- 实现
URLStreamHandlerFactory
接口,通过方法URL.setURLStreamHandlerFactory
设置。该属性是一个静态属性,且只能被设置一次。 - 直接提供
URLStreamHandler
的子类,作为URL的构造方法的入参之一。但是在JVM中有固定的规范要求: - 子类的类名必须是 Handler ,同时最后一级的包名必须是协议的名称。比如自定义了Http的协议实现,则类名必然为xx.http.Handler
- JVM 启动的时候,需要设置
java.protocol.handler.pkgs
系统属性,如果有多个实现类,那么中间用 | 隔开。因为JVM在尝试寻找Handler时,会从这个属性中获取包名前缀,最终使用包名前缀.协议名.Handler
,使用Class.forName
方法尝试初始化类,如果初始化成功,则会使用该类的实现作为协议实现。
因此,Spring Boot为了能够加载到当前jar中的class文件以及classpath下及其jar包下所有的class文件,spring官方扩展了URLStreamHandler
实现了自己的jar读取协议该处理类在org.springframework.boot.loader.jar.Handler
这里,感兴趣的可以去看看源码是怎么实现协议的扩展的。
来源:oschina
链接:https://my.oschina.net/u/3226414/blog/4355279