精尽Spring Boot源码分析 – Jar 包的启动实现

该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好!

建议读者私信我领取相关源码注释SpringBoot源码分析结合阅读!

最好对 Spring 源码有一定的了解,如果该篇内容对您有帮助,麻烦点个“赞”,也可以关注小编,感激不尽~

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以很方便的将我们的 Spring Boot 项目打成 jar 包或者 war 包。

考虑到部署的便利性,我们绝大多数(99.99%)的场景下,都会选择打成 jar 包,这样一来,我们就无需将项目部署于 Tomcat、Jetty 等 Servlet 容器中。

那么,通过 Spring Boot 插件生成的 jar 包是如何运行,并启动 Spring Boot 应用的呢?这个就是本文的目的,我们一起来弄懂 Spring Boot jar 包的运行原理。

这里,我通过 Spring Boot Maven Plugin 生成了一个 jar 包,其里面的结构如下所示:

在这里插入图片描述

  1. BOOT-INF 目录,里面保存了我们自己 Spring Boot 项目编译后的所有文件,其中 classes 目录下面就是编译后的
    .class 文件,包括项目中的配置文件等,lib 目录下就是我们引入的第三方依赖
  2. META-INF 目录,通过 MANIFEST.MF 文件提供 jar 包的元数据,声明 jar 的启动类等信息。每个 Java jar
    包应该是都有这个文件的,参考 Oracle 官方对于 jar 的说明,里面有一个 Main-Class 配置用于指定启动类
  3. org.springframework.boot.loader 目录,也就是 Spring Boot 的
    spring-boot-loader 工具模块,它就是 java -jar xxx.jar 启动 Spring Boot
    项目的秘密所在,上面的 Main-Class 指定的就是该工具模块中的一个类

MANIFEST.MF

META-INF/MANIFEST.MF 文件如下:

Manifest-Version: 1.0
Implementation-Title: spring-boot-study
Implementation-Version: 1.0.0-SNAPSHOT
Built-By: jingping
Implementation-Vendor-Id: org.springframework.boot.demo
Spring-Boot-Version: 2.0.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher # spring-boot-loader 中的启动类
Start-Class: org.springframework.boot.demo.Application # 你的 Spring Boot 项目中的启动类
Spring-Boot-Classes: BOOT-INF/classes/ # 你的 Spring Boot 项目编译后的 .class 文件所在目录
Spring-Boot-Lib: BOOT-INF/lib/ # 你的 Spring Boot 项目所引入的第三方依赖所在目录
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_251
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/info-dependencies/dwzq-info/info-stock-project/sp-provider
复制代码

参考 Oracle 官方对该的说明:

Main-Class:Java 规定的 jar 包的启动类,这里设置为 spring-boot-loader 项目的 JarLauncher 类,进行 Spring Boot 应用的启动

Start-Class:Spring Boot 规定的主启动类,这里通过 Spring Boot Maven Plugin 插件打包时,会设置为我们定义的 Application 启动类

为什么不直接将我们的 Application 启动类设置为 Main-Class 启动呢?

因为通过 Spring Boot Maven Plugin 插件打包后的 jar 包,我们的 .class 文件在
BOOT-INF/classes/ 目录下,在 Java 默认的 jar 包加载规则下找不到我们的 Application
启动类,也就需要通过 JarLauncher 启动加载。

当然,还有一个原因,Java 规定可执行器的 jar 包禁止嵌套其它 jar 包,在 BOOT-INF/lib 目录下有我们 Spring
Boot 应用依赖的所有第三方 jar 包,因此spring-boot-loader 项目自定义实现了 ClassLoader 实现类

LaunchedURLClassLoader,支持加载 BOOT-INF/classes 目录下的 .class 文件,以及 BOOT-INF/lib 目录下的 jar 包。

接下来,我们一起来看看 Spring Boot 的 JarLauncher 这个类

1.JarLauncher

类图:

