[TOC]java
本文会探究下SpringBoot的启动原理。SpringBoot在打包的时候会将依赖包也打进最终的Jar,变成一个可运行的FatJar。也就是会造成一个Jar in Jar的结构。默认状况下,JDK提供的ClassLoader只能识别Jar中的class文件以及加载classpath下的其余jar包中的class文件。对于在jar包中的jar包是没法加载的。算法
java中描述资源常使用URL。而URL有一个方法用于打开连接java.net.URL#openConnection()
。因为URL用于表达各类各样的资源,打开资源的具体动做由java.net.URLStreamHandler
这个类的子类来完成。根据不一样的协议,会有不一样的handler实现。而JDK内置了至关多的handler实现用于应对不一样的协议。好比jar
、file
、http
等等。URL内部有一个静态HashTable
属性,用于保存已经被发现的协议和handler实例的映射。spring
得到URLStreamHandler
有三种方法数组
URLStreamHandlerFactory
接口,经过方法URL.setURLStreamHandlerFactory
设置。该属性是一个静态属性,且只能被设置一次。URLStreamHandler
的子类,做为URL的构造方法的入参之一。可是在JVM中有固定的规范要求:
java.protocol.handler.pkgs
系统属性,若是有多个实现类,那么中间用 | 隔开。由于JVM在尝试寻找Handler时,会从这个属性中获取包名前缀,最终使用包名前缀.协议名.Handler
,使用Class.forName
方法尝试初始化类,若是初始化成功,则会使用该类的实现做为协议实现。SpringBoot定义了一个接口用于描述资源,也就是org.springframework.boot.loader.archive.Archive
。该接口有两个实现,分别是org.springframework.boot.loader.archive.ExplodedArchive
和org.springframework.boot.loader.archive.JarFileArchive
。前者用于在文件夹目录下寻找资源,后者用于在jar包环境下寻找资源。而在SpringBoot打包的fatJar中,则是使用后者。maven
SpringBoot使用插件spring-boot
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.tccdemo.Eureka</mainClass> </configuration> </plugin>
进行打包,打包后的文件布局以下:工具
来看描述文件MANIFEST.MF
的内容布局
Manifest-Version: 1.0 Implementation-Title: eureka Implementation-Version: 1.0-SNAPSHOT Built-By: Administrator Implementation-Vendor-Id: com.tccdemo Spring-Boot-Version: 2.0.2.RELEASE Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.tccdemo.Eureka Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Created-By: Apache Maven 3.6.1 Build-Jdk: 1.8.0_201 Implementation-URL: http://www.example.com
最为显眼的就是程序的启动类并非咱们项目的启动类,而是SpringBoot的JarLauncher
。下面会来深究下这个类的做用。性能
首先来看启动方法ui
public static void main(String[] args) throws Exception { new JarLauncher().launch(args); }
JarLauncher
继承于org.springframework.boot.loader.ExecutableArchiveLauncher
。该类的无参构造方法最主要的功能就是构建了当前main方法所在的FatJar的JarFileArchive
对象。下面来看launch方法。该方法主要是作了2个事情:
MANIFEST.MF
文件中Start-Class
指向的业务类,而且执行静态方法main。进而启动整个程序。经过静态方法org.springframework.boot.loader.JarLauncher#main
就能够顺利启动整个程序。这里面的关键在于SpringBoot自定义的classLoader可以识别FatJar中的资源,包括有:在指定目录下的项目编译class、在指令目录下的项目依赖jar。JDK默认用于加载应用的AppClassLoader只能从jar的根目录开始加载class文件,而且也不支持jar in jar这种格式。
为了实现这个目标,SpringBoot首先从支持jar in jar中内容读取作了定制,也就是支持多个!/
分隔符的url路径。SpringBoot定制了如下两个方面:
java.net.URLStreamHandler
的子类org.springframework.boot.loader.jar.Handler
。该Handler支持识别多个!/
分隔符,而且正确的打开URLConnection
。打开的Connection是SpringBoot定制的org.springframework.boot.loader.jar.JarURLConnection
实现。java.net.JarURLConnection
的子类org.springframework.boot.loader.jar.JarURLConnection
。该连接支持多个!/
分隔符,而且本身实现了在这种状况下获取InputStream的方法。而为了可以在org.springframework.boot.loader.jar.JarURLConnection
正确获取输入流,SpringBoot自定义了一套读取ZipFile的工具类和方法。这部分和ZIP压缩算法规范紧密相连,就不深刻了。可以读取多个!/
的url后,事情就变得很简单了。上文提到的ExecutableArchiveLauncher
的launch
方法会以当前的FatJar构建一个JarFileArchive
,而且经过该对象获取其内部全部的资源URL,这些URL包含项目编译class和依赖jar包。在构建这些URL的时候传入的就是SpringBoot定制的Handler。将获取的URL数组做为参数传递给自定义的ClassLoaderorg.springframework.boot.loader.LaunchedURLClassLoader
。该ClassLoader继承自UrlClassLoader。UrlClassLoader加载class就是依靠初始参数传入的Url数组,而且尝试Url指向的资源中加载Class文件。有了自定义的Handler,再从Url中尝试获取资源就变得很容易了。
至此,SpringBoot自定义的ClassLoader就可以加载FatJar中的依赖包的class文件了。
SpringBoot提供了一个很好的思路,可是其内部实现很是复杂,特别是其自行实现了一个ZipFIle的解析器。可是本质上这些背后的工做都是为了可以读取到FatJar内部的Jar的class文件资源。也就是只要有办法可以读取这些资源其实就能够实现加载Class文件了。而依靠JDK自己提供的JarFile其实就能够作到了。而读取到全部资源后,自定义一个ClassLoader加载读取到二进制数据进而定义Class对象并非很难的项目实现。固然,SpringBoot定制的Zip解析能够在加载类阶段避免频繁的文件解压动做,在性能上良好一些。
文章原创首发于公众号:林斌说Java,转载请注明来源,谢谢。
欢迎扫码关注