目录

  • 一. 🦁 前言
  • 二. 🦁 目录结构
  • 2.1 MANIFEST.MF中的jar启动类
  • 2.2 深入剥析
  • 2.2.1 registerUrlProtocolHandler()
  • 2.2.2 createClassLoader(getClassPathArchives())
  • 2.2.3 launch(args,getMainClass(),classLoader)
  • 三.总结

一. 🦁 前言

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以方便的将 Spring Boot 项目打成 jar 包或者 war 包。
考虑到部署的便利性,我们绝大多数人在 99.99% 的场景下,都会选择打成 jar 包。这样,我们就无需在部署项目的服务器上,配置相应的 TomcatJettyServlet 容器。
那么,jar 包是如何运行,并启动 Spring Boot 项目的呢?这个就是本文的目的,一起弄懂 Spring Boot jar 包的运行原理

二. 🦁 目录结构

下面,我们来打开一个 Spring Boot jar 包,看看其里面的结构。如下图所示,一共分成四部分:

【SpringBoot源码剥析】| 项目运行原理_hive


【SpringBoot源码剥析】| 项目运行原理_jar_02

  1. META-INF 目录:通过 MANIFEST.MF 文件提供 jar 包的元数据,声明了 jar 的启动类。通常包含一些特定的文件和目录,用于描述存档文件的内容和结构。该目录通常包含以下文件:
  • MANIFEST.MF文件:该文件包含关于JAR文件的元数据信息,如版本号、创建者、主类等。
  • INDEX.LIST文件:该文件包含JAR文件中所有文件的索引列表。
  • SIGNATURE文件:该文件包含JAR文件的数字签名,以确保文件的完整性和安全性。
  • *.SF文件:该文件包含JAR文件中每个文件的数字签名,以确保文件的完整性和安全性。

