SpringBoot Jar文件结构
SpringBoot打成的Jar包叫executable jar
,也叫fat jar
,该jar包中包含了所有用户代码,资源以及依赖的jar
还有一种jar包叫shaded jar
,与executable jar不一样的是,它会把用户代码以及依赖的jar包中的代码都放入一个jar包中
Executable Jar结构
├── BOOT-INF
│ ├── classes
│ └── lib
├── META-INF
└── org
└── springframework
└── boot
└── loader
├── archive
├── data
├── jar
└── util
BOOT-INF
- classes:存放用户代码以及资源文件
- lib:存放程序依赖的所有jar包
META-INF
- MANIFEST.MF:清单文件
# 清单版本
Manifest-Version: 1.0
# 用户代码启动类
Start-Class: com.xxx.xxx.App
# 用户代码路径
Spring-Boot-Classes: BOOT-INF/classes/
# 依赖jar包路径
Spring-Boot-Lib: BOOT-INF/lib/
# SpringBoot版本
Spring-Boot-Version: 2.2.7.RELEASE
# 项目启动入口类
Main-Class: org.springframework.boot.loader.JarLauncher
org.springframework.boot.loader
由springboot打包插件自动打进jar包的一些类,比如JarLauncher等等
SpringBoot执行Jar过程分析
MANIFEST.MF文件中指定的启动类
Main-Class: org.springframework.boot.loader.JarLauncher
首先来看一下这个类
/**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
* included inside a {@code /BOOT-INF/lib} directory and that application classes are
* included inside a {@code /BOOT-INF/classes} directory.
* 注释很明确了,JarLauncher就是用于启动jar归档文件的,它会假定所有的依赖jar包都在/BOOT-INF/lib目录下,用户代码则在/BOOT-INF/classes目录下
*/
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
protected JarLauncher(Archive archive) {
super(archive);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
看一下类图
JarLauncher继承了ExecutableArchiveLauncher,ExecutableArchiveLauncher又继承了Launcher
JarLauncher启动时会调用父类ExecutableArchiveLauncher
的构造方法,将Archive传入
Archive
Archive即归档文件,比如tar、zip(jar就属于zip)之类的就属于归档文件,SpringBoot将其抽象为一种资源,它有两个实现类,分别是
- ExplodedArchive:文件目录资源
- JarFileArchive:jar文件资源
JarFileArchive
public class JarFileArchive implements Archive {
private final JarFile jarFile;
private URL url;
private File tempUnpackFolder;
public JarFileArchive(File file) throws IOException {
this(file, file.toURI().toURL());
}
public JarFileArchive(File file, URL url) throws IOException {
this(new JarFile(file));
this.url = url;
}
public JarFileArchive(JarFile jarFile) {
this.jarFile = jarFile;
}
/**
* Returns nested {@link Archive}s for entries that match the specified filter.
* 返回嵌套的archive
*/
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}
通过构造方法可以得知,JarFileArchive支持文件启动,也支持Jar直接启动,SpringBoot中使用的是文件启动的方式,在SpringBoot中,每一个archive都会有自己的URL,类似jar:file:/spring-boot-demo/target/spring-boot-demo.jar!/BOOT-INF/lib/spring-boot-starter-web-2.2.7.RELEASE.jar
org.springframework.boot.loader.Launcher#launch(java.lang.String[])
来看一下这个方法,JarLauncher通过该方法来启动jar文件
protected void launch(String[] args) throws Exception {
// 注册一个'java.protocol.handler.pkgs'属性让URLStreamHandler可以处理jar文件URL
JarFile.registerUrlProtocolHandler();
// 通过getClassPathArchives方法获取到jar文件中所有的archives
// 接着创建LaunchedURLClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// 先获取到启动参数,classloader,并通过getMainClass方法获取到MANIFEST.MF中Start-Class属性中指定的类名(如果不存在这个属性会抛出异常,启动失败)
// 接着执行启动流程
launch(args, getMainClass(), classLoader);
}
@Override
protected String getMainClass() throws Exception {
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;
}
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
- 设置线程上下文Classloader
- 创建Main方法执行器并执行run方法
org.springframework.boot.loader.MainMethodRunner
这是一个被Launcher用来调用main方法的工具类
public class MainMethodRunner {
// main方法所在类的名字
private final String mainClassName;
// main方法参数
private final String[] args;
/**
* Create a new {@link MainMethodRunner} instance.
*/
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
public void run() throws Exception {
// 获取当前线程的上下文ClassLoader,也就是之前创建好的LaunchedURLClassLoader来加载mainClass
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
// 获取main方法,并执行
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
}
至此,整个JarLauncher的执行流程就分析完了
SpringBoot为什么要在这里使用自定义的ClassLoader来执行类加载?
在标准Jar包中,只有在最顶层目录的类才能够被启动,比如Executable Jar
中的org.springframework.boot这个目录下的启动类,并且AppClassLoader(默认的用户类加载器)只能够加载顶层目录的类
但是SpringBoot将用户的代码和其他依赖都放入到了顶层目录的子目录下,因此无法直接被类加载器加载到,也无法被启动
所以SpringBoot自定义了一个类加载器LaunchedURLClassLoader来加载用户代码以及依赖包中的类
源码
protected void launch(String[] args) throws Exception {
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives
Launcher Returns the archives that will be used to construct the class path.
@Override
protected List<Archive> getClassPathArchives() throws Exception {
// 过滤出archive中在BOOT-INF/classes目录下的用户代码和BOOT-INF/lib目录下的依赖jar包
List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
先看一下这个方法,主要就是将当前Archive中嵌套的Archive取出来然后放入到一个List中,也就是把Jar包中的依赖解析出来
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
// 如果entry是一个目录,判断它是否是BOOT-INF/classes下的文件并返回
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// 如果entry不是一个目录,则判断它是否是BOOT-INF/lib下面的依赖jar包并返回
return entry.getName().startsWith(BOOT_INF_LIB);
}
然后根据这些Archives来创建LaunchedURLClassLoader
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
- 提取出archives中的url,之前提到过,每一个archive都会有一个url
- 创建LaunchedURLClassLoader,指定其父类为当前ClassLoader,也就是AppClassLoader
- 返回创建好的ClassLoader