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);
	}

}

看一下类图

-w288

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