Spring Boot 源码分析 - Jar 包的启动实现
参考 知识星球 中 芋道源码 星球的源码解析,一个活跃度非常高的 Java 技术社群,感兴趣的小伙伴可以加入 芋道源码 星球,一起学习😄
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读
Spring Boot 版本:2.2.x
最好对 Spring 源码有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章导读》 系列文章
如果该篇内容对您有帮助,麻烦点击一下“推荐”,也可以关注博主,感激不尽 ~
该系列其他文章请查看:《精尽 Spring Boot 源码分析 - 文章导读》
概述
Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin
,可以很方便的将我们的 Spring Boot 项目打成 jar
包或者 war
包。
考虑到部署的便利性,我们绝大多数(99.99%)的场景下,都会选择打成 jar
包,这样一来,我们就无需将项目部署于 Tomcat、Jetty 等 Servlet 容器中。
那么,通过 Spring Boot 插件生成的 jar
包是如何运行,并启动 Spring Boot 应用的呢?这个就是本文的目的,我们一起来弄懂 Spring Boot jar
包的运行原理。
这里,我通过 Spring Boot Maven Plugin 生成了一个 jar 包,其里面的结构如下所示:

BOOT-INF
目录,里面保存了我们自己 Spring Boot 项目编译后的所有文件,其中classes
目录下面就是编译后的 .class 文件,包括项目中的配置文件等,lib
目录下就是我们引入的第三方依赖META-INF
目录,通过MANIFEST.MF
文件提供jar
包的元数据,声明jar
的启动类等信息。每个 Javajar
包应该是都有这个文件的,参考 Oracle 官方对于jar
的说明,里面有一个Main-Class
配置用于指定启动类org.springframework.boot.loader
目录,也就是 Spring Boot 的spring-boot-loader
工具模块,它就是java -jar xxx.jar
启动 Spring Boot 项目的秘密所在,上面的Main-Class
指定的就是该工具模块中的一个类
MANIFEST.MF
META-INF/MANIFEST.MF
文件如下:
Manifest-Version: 1.0
Implementation-Title: spring-boot-study
Implementation-Version: 1.0.0-SNAPSHOT
Built-By: jingping
Implementation-Vendor-Id: org.springframework.boot.demo
Spring-Boot-Version: 2.0.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher # spring-boot-loader 中的启动类
Start-Class: org.springframework.boot.demo.Application # 你的 Spring Boot 项目中的启动类
Spring-Boot-Classes: BOOT-INF/classes/ # 你的 Spring Boot 项目编译后的 .class 文件所在目录
Spring-Boot-Lib: BOOT-INF/lib/ # 你的 Spring Boot 项目所引入的第三方依赖所在目录
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_251
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/info-dependencies/dwzq-info/info-stock-project/sp-provider
参考 Oracle 官方对该的说明:
Main-Class
:Java 规定的jar
包的启动类,这里设置为spring-boot-loader
项目的 JarLauncher 类,进行 Spring Boot 应用的启动Start-Class
:Spring Boot 规定的主启动类,这里通过 Spring Boot Maven Plugin 插件打包时,会设置为我们定义的 Application 启动类
为什么不直接将我们的 Application 启动类设置为 Main-Class
启动呢?
因为通过 Spring Boot Maven Plugin 插件打包后的
jar
包,我们的 .class 文件在BOOT-INF/classes/
目录下,在 Java 默认的jar
包加载规则下找不到我们的 Application 启动类,也就需要通过 JarLauncher 启动加载。当然,还有一个原因,Java 规定可执行器的
jar
包禁止嵌套其它jar
包,在BOOT-INF/lib
目录下有我们 Spring Boot 应用依赖的所有第三方jar
包,因此spring-boot-loader
项目自定义实现了 ClassLoader 实现类 LaunchedURLClassLoader,支持加载BOOT-INF/classes
目录下的.class
文件,以及BOOT-INF/lib
目录下的jar
包。
接下来,我们一起来看看 Spring Boot 的 JarLauncher 这个类
1. JarLauncher
类图:

