硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
Posted 芋道源码
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!相关的知识,希望对你有一定的参考价值。
管她前浪,还是后浪?
能浪的浪,才是好浪!
每天 8:55 更新文章,每天掉亿点点头发...
源码精品专栏
“摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/jar/ 「芋道源码」欢迎转载,保留摘要,谢谢!
-
1. 概述 -
2. MANIFEST.MF -
3. JarLauncher -
4. LaunchedURLClassLoader -
666. 彩蛋
大家好,我是艿艿,一个熬夜退役选手~
这两周很不顺心,接连的变化,一脸的懵逼。还好我还有 150000 女粉,我又充满能量的肝完了本文。
1. 概述
Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin
,可以方便的将 Spring Boot 项目打成 jar
包或者 war
包。
考虑到部署的便利性,我们绝大多数 99.99% 的场景下,我们会选择打成 jar
包。这样,我们就无需在部署项目的服务器上,配置相应的 Tomcat、Jetty 等 Servlet 容器。
那么,jar
包是如何运行,并启动 Spring Boot 项目的呢?这个就是本文的目的,一起弄懂 Spring Boot jar
包的运行原理。
下面,我们来打开一个 Spring Boot jar
包,看看其里面的结构。如下图所示,一共分成四部分:
-
①
META-INF
目录:通过MANIFEST.MF
文件提供jar
包的元数据,声明了jar
的启动类。 -
②
org
目录:为 Spring Boot 提供的spring-boot-loader
项目,它是java -jar
启动 Spring Boot 项目的秘密所在,也是稍后我们将深入了解的部分。“
Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched using
java -jar
. Generally you will not need to usespring-boot-loader
directly, but instead work with the Gradle or Maven plugin. -
③
BOOT-INF/lib
目录:我们 Spring Boot 项目中引入的依赖的jar
包们。spring-boot-loader
项目很大的一个作用,就是解决jar
包里嵌套jar
的情况,如何加载到其中的类。 -
④
BOOT-INF/classes
目录:我们在 Spring Boot 项目中 Java 类所编译的.class
、配置文件等等。
先简单剧透下,spring-boot-loader
项目需要解决两个问题:
-
第一,如何引导执行我们创建的 Spring Boot 应用的启动类,例如上述图中的 Application 类。 -
第二,如何加载 BOOT-INF/class
目录下的类,以及BOOT-INF/lib
目录下内嵌的jar
包中的类。
下面,尾随艿艿,一起来抽丝剥茧!
2. MANIFEST.MF
我们来查看 META-INF/MANIFEST.MF
文件,里面的内容如下:
Manifest-Version: 1.0
Implementation-Title: lab-39-demo
Implementation-Version: 2.2.2.RELEASE
Start-Class: cn.iocoder.springboot.lab39.skywalkingdemo.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.2.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
它实际是一个 Properties 配置文件,每一行都是一个配置项目。重点来看看两个配置项:
-
Main-Class
配置项:Java 规定的jar
包的启动类,这里设置为spring-boot-loader
项目的 JarLauncher 类,进行 Spring Boot 应用的启动。 -
Start-Class
配置项:Spring Boot 规定的 主启动类,这里设置为我们定义的 Application 类。
“小知识补充:为什么会有
Main-Class
/Start-Class
配置项呢?因为我们是通过 Spring Boot 提供的 Maven 插件spring-boot-maven-plugin
进行打包,该插件将该配置项写入到MANIFEST.MF
中,从而能让spring-boot-loader
能够引导启动 Spring Boot 应用。
可能胖友会有疑惑,Start-Class
对应的 Application 类自带了 #main(String[] args)
方法,为什么我们不能直接运行会如何呢?我们来简单尝试一下哈,控制台执行如下:
$ java -classpath lab-39-demo-2.2.2.RELEASE.jar cn.iocoder.springboot.lab39.skywalkingdemo.Application
错误: 找不到或无法加载主类 cn.iocoder.springboot.lab39.skywalkingdemo.Application
直接找不到 Application 类,因为它在 BOOT-INF/classes
目录下,不符合 Java 默认的 jar
包的加载规则。因此,需要通过 JarLauncher 启动加载。
当然实际还有一个更重要的原因,Java 规定可执行器的 jar
包禁止嵌套其它 jar
包。但是我们可以看到 BOOT-INF/lib
目录下,实际有 Spring Boot 应用依赖的所有 jar
包。因此,spring-boot-loader
项目自定义实现了 ClassLoader 实现类 LaunchedURLClassLoader,支持加载 BOOT-INF/classes
目录下的 .class
文件,以及 BOOT-INF/lib
目录下的 jar
包。
3. JarLauncher
JarLauncher 类是针对 Spring Boot jar
包的启动类,整体类图如下所示:
“友情提示:WarLauncher 类,是针对 Spring Boot
war
包的启动类,后续胖友可以自己瞅瞅,差别并不大哈~
JarLauncher 的源码比较简单,如下图所示:
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
通过 #main(String[] args)
方法,创建 JarLauncher 对象,并调用其 #launch(String[] args)
方法进行启动。整体的启动逻辑,其实是由父类 Launcher 所提供,如下图所示:
父类 Launcher 的 #launch(String[] args)
方法,代码如下:
// Launcher.java
protected void launch(String[] args) throws Exception {
// <1> 注册 URL 协议的处理器
JarFile.registerUrlProtocolHandler();
// <2> 创建类加载器
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// <3> 执行启动类的 main 方法
launch(args, getMainClass(), classLoader);
}
-
<1>
处,调用 JarFile 的#registerUrlProtocolHandler()
方法,注册 Spring Boot 自定义的 URLStreamHandler 实现类,用于jar
包的加载读取。 -
<2>
处,调用自身的#createClassLoader(List<Archive> archives)
方法,创建自定义的 ClassLoader 实现类,用于从jar
包中加载类。 -
<3>
处,执行我们声明的 Spring Boot 启动类,进行 Spring Boot 应用的启动。
简单来说,就是整一个可以读取 jar
包中类的加载器,保证 BOOT-INF/lib
目录下的类和 BOOT-classes
内嵌的 jar
中的类能够被正常加载到,之后执行 Spring Boot 应用的启动。
下面,我们逐行代码来看看噢。即将代码多多,保持淡定,嘿嘿~
3.1 registerUrlProtocolHandler
“友情提示:对应
JarFile.registerUrlProtocolHandler();
代码段,不要迷路。
JarFile 是 java.util.jar.JarFile
的子类,如下所示:
public class JarFile extends java.util.jar.JarFile {
// ... 省略其它代码
}
JarFile 主要增强支持对内嵌的 jar
包的获取。如下图所示:
OK,介绍完之后,让我们回到 JarFile 的 #registerUrlProtocolHandler()
方法,注册 Spring Boot 自定义的 URL 协议的处理器。代码如下:
// JarFile.java
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
/**
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
* {@link URLStreamHandler} will be located to deal with jar URLs.
*/
public static void registerUrlProtocolHandler() {
// 获得 URLStreamHandler 的路径
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
// 将 Spring Boot 自定义的 HANDLERS_PACKAGE(org.springframework.boot.loader) 补充上去
System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
: handlers + "|" + HANDLERS_PACKAGE));
// 重置已缓存的 URLStreamHandler 处理器们
resetCachedUrlHandlers();
}
/**
* Reset any cached handlers just in case a jar protocol has already been used.
* We reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
* should have no effect other than clearing the handlers cache.
*
* 重置 URL 中的 URLStreamHandler 的缓存,防止 `jar://` 协议对应的 URLStreamHandler 已经创建
* 我们通过设置 URLStreamHandlerFactory 为 null 的方式,清空 URL 中的该缓存。
*/
private static void resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null);
} catch (Error ex) {
// Ignore
}
}
-
胖友先跟着注释,自己阅读下如上的代码~
目的很明确,通过将 org.springframework.boot.loader
包设置到 "java.protocol.handler.pkgs"
环境变量,从而使用到自定义的 URLStreamHandler 实现类 Handler,处理 jar:
协议的 URL。
“友情提示:这里我们暂时不深入 Handler 的源码,避免直接走的太深,丢失了主干。后续胖友可结合《Java URL 协议扩展实现》文章,进行 Handler 的实现理解。
另外,HandlerTests 提供的单元测试,也是非常有帮助的~
3.2 createClassLoader
“友情提示:对应
ClassLoader classLoader = createClassLoader(getClassPathArchives())
代码段,不要迷路。
3.2.1 getClassPathArchives
首先,我们先来看看 #getClassPathArchives()
方法,它是由 ExecutableArchiveLauncher 所实现,代码如下:
// ExecutableArchiveLauncher.java
private final Archive archive;
@Override
protected List<Archive> getClassPathArchives() throws Exception {
// <1> 获得所有 Archive
List<Archive> archives = new ArrayList<>(
this.archive.getNestedArchives(this::isNestedArchive));
// <2> 后续处理
postProcessClassPathArchives(archives);
return archives;
}
protected abstract boolean isNestedArchive(Archive.Entry entry);
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
“友情提示:这里我们会看到一个 Archive 对象,先可以暂时理解成一个一个的档案,稍后会清晰认识的~
<1>
处,this::isNestedArchive
代码段,创建了 EntryFilter 匿名实现类,用于过滤 jar
包不需要的目录。
// Archive.java
/**
* Represents a single entry in the archive.
*/
interface Entry {
/**
* Returns {@code true} if the entry represents a directory.
* @return if the entry is a directory
*/
boolean isDirectory();
/**
* Returns the name of the entry.
* @return the name of the entry
*/
String getName();
}
/**
* Strategy interface to filter {@link Entry Entries}.
*/
interface EntryFilter {
/**
* Apply the jar entry filter.
* @param entry the entry to filter
* @return {@code true} if the filter matches
*/
boolean matches(Entry entry);
}
这里在它的内部,调用了 #isNestedArchive(Archive.Entry entry)
方法,它是由 JarLauncher 所实现,代码如下:
// JarLauncher.java
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
// 如果是目录的情况,只要 BOOT-INF/classes/ 目录
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// 如果是文件的情况,只要 BOOT-INF/lib/ 目录下的 `jar` 包
return entry.getName().startsWith(BOOT_INF_LIB);
}
-
目的就是过滤获得, BOOT-INF/classes/
目录下的类,以及BOOT-INF/lib/
的内嵌jar
包。
<1>
处,this.archive.getNestedArchives
代码段,调用 Archive 的 #getNestedArchives(EntryFilter filter)
方法,获得 archive
内嵌的 Archive 集合。代码如下:
// Archive.java
/**
* Returns nested {@link Archive}s for entries that match the specified filter.
* @param filter the filter used to limit entries
* @return nested archives
* @throws IOException if nested archives cannot be read
*/
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
Archive 接口,是 spring-boot-loader
项目定义的档案抽象,其子类如下图所示:
-
ExplodedArchive 是针对 目录的 Archive 实现类。 -
JarFileArchive 是针对 jar
包的 Archive 实现类。
“友情提示:这块可能有一丢丢复杂,胖友吃耐心哈~
那么,我们在 ExecutableArchiveLauncher 的 archive
属性是怎么来的呢?答案在 ExecutableArchiveLauncher 的构造方法中,代码如下:
// ExecutableArchiveLauncher.java
public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected ExecutableArchiveLauncher(Archive archive) {
this.archive = archive;
}
// ... 省略其它
}
// Launcher.java
public abstract class Launcher {
protected final Archive createArchive() throws Exception {
// 获得 jar 所在的绝对路径
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
String path = (location != null) ? location.getSchemeSpecificPart() : null;
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
// 如果是目录,则使用 ExplodedArchive 进行展开
// 如果不是目录,则使用 JarFileArchive
return (root.isDirectory() ? new ExplodedArchive(root)
: new JarFileArchive(root));
}
}
根据根路径是否为目录的情况,创建 ExplodedArchive 或 JarFileArchive 对象。那么问题就来了,这里的 root
是什么呢?艿艿一波骚操作,终于输出了答案,如下图所示:
以上是关于硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!的主要内容,如果未能解决你的问题,请参考以下文章
Java面试题,这一次带你搞懂Spring代理创建过程,一招彻底弄懂!