在这里插入图片描述
上面的 WarLauncher 是针对war包的启动类,和 JarLauncher 差不多,感兴趣的可以看一看,这里我们直接来看到 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) {
		// 只接受 `BOOT-INF/classes/` 目录
		if (entry.isDirectory()) {
			return entry.getName().equals(BOOT_INF_CLASSES);
		}
		// 只接受 `BOOT-INF/lib/` 目录下的 jar 包
		return entry.getName().startsWith(BOOT_INF_LIB);
	}

	/**
	 * 这里是 java -jar 启动 SpringBoot 打包后的 jar 包的入口
	 * 可查看 jar 包中的 META-INF/MANIFEST.MF 文件(该文件用于对 Java 应用进行配置)
	 * 参考 Oracle 官方对于 jar 的说明(https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html)
	 * 该文件其中会有一个配置项:Main-Class: org.springframework.boot.loader.JarLauncher
	 * 这个配置表示会调用 JarLauncher#main(String[]) 方法,也就当前方法
	 */
	public static void main(String[] args) throws Exception {
		// <1> 创建当前类的实例对象,会创建一个 Archive 对象(当前应用),可用于解析 jar 包(当前应用)中所有的信息
		// <2> 调用其 launch(String[]) 方法
		new JarLauncher().launch(args);
	}
}
复制代码

可以看到它有个 main(String[]) 方法,前面说到的 META-INF/MANIFEST.MF 文件中的 Main-Class 配置就是指向了这个类,也就会调用这里的 main 方法,会做下面两件事:

创建一个 JarLauncher 实例对象,在 ExecutableArchiveLauncher 父类中会做以下事情:

public abstract class ExecutableArchiveLauncher extends Launcher {

	private final Archive archive;

	public ExecutableArchiveLauncher() {
		try {
			// 为当前应用创建一个 Archive 对象,可用于解析 jar 包(当前应用)中所有的信息
			this.archive = createArchive();
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
	
	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");
		}
		// 当前 jar 包
		File root = new File(path);
		if (!root.exists()) {
			throw new IllegalStateException("Unable to determine code source archive from " + root);
		}
		// 为当前 jar 包创建一个 JarFileArchive(根条目),需要通过它解析出 jar 包中的所有信息
		// 如果是文件夹的话则创建 ExplodedArchive(根条目)
		return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
	}
}

复制代码
  1. 会为当前应用创建一个 Archive 对象,可用于解析 jar
    包(当前应用)中所有的信息,可以把它理解为一个“根”对象,可以通过它获取我们所需要的类信息
  2. 调用 JarLauncher#launch(String[]) 方法,也就是调用父类 Launcher 的这个方法

launch 方法

public abstract class Launcher {

/**
 * Launch the application. This method is the initial entry point that should be
 * called by a subclass {@code public static void main(String[] args)} method.
 * @param args the incoming arguments
 * @throws Exception if the application fails to launch
 */
protected void launch(String[] args) throws Exception {
	// <1> 注册 URL(jar)协议的处理器
	JarFile.registerUrlProtocolHandler();
	// <2> 先从 `archive`(当前 jar 包应用)解析出所有的 JarFileArchive
	// <3> 创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前 jar 中所有的类
	ClassLoader classLoader = createClassLoader(getClassPathArchives());
	// <4> 获取当前应用的启动类(你自己写的那个 main 方法)
	// <5> 执行你的那个 main 方法
	launch(args, getMainClass(), classLoader);
}
复制代码

会做以下几件事:

