1、引言
在 SpringBoot 项目的根目录下执行mvn clean package
,maven 会将当前的项目打成可执行的 jar 包,随后通过java -jar xxx.jar
命令,即可启动运行 SpringBoot 服务,那SpringBoot的可执行jar包是如何运行的呢?
2、SpringBoot FAT JAR 结构
SpringBoot 的可执行 jar 包又称作“fat jar”,是包含所有三方依赖的jar。它与传统jar包最大的不同是包含了一个 lib目录和内嵌了 web 容器,SpringBoot fat jar 通常是由集成在 pom.xml 文件中的 maven 插件来生成的。配置在 pom 文件 build 元素中的 plugins 内:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
复制代码
执行mvn clean package
后,进入项目根目录下target/
目录,发现多了这两个文件xxx.jar
、xxx.jar.original
,同时,xxx.jar
的大小要远大于xxx.jar.original
。其实,xxx.jar.original
是属于原始 Maven 打包 jar 文件,而xxx.jar
则是运行spring-boot-maven-plugin
插件后,在xxx.jar.original
的基础上引入了第三方依赖后repackage
而成的。 spring-boot-maven-plugin
默认有5 个goals
:repackage、run、start、 stop、 build-info。在打包的时候默认使用的是repackage
,repackage
能够将mvn package
生成的软件包,再次打包为可执行的软件包,并将mvn package
生成的软件包重命名为 xxx.original
。 我们可以通过将xxx.jar
可执行jar解包之后看下文件的结构,jdk提供了jar -xvf xxx.jar
工具解压jar包;Fat JAR 采用 zip 压缩格式存储,因此凡是能解压 zip 压缩文件的软件,均可将 JAR 包解压,这里我们使用unzip解压:
解压后有三个目录:BOOT-INF
、META-INF
、org
,文件分类用途如下:
- BOOT-INF/classes 目录存放应用编译后的 class 文件,业务代码
- BOOT-INF/lib 目录存放应用依赖的 JAR 包
- META-INF/目录存放应用相关的元信息,如 MANIFEST.MF 文件
- org/目录存放 Spring Boot 相关的 class 文件
3、 SpringBoot FAT JAR 启动
当 SpringBoot 应用可执行 JAR 文件被java -jar
命令执行时,其命令本身对 JAR 文件是否来自 SpringBoot 插件打包并不感知。换言之,该命令引导的是标准可执行 JAR 文件,而按照 Java 官方文档的规定,java -jar
命令引导的具体启动类必须配置在MANIFEST.MF
资源的Main-Class
属性中: 发现
Main-Class
属性指向org.springframework.boot.loader.JarLauncher
,也就是说SpringBoot FATJAR 包的启动类并是不我们项目的主配置类,而是JarLauncher
,项目的主配置类被定义成了Start-Class
;这时候我们可以大胆猜测了,java -jar xxx.jar
启动了JarLauncher
启动类,JarLauncher
执行了引导类(Start-Class
)的main方法,从而启动了SpringBoot项目,接下来就是小心求证这一猜想。
通过上述解压 jar 包的方法,查看xxx.jar.original
内的文件可以看出,org.springframework.boot.loader.JarLauncher
并非项目中的文件,而是spring-boot-maven-plugin
插件repackage
进来的。 通过 search.maven.org 搜索JarLauncher
的依赖坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
复制代码
将该依赖坐标加到我们的项目pom的dependences中,这样就能查看对应源码了,后面分析spring-boot-loader
原理。
既然JarLauncher
就在打包后的 FAT JAR 中,就可以在解压后的根目录中,直接使用 java 命令引导 JarLauncher
类文件,同样可以启动 Springboot 项目,其实java -jar
也是做了一样的事情:
4、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);
}
}
复制代码
Launcher 类的具体实现类有3个:JarLauncher、WarLauncher 和 PropertiesLauncher,这里以JarLauncher
为例来解析说明Spring Boot 基于 Launcher
来实现的启动过程。 程序入口为main函数中的new JarLauncher().launch(args);
调用new JarLauncher()
构造函数会触发其父类的构造函数调用ExecutableArchiveLauncher()
调用:
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
复制代码
/*
在 createArchive 方法中,根据当前类信息获得当前归档文件的路径(即打包后生成的可执行的 xxx.jar),
并检查文件路径是否存在,如果存在且是文件夹,则创建 ExplodedArchive 的对象,否则创建 JarFileArchive 的对象。
*/
protected final Archive createArchive() throws Exception {
// 通过获得当前 class 类的信息,查找到当前归档文件的路径
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));
}
复制代码
我们再回到 JarLauncher
的入口程序,当创建完 JarLauncher
对象,获得了当前归档文件的Archive
,下一步便是调用launch
方法,该方法由Launcher
类实现。Launcher
中的这个launch
方法就是启动应用程序的入口,而该方法的定义是为了让子类的静态main
方法调用的。
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
// 找到 /BOOT-INF/lib 下的 jar,及/BOOT-INF/classes 下所对应的archive,
// 通过这些 archive 的 URL生成 LaunchedURLClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// 获取 MANIFEST.MF 中的Start-Class(springboot项目的主配置类),使用创建的classLoader加载,并执行其 main 方法
launch(args, getMainClass(), classLoader);
}
复制代码
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 获取 MANIFEST.MF 中的Start-Class(springboot项目的主配置类)
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException(
"No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
复制代码
执行launch
方法:
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
// 将classLoader(LaunchedURLClassLoader),设置为线程上下文类加载器
Thread.currentThread().setContextClassLoader(classLoader);
// 获取Start-Class(SpringBoot主配置类),并创建 MainMethodRunner 对象,调用其 run 方法。
createMainMethodRunner(mainClass, args, classLoader).run();
}
复制代码
MainMethodRunner
类的实现:
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null ? args.clone() : null);
}
public void run() throws Exception {
// 使用线程上下文类加载器加载Start-Class(springboot项目的主配置类)
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
// 反射调用 Start-Class 的main方法
mainMethod.invoke(null, new Object[] { this.args });
}
}
复制代码
5、总结
至此,springboot 可执行jar包的启动流程就结束了,接下去就是 SpringBoot 主配置类的main
方法调用: SpringApplication.run(LearnSringBootApplication.class, args);
进入 SpringBoot 的启动流程,涉及到 spring 容器的启动和 SpringBoot 自动装配的相关内容,这将是一个更加复杂的过程啦!