Spring Boot 启动原理思维导图
SpringBoot 如何通过 jar 启动?
使用 -jar 参数时,后面的参数是 jar 文件的名字;该 jar 文件中包含的是 class 和资源文件;在 mainfest 文件中有 Main-Class 的定义;Main-Class 指定了整个应用的启动类;
Start-Class 指定的是代码中的唯一类,也是真正的应用启动类
jar 包目录结构:
spring-boot-learn-0.0.1-SNAPSHOT
├── META-INF
│ └── MANIFEST.MF
├── BOOT-INF
│ ├── classes
│ │ └── 应用程序类
│ └── lib
│ └── 第三方依赖jar
└── org
└── springframework
└── boot
└── loader
└── springboot启动程序
META-INF 内容:
Manifest-Version: 1.0
Implementation-Title: spring-learn
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.zrk.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.5.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
Archive 概念:
- archive 即归档文件,在 linux 下比较常见
- 通常就是一个 tar/zip 格式的压缩包
- jar 是 zip 格式
SpringBoot 抽象了 Archive 的概念;一个 Archive 可以是 jar,可以是一个文件目录,可以抽象为统一访问资源的逻辑层;源码如下:
public interface Archive extends Iterable<Archive.Entry> {
// 获取该归档的url
URL getUrl() throws MalformedURLException;
// 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
Manifest getManifest() throws IOException;
// 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}
SpringBoot jar 应用启动流程解析
- SpringBoot 应用打包后,生成一个 Fat jar,包含了应用依赖的 jar 包和 SpringBoot loader 相关的类
- Fat jar 的启动 Main 函数是 JarLauncher,它负责创建一个 LaunchedURLClassLoader 来加载/lib 下面的 jar,并以一个新线程启动应用的 Main 函数
那么,ClassLoader 是如何读取到 Resource,又有哪些能力? 查找资源和读取资源的能力;对应 api
public URL findResource(String name)
public InputStream getResourceAsStream(String name)
SpringBoot 构造 LaunchedURLClassLoader 时,传递了一个 URL[] 数组,数组里是 lib 目录下的 jar 的 URL;对于一个 URL,JDK 或者 ClassLoader 读取其内容流程如下:
- LaunchedURLClassLoader.loadClass
- URL.getContent()
- URL.openConnection()
- Handler.openConnection(URL)
最终调用的是 JarURLConnection 的 getInputStream() 函数:
//org.springframework.boot.loader.jar.JarURLConnection
@Override
public InputStream getInputStream() throws IOException {
connect();
if (this.jarEntryName.isEmpty()) {
throw new IOException("no entry name specified");
}
return this.jarEntryData.getInputStream();
}
从一个 URL,到最终读取到 URL 里的内容,整个过程是比较复杂的:
- SpringBoot 注册了一个 Handler 来处理”jar:”这种协议的 URL
- SpringBoot 扩展了 JarFile 和 JarURLConnection,内部处理 jar in jar 的情况
- 在处理多重 jar in jar 的 URL 时,SpringBoot 会循环处理,并缓存已经加载到的 JarFile
- 对于多重 jar in jar,实际上是解压到了临时目录来处理
- 在获取 URL 的 InputStream 时,最终获得到的是 JarFile 里的 JarEntryData
URLClassLoader 是如何 getResource 的呢?
URLClassLoader 在构造时,有 URL[] 数组参数,它内部会用这个数组来构造一个 URLClassPath:URLClassPath ucp = new URLClassPath(urls);
在 URLClassPath 内部会为这些 urls 都构造一个 Loader,然后在 getResource 时会重这些 Loader 里一个个去尝试获取;若获取成功,则包装为一个 Resource:
Resource getResource(final String name, boolean check) {
final URL url;
try {
url = new URL(base, ParseUtil.encodePath(name, false));
} catch (MalformedURLException e) {
throw new IllegalArgumentException("name");
}
final URLConnection uc;
try {
if (check) {
URLClassPath.check(url);
}
uc = url.openConnection();
InputStream in = uc.getInputStream();
if (uc instanceof JarURLConnection) {
/* Need to remember the jar file so it can be closed
* in a hurry.
*/
JarURLConnection juc = (JarURLConnection)uc;
jarfile = JarLoader.checkJar(juc.getJarFile());
}
} catch (Exception e) {
return null;
}
return new Resource() {
public String getName() { return name; }
public URL getURL() { return url; }
public URL getCodeSourceURL() { return base; }
public InputStream getInputStream() throws IOException {
return uc.getInputStream();
}
public int getContentLength() throws IOException {
return uc.getContentLength();
}
};
}
JarURLConnection juc = (JarURLConnection)uc;