  1. 调用 JarFile#registerUrlProtocolHandler() 方法,注册
    URL(jar)协议的处理器,主要是使用自定义的 URLStreamHandler 处理器处理 jar 包
  2. 调用 getClassPathArchives() 方法,先从 archive(当前 jar 包应用)解析出所有的
    JarFileArchive,这个 archive就是在上面创建 JarLauncher 实例对象过程中创建的
  3. 调用 createClassLoader(List) 方法,创建 Spring Boot 自定义的
    ClassLoader 类加载器,可加载当前 jar包中所有的类,包括依赖的第三方包 调用 getMainClass()
    方法,获取当前应用的启动类(你自己写的那个 main 方法所在的 Class 类对象)
  4. 调用 launch(…) 方法,执行你的项目中那个启动类的 main 方法(反射)
  5. 你可以理解为会创建一个自定义的 ClassLoader 类加载器,主要可加载 BOOT-INF/classes 目录下的类,以及
    BOOT-INF/lib 目录下的 jar 包中的类,然后调用你 Spring Boot 应用的启动类的 main 方法

接下来我们逐步分析上面的每个步骤

2.1 registerUrlProtocolHandler 方法

备注:注册 URL(jar)协议的处理器

这个方法在 org.springframework.boot.loader.jar.JarFile 中,这个类是 java.util.jar.JarFile 的子类,对它进行扩展,提供更多的功能,便于操作 jar 包

public static void registerUrlProtocolHandler() {
    // <1> 获取系统变量中的 `java.protocol.handler.pkgs` 配置的 URLStreamHandler 路径
    String handlers = System.getProperty(PROTOCOL_HANDLER, "");
    // <2> 将 Spring Boot 自定义的 URL 协议处理器路径(`org.springframework.boot.loader`)添加至系统变量中
    // JVM 启动时会获取 `java.protocol.handler.pkgs` 属性,多个用 `|` 分隔,以他们作为包名前缀,然后使用 `包名前缀.协议名.Handler` 作为该协议的实现
    // 那么这里就会将 `org.springframework.boot.loader.jar.Handler` 作为 jar 包协议的实现
    System.setProperty(PROTOCOL_HANDLER,
            ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
    // <3> 重置已缓存的 URLStreamHandler 处理器们,避免重复创建
    resetCachedUrlHandlers();
}

复制代码

方法的处理过程如下:

  1. 获取系统变量中的 java.protocol.handler.pkgs 配置的 URLStreamHandler 路径
  2. 将 Spring Boot 自定义的 URL
    协议处理器路径(org.springframework.boot.loader)添加至系统变量中 JVM 启动时会获取
    java.protocol.handler.pkgs 属性,多个用 | 分隔,以他们作为包名前缀,然后使用
    包名前缀.协议名.Handler 作为该协议的实现 那么这里就会将
    org.springframework.boot.loader.jar.Handler 作为 jar 包协议的实现,用于处理 jar 包
  3. 重置已缓存的 URLStreamHandler 处理器们,避免重复创建
private static void resetCachedUrlHandlers() {    try {        URL.setURLStreamHandlerFactory(null);    } catch (Error ex) {        // Ignore    }}
复制代码

2.2 getClassPathArchives 方法

备注:从 archive(当前 jar 包应用)解析出所有的 JarFileArchive

该方法在 org.springframework.boot.loader.ExecutableArchiveLauncher 子类中实现,如下:

@Overrideprotected List<Archive> getClassPathArchives() throws Exception {    // <1> 创建一个 Archive.EntryFilter 类,用于判断 Archive.Entry 是否匹配,过滤 jar 包(当前应用)以外的东西    // <2> 从 `archive`(当前 jar 包)解析出所有 Archive 条目信息    List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));    postProcessClassPathArchives(archives);    // <3> 返回找到的所有 JarFileArchive    // `BOOT-INF/classes/` 目录对应一个 JarFileArchive(因为就是当前应用中的内容)    // `BOOT-INF/lib/` 目录下的每个 jar 包对应一个 JarFileArchive    return archives;}
复制代码

过程如下:

创建一个 Archive.EntryFilter 实现类,用于判断 Archive.Entry 是否匹配,过滤掉 jar 包(当前应用)以外的东西

  1. public class JarLauncher extends ExecutableArchiveLauncher { 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); }}
  2. 从 archive(当前 jar 包)解析出所有 Archive 条目信息,这个 archive 在上面 1. JarLauncher
    讲到过,创建 JarLauncher 实例化对象的时候会初始化 archive,是一个 JarFileArchive
    对象,也就是我们打包后的 jar 包,那么接下来需要从中解析出所有的 Archive 对象
// JarFileArchive.java@Overridepublic List<Archive> getNestedArchives(EntryFilter filter) throws IOException {    List<Archive> nestedArchives = new ArrayList<>();    // 遍历 jar 包(当前应用)中所有的 Entry    for (Entry entry : this) {        // 进行过滤,`BOOT-INF/classes/` 目录或者 `BOOT-INF/lib/` 目录下的 jar 包        if (filter.matches(entry)) {            // 将 Entry 转换成 JarFileArchive            nestedArchives.add(getNestedArchive(entry));        }    }    // 返回 jar 包(当前应用)找到的所有 JarFileArchive    // `BOOT-INF/classes/` 目录对应一个 JarFileArchive(因为就是当前应用中的内容)    // `BOOT-INF/lib/` 目录下的每个 jar 包对应一个 JarFileArchive    return Collections.unmodifiableList(nestedArchives);}
复制代码

返回 jar 包(当前应用)找到的所有 JarFileArchive:

  • BOOT-INF/classes/ 目录对应一个 JarFileArchive(因为就是当前 Spring Boot 应用编译后的内容)
  • BOOT-INF/lib/ 目录下的每个 jar 包对应一个 JarFileArchive

返回从 jar 包中找到的所有 JarFileArchive
这一步骤就是从 jar 包中解析出我们需要的东西来,如上描述,每个 JarFileArchive 会对应一个 JarFile 对象

2.3 createClassLoader 方法

备注:创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前 jar 中所有的类

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {    // <1> 获取所有 JarFileArchive 对应的 URL    List<URL> urls = new ArrayList<>(archives.size());    for (Archive archive : archives) {        urls.add(archive.getUrl());    }    // <2> 创建 Spring Boot 自定义的 ClassLoader 类加载器,并设置父类加载器为当前线程的类加载器    // 通过它解析这些 URL,也就是加载 `BOOT-INF/classes/` 目录下的类和 `BOOT-INF/lib/` 目录下的所有 jar 包    return createClassLoader(urls.toArray(new URL[0]));}protected ClassLoader createClassLoader(URL[] urls) throws Exception {    return new LaunchedURLClassLoader(urls, getClass().getClassLoader());}
复制代码

该过程如下:

  1. 获取所有 JarFileArchive 对应的 URL 创建 Spring Boot 自定义的 ClassLoader
    类加载器,并设置父类加载器为当前线程的类加载器
  2. 可以看到 LaunchedURLClassLoader 为自定义类加载器,这样就能从我们 jar 包中的
    BOOT-INF/classes/ 目录下和 BOOT-INF/lib/ 目录下的所有三方依赖包中加载出 Class 类对象

2.4 getMainClass 方法

备注:获取当前应用的启动类(你自己写的那个 main 方法)

// ExecutableArchiveLauncher.java@Overrideprotected String getMainClass() throws Exception {    // 获取 jar 包(当前应用)的 Manifest 对象,也就是 META-INF/MANIFEST.MF 文件中的属性    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;}
复制代码

过程如下:

  1. 获取 jar 包(当前应用)的 Manifest 对象,也就是 META-INF/MANIFEST.MF 文件中的属性
  2. 获取启动类(当前应用自己的启动类),也就是 Start-Class 配置,并返回

可以看到,这一步就是找到你 Spring Boot 应用的启动类,前面 ClassLoader 类加载器都准备好了,那么现在不就可以直接调用这个类的 main 方法来启动应用了

2.5 launch 方法

备注:执行你的 Spring Boot 应用的启动类的 main 方法

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {    // 设置当前线程的 ClassLoader 为刚创建的类加载器    Thread.currentThread().setContextClassLoader(classLoader);    // 创建一个 MainMethodRunner 对象(main 方法执行器)    // 执行你的 main 方法(反射)    createMainMethodRunner(mainClass, args, classLoader).run();}protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {    return new MainMethodRunner(mainClass, args);}
复制代码

整个过程很简单,先设置当前线程的 ClassLoader 为刚创建的类加载器,然后创建一个 MainMethodRunner
对象(main方法执行器),执行你的main方法(反射),启动 Spring Boot 应用

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 {		// 根据名称加载 main 方法所在类的 Class 对象		Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);		// 获取 main 方法		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);		// 执行这个 main 方法(反射)		mainMethod.invoke(null, new Object[] { this.args });	}}
复制代码

这里就是通过反射调用你的 Spring Boot 应用的启动类的 main 方法

LaunchedURLClassLoader

org.springframework.boot.loader.LaunchedURLClassLoader 是 spring-boot-loader 中自定义的类加载器,实现对 jar 包中 BOOT-INF/classes 目录下的类和 BOOT-INF/lib 下第三方 jar 包中的类的加载。

public class LaunchedURLClassLoader extends URLClassLoader {	static {		ClassLoader.registerAsParallelCapable();	}	/**	 * Create a new {@link LaunchedURLClassLoader} instance.	 * @param urls the URLs from which to load classes and resources	 * @param parent the parent class loader for delegation	 */	public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {		super(urls, parent);	}	/**	 * 重写类加载器中加载 Class 类对象方法	 */	@Override	protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {		Handler.setUseFastConnectionExceptions(true);		try {			try {				// 判断这个类是否有对应的 Package 包				// 没有的话会从所有 URL(包括内部引入的所有 jar 包)中找到对应的 Package 包并进行设置				definePackageIfNecessary(name);			}			catch (IllegalArgumentException ex) {				// Tolerate race condition due to being parallel capable				if (getPackage(name) == null) {					// This should never happen as the IllegalArgumentException indicates					// that the package has already been defined and, therefore,					// getPackage(name) should not return null.					throw new AssertionError("Package " + name + " has already been defined but it could not be found");				}			}			// 加载对应的 Class 类对象			return super.loadClass(name, resolve);		}		finally {			Handler.setUseFastConnectionExceptions(false);		}	}	/**	 * Define a package before a {@code findClass} call is made. This is necessary to	 * ensure that the appropriate manifest for nested JARs is associated with the	 * package.	 * @param className the class name being found	 */	private void definePackageIfNecessary(String className) {		int lastDot = className.lastIndexOf('.');		if (lastDot >= 0) {			// 获取包名			String packageName = className.substring(0, lastDot);			// 没找到对应的 Package 包则进行解析			if (getPackage(packageName) == null) {				try {					// 遍历所有的 URL,从所有的 jar 包中找到这个类对应的 Package 包并进行设置					definePackage(className, packageName);				}				catch (IllegalArgumentException ex) {					// Tolerate race condition due to being parallel capable					if (getPackage(packageName) == null) {						// This should never happen as the IllegalArgumentException						// indicates that the package has already been defined and,						// therefore, getPackage(name) should not have returned null.						throw new AssertionError(								"Package " + packageName + " has already been defined but it could not be found");					}				}			}		}	}	private void definePackage(String className, String packageName) {		try {			AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {				// 把类路径解析成类名并加上 .class 后缀				String packageEntryName = packageName.replace('.', '/') + "/";				String classEntryName = className.replace('.', '/') + ".class";				// 遍历所有的 URL(包括应用内部引入的所有 jar 包)				for (URL url : getURLs()) {					try {						URLConnection connection = url.openConnection();						if (connection instanceof JarURLConnection) {							JarFile jarFile = ((JarURLConnection) connection).getJarFile();							// 如果这个 jar 中存在这个类名,且有对应的 Manifest							if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null									&& jarFile.getManifest() != null) {								// 定义这个类对应的 Package 包								definePackage(packageName, jarFile.getManifest(), url);								return null;							}						}					}					catch (IOException ex) {						// Ignore					}				}				return null;			}, AccessController.getContext());		}		catch (java.security.PrivilegedActionException ex) {			// Ignore		}	}}
复制代码

上面的代码就不一一讲述了,LaunchedURLClassLoader 重写了 ClassLoader 的
loadClass(String, boolean) 加载 Class 类对象方法,在加载对应的 Class
类对象之前新增了一部分逻辑,会尝试从 jar 包中定义 Package 包对象,这样就能加载到对应的 Class 类对象。

总结

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以很方便的将我们的 Spring Boot 项目打成 jar 包,jar 包中主要分为三个模块:

  • BOOT-INF 目录,里面保存了我们自己 Spring Boot 项目编译后的所有文件,其中 classes 目录下面就是编译后的
    .class 文件,包括项目中的配置文件等,lib 目录下就是我们引入的第三方依赖
  • META-INF 目录,通过 MANIFEST.MF 文件提供 jar 包的元数据,声明 jar 的启动类等信息。每个 Java jar
    包应该是都有这个文件的,参考 Oracle 官方对于 jar 的说明,里面有一个 Main-Class 配置用于指定启动类
  • org.springframework.boot.loader 目录,也就是 Spring Boot 的
    spring-boot-loader 子模块,它就是 java -jar xxx.jar 启动 Spring Boot
    项目的秘密所在,上面的 Main-Class 指定的就是里面的一个类
  • 通过 java -jar 启动应用时,根据 Main-Class 配置会调用
    org.springframework.boot.loader.JarLauncher 的main(String[])
    方法;其中会先创建一个自定义的 ClassLoader 类加载器,可从BOOT-INF目录下加载出我们 Spring Boot 应用的
    Class 类对象,包括依赖的第三方 jar 包中的 Class 类对象;然后根据 Start-Class 配置调用我们 Spring
    Boot 应用启动类的 main(String[]) 方法(反射),这样也就启动了应用,至于我们的 main(String[])
    方法中做了哪些事情,也就是后续所讲的内容。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享