一句话总结:
确保JAR文件的完整性和安全性,并提供关于JAR文件的元数据信息。这些信息对于Java应用程序的部署和运行都是非常重要的。

  1. org 目录:为 Spring Boot 提供的 spring-boot-loader 项目,它是 java -jar` 启动 Spring Boot 项目的核心,通过将Spring Boot应用程序打包为自包含的可执行JAR文件来简化Spring Boot应用程序的部署。
  2. BOOT-INF/lib 目录:Spring Boot 项目中引入的依赖jar 包存放之处。spring-boot-loader 项目很大的一个作用,就是解决 jar 包里嵌套 jar 的情况,如何加载到其中的类。
  3. BOOT-INF/classes 目录:我们在 Spring Boot 项目中 Java 类所编译的 .class、配置文件等。

2.1 MANIFEST.MF中的jar启动类

Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.2.2
Build-Jdk-Spec: 11
Implementation-Title: campusemploydemo
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.jc.campusemploydemo.CampusemploydemoApplication
Spring-Boot-Version: 2.7.1
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx

前面说过这个文件是一个清单文件,主要声明jar包的元数据信息(版本号、项目名、主要类、依赖库等)。详细解析如下:

Manifest-Version:清单文件的版本号
Created-By:清单文件的创建者
Main-Class:JAR 文件的主类
Class-Path:JAR 文件所依赖的其他 JAR 文件路径
Start-Class:配置Spring Boot 规定的启动类
Implementation-Title:实现的名称
Implementation-Version:实现的版本号
Implementation-Vendor:实现的提供者
Specification-Title:规范的名称
Specification-Version:规范的版本号
Specification-Vendor:规范的提供者
通过 MANIFEST.MF 文件,可以方便地查看和管理 JAR

现在我们来看看Main-ClassStart-Class配置类:

  • Main-Class:这里设置为 spring-boot-loader 项目的 JarLauncher类,进行 Spring Boot 应用的启动。
    JarLauncher:是针对 Spring Boot jar 包的启动类,整体类图如下所示:

其源码如下:

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所提供)方法进行启动。整体的启动逻辑如下图所示:

【SpringBoot源码剥析】| 项目运行原理_hive_03


JarFile.registerUrlProtocolHandler():调用 JarFile 的 registerUrlProtocolHandler() 方法,注册 Spring Boot 自定义的 URLStreamHandler实现类,用于 jar 包的加载读取。

createClassLoader(List archives): 创建自定义的 ClassLoader实现类,用于从 jar 包中加载类。

launch(args, getMainClass(), classLoader):执行我们声明的 Spring Boot 启动类,进行 Spring Boot 应用的启动。

简单来说,就是整一个可以读取 jar 包中类的加载器,保证 BOOT-INF/lib 目录下的类和 BOOT-classes 内嵌的 jar 中的类能够被正常加载到,之后执行 Spring Boot 应用的启动。

2.2 深入剥析

2.2.1 registerUrlProtocolHandler()
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。

  • Start-Class 配置项:Spring Boot 规定的启动类,这里设置为我们定义的 Application 类。

tips:
Main-Class/Start-Class的配置主要是通过Maven插件spring-boot-maven-plugin打包写入了 MANIFEST.MF 文件中,从而让 spring-boot-loader 引导启动 Spring Boot 应用。

2.2.2 createClassLoader(getClassPathArchives())

很明显这个方法是以另一个方法作为参数实现的方法,先来分析下该方法的参数 getClassPathArchives()、它是由 ExecutableArchiveLauncher 所实现,代码如下:

private final Archive archive;

@Override
protected List<Archive> getClassPathArchives() throws Exception {
	// ① 获得所有 Archive
	List<Archive> archives = new ArrayList<>(
			this.archive.getNestedArchives(this::isNestedArchive));
	// ② 后续处理
	postProcessClassPathArchives(archives);
	return archives;
}

protected abstract boolean isNestedArchive(Archive.Entry entry);

protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
2.2.3 launch(args,getMainClass(),classLoader)

我们先来看看参数的方法:

  1. getMainClass()
@Override
	protected String getMainClass() throws Exception {
		//读取文件内容
		Manifest manifest = this.archive.getManifest();
		//
		String mainClass = null;
		if (manifest != null) {
			//获取Start-Class属性
			//
			mainClass = manifest.getMainAttributes().getValue("Start-Class");
		}
		if (mainClass == null) {
			throw new IllegalStateException(
					"No 'Start-Class' manifest entry specified in " + this);
		}
		//返回内容
		return mainClass;
	}

jar 包的 MANIFEST.MF 文件的 Start-Class 配置项,,获得我们设置的 Spring Boot 的启动类。
2. launch(args,getMainClass(),classLoader)
它是由 Launcher 类所实现,代码如下:

/**
	 * Launch the application given the archive file and a fully configured
	 * classloader.
	 * 
	 * @param args
	 *            the incoming arguments
	 * @param mainClass
	 *            the main class to run
	 * @param classLoader
	 *            the classloader
	 * @throws Exception
	 *             if the launch fails
	 */
	protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
		// ① 设置类加载器
		Thread.currentThread().setContextClassLoader(classLoader);
		// ② 创建一个新的Runner
		createMainMethodRunner(mainClass, args, classLoader).run();
	}

该方法负责最终的 Spring Boot 应用真正的启动

  • 处:设置createClassLoader创建的 LaunchedURLClassLoader 作为类加载器,从而保证能够从 jar 加载到相应的类。
  • 处,调用 createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) 方法,创建 MainMethodRunner对象,并执行其 run() 方法来启动 Spring Boot 应用。
  1. MainMethodRunner
public class MainMethodRunner {

	private final String mainClassName;

	private final String[] args;

	/**
	 * Create a new {@link MainMethodRunner} instance.
	 * @param mainClass the main class
	 * @param args incoming arguments
	 */
	public MainMethodRunner(String mainClass, String[] args) {
		this.mainClassName = mainClass;
		this.args = (args != null) ? args.clone() : null;
	}

	public void run() throws Exception {
	    // ① 加载 Spring Boot
		Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
		// ② 反射调用 main 方法
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		mainMethod.invoke(null, new Object[] { this.args });
	}

}

① 通过 LaunchedURLClassLoader 类加载器,加载到我们设置的 Spring Boot 的主启动类。
② 通过反射调用主启动类的 main(String[] args) 方法,启动 Spring Boot 应用。这里也告诉了我们答案,为什么我们通过编写一个带有 main(String[] args) 方法的类,就能够启动 Spring Boot 应用。

  1. run()
public void run() throws Exception {
		//看到这里了
		//main[1] print mainClass
		Class<?> mainClass = Thread.currentThread().getContextClassLoader()
				.loadClass(this.mainClassName);
		//然后获取主方法
		//main[1] print mainMethod
		// mainMethod = "public static void zipkin.server.ZipkinServer.main(java.lang.String[])"
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		//触发方法,因为main是静态函数,所以第一个参数为null
		mainMethod.invoke(null, new Object[] { this.args });
	}

三.总结

SpringBoot Jar包启动原理到这里结束啦,咱们下期见!!!