上面的 WarLauncher 是针对 war
包的启动类,和 JarLauncher 差不多,感兴趣的可以看一看,这里我们直接来看到 JarLauncher 这个类
public class JarLauncher extends ExecutableArchiveLauncher {
<span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">String</span> <span class="hljs-variable">BOOT_INF_CLASSES</span> <span class="hljs-operator">=</span> <span class="hljs-string">"BOOT-INF/classes/"</span>;
<span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">String</span> <span class="hljs-variable">BOOT_INF_LIB</span> <span class="hljs-operator">=</span> <span class="hljs-string">"BOOT-INF/lib/"</span>;
<span class="hljs-keyword">public</span> <span class="hljs-title function_">JarLauncher</span><span class="hljs-params">()</span> {
}
<span class="hljs-keyword">protected</span> <span class="hljs-title function_">JarLauncher</span><span class="hljs-params">(Archive archive)</span> {
<span class="hljs-built_in">super</span>(archive);
}
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">protected</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">isNestedArchive</span><span class="hljs-params">(Archive.Entry entry)</span> {
<span class="hljs-comment">// 只接受 `BOOT-INF/classes/` 目录</span>
<span class="hljs-keyword">if</span> (entry.isDirectory()) {
<span class="hljs-keyword">return</span> entry.getName().equals(BOOT_INF_CLASSES);
}
<span class="hljs-comment">// 只接受 `BOOT-INF/lib/` 目录下的 jar 包</span>
<span class="hljs-keyword">return</span> entry.getName().startsWith(BOOT_INF_LIB);
}
<span class="hljs-comment">/**
* 这里是 java -jar 启动 SpringBoot 打包后的 jar 包的入口
* 可查看 jar 包中的 META-INF/MANIFEST.MF 文件(该文件用于对 Java 应用进行配置)
* 参考 Oracle 官方对于 jar 的说明(https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html)
* 该文件其中会有一个配置项:Main-Class: org.springframework.boot.loader.JarLauncher
* 这个配置表示会调用 JarLauncher#main(String[]) 方法,也就当前方法
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> Exception {
<span class="hljs-comment">// <1> 创建当前类的实例对象,会创建一个 Archive 对象(当前应用),可用于解析 jar 包(当前应用)中所有的信息</span>
<span class="hljs-comment">// <2> 调用其 launch(String[]) 方法</span>
<span class="hljs-keyword">new</span> <span class="hljs-title class_">JarLauncher</span>().launch(args);
}
}
可以看到它有个 main(String[])
方法,前面说到的 META-INF/MANIFEST.MF
文件中的 Main-Class
配置就是指向了这个类,也就会调用这里的 main
方法,会做下面两件事:
-
创建一个
JarLauncher
实例对象,在ExecutableArchiveLauncher
父类中会做以下事情:public abstract class ExecutableArchiveLauncher extends Launcher {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Archive archive; <span class="hljs-keyword">public</span> <span class="hljs-title function_">ExecutableArchiveLauncher</span><span class="hljs-params">()</span> { <span class="hljs-keyword">try</span> { <span class="hljs-comment">// 为当前应用创建一个 Archive 对象,可用于解析 jar 包(当前应用)中所有的信息</span> <span class="hljs-built_in">this</span>.archive = createArchive(); } <span class="hljs-keyword">catch</span> (Exception ex) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IllegalStateException</span>(ex); } } <span class="hljs-keyword">protected</span> <span class="hljs-keyword">final</span> Archive <span class="hljs-title function_">createArchive</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception { <span class="hljs-comment">// 获取 jar 包(当前应用)所在的绝对路径</span> <span class="hljs-type">ProtectionDomain</span> <span class="hljs-variable">protectionDomain</span> <span class="hljs-operator">=</span> getClass().getProtectionDomain(); <span class="hljs-type">CodeSource</span> <span class="hljs-variable">codeSource</span> <span class="hljs-operator">=</span> protectionDomain.getCodeSource(); <span class="hljs-type">URI</span> <span class="hljs-variable">location</span> <span class="hljs-operator">=</span> (codeSource != <span class="hljs-literal">null</span>) ? codeSource.getLocation().toURI() : <span class="hljs-literal">null</span>; <span class="hljs-type">String</span> <span class="hljs-variable">path</span> <span class="hljs-operator">=</span> (location != <span class="hljs-literal">null</span>) ? location.getSchemeSpecificPart() : <span class="hljs-literal">null</span>; <span class="hljs-keyword">if</span> (path == <span class="hljs-literal">null</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IllegalStateException</span>(<span class="hljs-string">"Unable to determine code source archive"</span>); } <span class="hljs-comment">// 当前 jar 包</span> <span class="hljs-type">File</span> <span class="hljs-variable">root</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">File</span>(path); <span class="hljs-keyword">if</span> (!root.exists()) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IllegalStateException</span>(<span class="hljs-string">"Unable to determine code source archive from "</span> + root); } <span class="hljs-comment">// 为当前 jar 包创建一个 JarFileArchive(根条目),需要通过它解析出 jar 包中的所有信息</span> <span class="hljs-comment">// 如果是文件夹的话则创建 ExplodedArchive(根条目)</span> <span class="hljs-keyword">return</span> (root.isDirectory() ? <span class="hljs-keyword">new</span> <span class="hljs-title class_">ExplodedArchive</span>(root) : <span class="hljs-keyword">new</span> <span class="hljs-title class_">JarFileArchive</span>(root)); }
}
会为当前应用创建一个 Archive 对象,可用于解析
jar
包(当前应用)中所有的信息,可以把它理解为一个“根”对象,可以通过它获取我们所需要的类信息 -
调用
JarLauncher#launch(String[])
方法,也就是调用父类Launcher
的这个方法
2. Launcher
org.springframework.boot.loader.Launcher
,Spring Boot 应用的启动器
2. launch 方法
public abstract class Launcher {
<span class="hljs-comment">/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {<span class="hljs-doctag">@code</span> public static void main(String[] args)} method.
* <span class="hljs-doctag">@param</span> args the incoming arguments
* <span class="hljs-doctag">@throws</span> Exception if the application fails to launch
*/</span>
<span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">launch</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> Exception {
<span class="hljs-comment">// <1> 注册 URL(jar)协议的处理器</span>
JarFile.registerUrlProtocolHandler();
<span class="hljs-comment">// <2> 先从 `archive`(当前 jar 包应用)解析出所有的 JarFileArchive</span>
<span class="hljs-comment">// <3> 创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前 jar 中所有的类</span>
<span class="hljs-type">ClassLoader</span> <span class="hljs-variable">classLoader</span> <span class="hljs-operator">=</span> createClassLoader(getClassPathArchives());
<span class="hljs-comment">// <4> 获取当前应用的启动类(你自己写的那个 main 方法)</span>
<span class="hljs-comment">// <5> 执行你的那个 main 方法</span>
launch(args, getMainClass(), classLoader);
}
}
会做以下几件事:
- 调用
JarFile#registerUrlProtocolHandler()
方法,注册 URL(jar)协议的处理器,主要是使用自定义的 URLStreamHandler 处理器处理 jar 包 - 调用
getClassPathArchives()
方法,先从archive
(当前 jar 包应用)解析出所有的 JarFileArchive,这个archive
就是在上面创建JarLauncher
实例对象过程中创建的 - 调用
createClassLoader(List<Archive>)
方法,创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前jar
包中所有的类,包括依赖的第三方包 - 调用
getMainClass()
方法,获取当前应用的启动类(你自己写的那个main
方法所在的 Class 类对象) - 调用
launch(...)
方法,执行你的项目中那个启动类的main
方法(反射)
你可以理解为会创建一个自定义的 ClassLoader 类加载器,主要可加载 BOOT-INF/classes
目录下的类,以及 BOOT-INF/lib
目录下的 jar
包中的类,然后调用你 Spring Boot 应用的启动类的 main
方法
接下来我们逐步分析上面的每个步骤
2.1 registerUrlProtocolHandler 方法
备注:注册 URL(jar)协议的处理器
这个方法在 org.springframework.boot.loader.jar.JarFile
中,这个类是 java.util.jar.JarFile
的子类,对它进行扩展,提供更多的功能,便于操作 jar
包
public static void registerUrlProtocolHandler() {
// <1> 获取系统变量中的 `java.protocol.handler.pkgs` 配置的 URLStreamHandler 路径
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
// <2> 将 Spring Boot 自定义的 URL 协议处理器路径(`org.springframework.boot.loader`)添加至系统变量中
// JVM 启动时会获取 `java.protocol.handler.pkgs` 属性,多个用 `|` 分隔,以他们作为包名前缀,然后使用 ` 包名前缀. 协议名.Handler` 作为该协议的实现
// 那么这里就会将 `org.springframework.boot.loader.jar.Handler` 作为 jar 包协议的实现
System.setProperty(PROTOCOL_HANDLER,
("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
// <3> 重置已缓存的 URLStreamHandler 处理器们,避免重复创建
resetCachedUrlHandlers();
}
方法的处理过程如下:
-
获取系统变量中的
java.protocol.handler.pkgs
配置的 URLStreamHandler 路径 -
将 Spring Boot 自定义的 URL 协议处理器路径(
org.springframework.boot.loader
)添加至系统变量中JVM 启动时会获取
java.protocol.handler.pkgs
属性,多个用|
分隔,以他们作为包名前缀,然后使用包名前缀.协议名.Handler
作为该协议的实现那么这里就会将
org.springframework.boot.loader.jar.Handler
作为 jar 包协议的实现,用于处理 jar 包 -
重置已缓存的 URLStreamHandler 处理器们,避免重复创建
private static void resetCachedUrlHandlers() { try { URL.setURLStreamHandlerFactory(null); } catch (Error ex) { // Ignore } }
2.2 getClassPathArchives 方法
备注:从
archive
(当前 jar 包应用)解析出所有的JarFileArchive
该方法在 org.springframework.boot.loader.ExecutableArchiveLauncher
子类中实现,如下:
@Override
protected List<Archive> getClassPathArchives() throws Exception {
// <1> 创建一个 Archive.EntryFilter 类,用于判断 Archive.Entry 是否匹配,过滤 jar 包(当前应用)以外的东西
// <2> 从 `archive`(当前 jar 包)解析出所有 Archive 条目信息
List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
// <3> 返回找到的所有 JarFileArchive
// `BOOT-INF/classes/` 目录对应一个 JarFileArchive(因为就是当前应用中的内容)
// `BOOT-INF/lib/` 目录下的每个 jar 包对应一个 JarFileArchive
return archives;
}
过程如下:
-
创建一个
Archive.EntryFilter
实现类,用于判断Archive.Entry
是否匹配,过滤掉jar
包(当前应用)以外的东西public class JarLauncher extends ExecutableArchiveLauncher {
<span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">String</span> <span class="hljs-variable">BOOT_INF_CLASSES</span> <span class="hljs-operator">=</span> <span class="hljs-string">"BOOT-INF/classes/"</span>; <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">String</span> <span class="hljs-variable">BOOT_INF_LIB</span> <span class="hljs-operator">=</span> <span class="hljs-string">"BOOT-INF/lib/"</span>; <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">isNestedArchive</span><span class="hljs-params">(Archive.Entry entry)</span> { <span class="hljs-comment">// 只接受 `BOOT-INF/classes/` 目录</span> <span class="hljs-keyword">if</span> (entry.isDirectory()) { <span class="hljs-keyword">return</span> entry.getName().equals(BOOT_INF_CLASSES); } <span class="hljs-comment">// 只接受 `BOOT-INF/lib/` 目录下的 jar 包</span> <span class="hljs-keyword">return</span> entry.getName().startsWith(BOOT_INF_LIB); }
}
-
从
archive
(当前 jar 包)解析出所有 Archive 条目信息,这个archive
在上面 1. JarLauncher 讲到过,创建 JarLauncher 实例化对象的时候会初始化archive
,是一个JarFileArchive
对象,也就是我们打包后的jar
包,那么接下来需要从中解析出所有的 Archive 对象// JarFileArchive.java @Override public List<Archive> getNestedArchives(EntryFilter filter) throws IOException { List<Archive> nestedArchives = new ArrayList<>(); // 遍历 jar 包(当前应用)中所有的 Entry for (Entry entry : this) { // 进行过滤,`BOOT-INF/classes/` 目录或者 `BOOT-INF/lib/` 目录下的 jar 包 if (filter.matches(entry)) { // 将 Entry 转换成 JarFileArchive nestedArchives.add(getNestedArchive(entry)); } } // 返回 jar 包(当前应用)找到的所有 JarFileArchive // `BOOT-INF/classes/` 目录对应一个 JarFileArchive(因为就是当前应用中的内容) // `BOOT-INF/lib/` 目录下的每个 jar 包对应一个 JarFileArchive return Collections.unmodifiableList(nestedArchives); }
返回 jar 包(当前应用)找到的所有 JarFileArchive:
BOOT-INF/classes/
目录对应一个 JarFileArchive(因为就是当前 Spring Boot 应用编译后的内容)BOOT-INF/lib/
目录下的每个 jar 包对应一个 JarFileArchive
-
返回从
jar
包中找到的所有 JarFileArchive
这一步骤就是从 jar
包中解析出我们需要的东西来,如上描述,每个 JarFileArchive 会对应一个 JarFile 对象
2.3 createClassLoader 方法
备注:创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前 jar 中所有的类
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
// <1> 获取所有 JarFileArchive 对应的 URL
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
// <2> 创建 Spring Boot 自定义的 ClassLoader 类加载器,并设置父类加载器为当前线程的类加载器
// 通过它解析这些 URL,也就是加载 `BOOT-INF/classes/` 目录下的类和 `BOOT-INF/lib/` 目录下的所有 jar 包
return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
该过程如下:
- 获取所有 JarFileArchive 对应的 URL
- 创建 Spring Boot 自定义的 ClassLoader 类加载器,并设置父类加载器为当前线程的类加载器
可以看到 LaunchedURLClassLoader 为自定义类加载器,这样就能从我们 jar
包中的 BOOT-INF/classes/
目录下和 BOOT-INF/lib/
目录下的所有三方依赖包中加载出 Class 类对象
2.4 getMainClass 方法
备注:获取当前应用的启动类(你自己写的那个 main 方法)
// ExecutableArchiveLauncher.java
@Override
protected String getMainClass() throws Exception {
// 获取 jar 包(当前应用)的 Manifest 对象,也就是 META-INF/MANIFEST.MF 文件中的属性
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 获取启动类(当前应用自己的启动类)
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException("No'Start-Class'manifest entry specified in" + this);
}
// 返回当前应用的启动类
return mainClass;
}
过程如下:
- 获取
jar
包(当前应用)的Manifest
对象,也就是META-INF/MANIFEST.MF
文件中的属性 - 获取启动类(当前应用自己的启动类),也就是
Start-Class
配置,并返回
可以看到,这一步就是找到你 Spring Boot 应用的启动类,前面 ClassLoader 类加载器都准备好了,那么现在不就可以直接调用这个类的 main
方法来启动应用了
2.5 launch 方法
备注:执行你的 Spring Boot 应用的启动类的
main
方法
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
// 设置当前线程的 ClassLoader 为刚创建的类加载器
Thread.currentThread().setContextClassLoader(classLoader);
// 创建一个 MainMethodRunner 对象(main 方法执行器)
// 执行你的 main 方法(反射)
createMainMethodRunner(mainClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
整个过程很简单,先设置当前线程的 ClassLoader 为刚创建的类加载器,然后创建一个 MainMethodRunner 对象(main
方法执行器),执行你的 main
方法(反射),启动 Spring Boot 应用
public class MainMethodRunner {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String mainClassName;
<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String[] args;
<span class="hljs-keyword">public</span> <span class="hljs-title function_">MainMethodRunner</span><span class="hljs-params">(String mainClass, String[] args)</span> {
<span class="hljs-built_in">this</span>.mainClassName = mainClass;
<span class="hljs-built_in">this</span>.args = (args != <span class="hljs-literal">null</span>) ? args.clone() : <span class="hljs-literal">null</span>;
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">run</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception {
<span class="hljs-comment">// 根据名称加载 main 方法所在类的 Class 对象</span>
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(<span class="hljs-built_in">this</span>.mainClassName);
<span class="hljs-comment">// 获取 main 方法</span>
<span class="hljs-type">Method</span> <span class="hljs-variable">mainMethod</span> <span class="hljs-operator">=</span> mainClass.getDeclaredMethod(<span class="hljs-string">"main"</span>, String[].class);
<span class="hljs-comment">// 执行这个 main 方法(反射)</span>
mainMethod.invoke(<span class="hljs-literal">null</span>, <span class="hljs-keyword">new</span> <span class="hljs-title class_">Object</span>[] { <span class="hljs-built_in">this</span>.args });
}
}
这里就是通过反射调用你的 Spring Boot 应用的启动类的 main
方法
LaunchedURLClassLoader
org.springframework.boot.loader.LaunchedURLClassLoader
是 spring-boot-loader
中自定义的类加载器,实现对 jar
包中 BOOT-INF/classes
目录下的类和 BOOT-INF/lib
下第三方 jar
包中的类的加载。
public class LaunchedURLClassLoader extends URLClassLoader {
<span class="hljs-keyword">static</span> {
ClassLoader.registerAsParallelCapable();
}
<span class="hljs-comment">/**
* Create a new {<span class="hljs-doctag">@link</span> LaunchedURLClassLoader} instance.
* <span class="hljs-doctag">@param</span> urls the URLs from which to load classes and resources
* <span class="hljs-doctag">@param</span> parent the parent class loader for delegation
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-title function_">LaunchedURLClassLoader</span><span class="hljs-params">(URL[] urls, ClassLoader parent)</span> {
<span class="hljs-built_in">super</span>(urls, parent);
}
<span class="hljs-comment">/**
* 重写类加载器中加载 Class 类对象方法
*/</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">protected</span> Class<?> loadClass(String name, <span class="hljs-type">boolean</span> resolve) <span class="hljs-keyword">throws</span> ClassNotFoundException {
Handler.setUseFastConnectionExceptions(<span class="hljs-literal">true</span>);
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// 判断这个类是否有对应的 Package 包</span>
<span class="hljs-comment">// 没有的话会从所有 URL(包括内部引入的所有 jar 包)中找到对应的 Package 包并进行设置</span>
definePackageIfNecessary(name);
}
<span class="hljs-keyword">catch</span> (IllegalArgumentException ex) {
<span class="hljs-comment">// Tolerate race condition due to being parallel capable</span>
<span class="hljs-keyword">if</span> (getPackage(name) == <span class="hljs-literal">null</span>) {
<span class="hljs-comment">// This should never happen as the IllegalArgumentException indicates</span>
<span class="hljs-comment">// that the package has already been defined and, therefore,</span>
<span class="hljs-comment">// getPackage(name) should not return null.</span>
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">AssertionError</span>(<span class="hljs-string">"Package "</span> + name + <span class="hljs-string">" has already been defined but it could not be found"</span>);
}
}
<span class="hljs-comment">// 加载对应的 Class 类对象</span>
<span class="hljs-keyword">return</span> <span class="hljs-built_in">super</span>.loadClass(name, resolve);
}
<span class="hljs-keyword">finally</span> {
Handler.setUseFastConnectionExceptions(<span class="hljs-literal">false</span>);
}
}
<span class="hljs-comment">/**
* Define a package before a {<span class="hljs-doctag">@code</span> findClass} call is made. This is necessary to
* ensure that the appropriate manifest for nested JARs is associated with the
* package.
* <span class="hljs-doctag">@param</span> className the class name being found
*/</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">definePackageIfNecessary</span><span class="hljs-params">(String className)</span> {
<span class="hljs-type">int</span> <span class="hljs-variable">lastDot</span> <span class="hljs-operator">=</span> className.lastIndexOf(<span class="hljs-string">'.'</span>);
<span class="hljs-keyword">if</span> (lastDot >= <span class="hljs-number">0</span>) {
<span class="hljs-comment">// 获取包名</span>
<span class="hljs-type">String</span> <span class="hljs-variable">packageName</span> <span class="hljs-operator">=</span> className.substring(<span class="hljs-number">0</span>, lastDot);
<span class="hljs-comment">// 没找到对应的 Package 包则进行解析</span>
<span class="hljs-keyword">if</span> (getPackage(packageName) == <span class="hljs-literal">null</span>) {
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// 遍历所有的 URL,从所有的 jar 包中找到这个类对应的 Package 包并进行设置</span>
definePackage(className, packageName);
}
<span class="hljs-keyword">catch</span> (IllegalArgumentException ex) {
<span class="hljs-comment">// Tolerate race condition due to being parallel capable</span>
<span class="hljs-keyword">if</span> (getPackage(packageName) == <span class="hljs-literal">null</span>) {
<span class="hljs-comment">// This should never happen as the IllegalArgumentException</span>
<span class="hljs-comment">// indicates that the package has already been defined and,</span>
<span class="hljs-comment">// therefore, getPackage(name) should not have returned null.</span>
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">AssertionError</span>(
<span class="hljs-string">"Package "</span> + packageName + <span class="hljs-string">" has already been defined but it could not be found"</span>);
}
}
}
}
}
<span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">definePackage</span><span class="hljs-params">(String className, String packageName)</span> {
<span class="hljs-keyword">try</span> {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
<span class="hljs-comment">// 把类路径解析成类名并加上 .class 后缀</span>
<span class="hljs-type">String</span> <span class="hljs-variable">packageEntryName</span> <span class="hljs-operator">=</span> packageName.replace(<span class="hljs-string">'.'</span>, <span class="hljs-string">'/'</span>) + <span class="hljs-string">"/"</span>;
<span class="hljs-type">String</span> <span class="hljs-variable">classEntryName</span> <span class="hljs-operator">=</span> className.replace(<span class="hljs-string">'.'</span>, <span class="hljs-string">'/'</span>) + <span class="hljs-string">".class"</span>;
<span class="hljs-comment">// 遍历所有的 URL(包括应用内部引入的所有 jar 包)</span>
<span class="hljs-keyword">for</span> (URL url : getURLs()) {
<span class="hljs-keyword">try</span> {
<span class="hljs-type">URLConnection</span> <span class="hljs-variable">connection</span> <span class="hljs-operator">=</span> url.openConnection();
<span class="hljs-keyword">if</span> (connection <span class="hljs-keyword">instanceof</span> JarURLConnection) {
<span class="hljs-type">JarFile</span> <span class="hljs-variable">jarFile</span> <span class="hljs-operator">=</span> ((JarURLConnection) connection).getJarFile();
<span class="hljs-comment">// 如果这个 jar 中存在这个类名,且有对应的 Manifest</span>
<span class="hljs-keyword">if</span> (jarFile.getEntry(classEntryName) != <span class="hljs-literal">null</span> && jarFile.getEntry(packageEntryName) != <span class="hljs-literal">null</span>
&& jarFile.getManifest() != <span class="hljs-literal">null</span>) {
<span class="hljs-comment">// 定义这个类对应的 Package 包</span>
definePackage(packageName, jarFile.getManifest(), url);
<span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}
}
}
<span class="hljs-keyword">catch</span> (IOException ex) {
<span class="hljs-comment">// Ignore</span>
}
}
<span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}, AccessController.getContext());
}
<span class="hljs-keyword">catch</span> (java.security.PrivilegedActionException ex) {
<span class="hljs-comment">// Ignore</span>
}
}
}
上面的代码就不一一讲述了,LaunchedURLClassLoader 重写了 ClassLoader 的 loadClass(String, boolean)
加载 Class 类对象方法,在加载对应的 Class 类对象之前新增了一部分逻辑,会尝试从 jar
包中定义 Package 包对象,这样就能加载到对应的 Class 类对象。
总结
Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin
,可以很方便的将我们的 Spring Boot 项目打成 jar
包,jar
包中主要分为三个模块:
BOOT-INF
目录,里面保存了我们自己 Spring Boot 项目编译后的所有文件,其中classes
目录下面就是编译后的 .class 文件,包括项目中的配置文件等,lib
目录下就是我们引入的第三方依赖META-INF
目录,通过MANIFEST.MF
文件提供jar
包的元数据,声明jar
的启动类等信息。每个 Javajar
包应该是都有这个文件的,参考 Oracle 官方对于jar
的说明,里面有一个Main-Class
配置用于指定启动类org.springframework.boot.loader
目录,也就是 Spring Boot 的spring-boot-loader
子模块,它就是java -jar xxx.jar
启动 Spring Boot 项目的秘密所在,上面的Main-Class
指定的就是里面的一个类
通过 java -jar
启动应用时,根据 Main-Class
配置会调用 org.springframework.boot.loader.JarLauncher
的main(String[])
方法;其中会先创建一个自定义的 ClassLoader 类加载器,可从BOOT-INF
目录下加载出我们 Spring Boot 应用的 Class 类对象,包括依赖的第三方 jar
包中的 Class 类对象;然后根据 Start-Class
配置调用我们 Spring Boot 应用启动类的 main(String[])
方法(反射),这样也就启动了应用,至于我们的 main(String[])
方法中做了哪些事情,也就是后续所讲的内容。