1-SpringBoot启动详解,loader机制详细分析

  • Post author:
  • Post category:其他





我准备战斗到最后,不是因为我勇敢,是我想见证一切。 –双雪涛《猎人》


Thinking

  1. 一个技术,为什么要用它,解决了那些问题?
  2. 如果不用会怎么样,有没有其它的解决方法?
  3. 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
  4. 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
  5. 这些问题你又如何去解决的呢?


声明:本文基于springboot 2.1.3.RELEASE

正文的第一个疑问就是:

  1. springboot 为什么可以直接使用jar 运行?
  2. 那么jar包的内部解构是怎么样的?
  3. springboot 的启动过程又是怎么样的?

这里首先声明在java的归档文件中(jar)的规定:

​ 在一个jar文件当中,我们必须要将指定为

Main-Class

的那个类作为顶层的jar文件(就是该启动类的jar文件必须放置在整个解压目录或者jar文件内部的顶层目录下),意思就是,执行的

Main-Class

是不允许被嵌套的。

​ 对于一个

Jar

文件来说,被指定为入口类

Main-Class

的一个具体类,那么这个类连同他的包结构,一定是位于这个

Jar

文件的顶层目录,而不能再位于其他的子目录中。

  • 以上两种说法,强调了SpringBoot就存在了问题,在一个打包完jar文件后,只能允许一个

    Main-Class

    入口类,可是我们自己写的启动类是根本不具备启动整个项目的实力的,那么

    SpringBoot

    是怎么完美的避开这种协议,实现jar包中嵌套用户启动类,并且可以完美随处运行的呢?



1、SpringBoot jar包的内部结构

​ 将打包好的Jar包,使用命令解压出来

image-20200524174620780

image-20200524174940413

  1. 存放的是 配置文件和编写的字节码文件
  2. 存放的是自动生成的一些配置信息(类似于:maven配置,jdk环境、os环境等)

    you should not put anything into META-INF yourself. Instead
  3. 存放的是真正的springboot 的启动文件的字节码文件

    1. image-20200524175422956
    2. 大致可以看到在该启动类下的路径,尝试使用该路径引入jar依赖
        <!--引入 springboot 启动依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-loader</artifactId>
        </dependency>

引入该jar后,可以简单的发现,该jar包的所有路径都是跟解压出来的jar的loader路径下的内容相同的。

image-20200524180459535

不难发现了,speingboot正是使用的

spring-boot-loader

这个jar来完成springboot 的整个启动加载动作的。

在源码面前,所有的花哨都是扯淡的😄,来🐱一眼里面最显眼的

JarLauncher

类。

image-20200524180811303

是不是一眼就感觉到亲切无比了哈?😄

这里声明一点,我们引入的

spring-boot-loader

是不会影响springboot 的启动的。但是会在加载的lib目录下生成jar文件,我们重新打包一次,🐱一眼。

image-20200524181433685

其他目录并没有什么改变。

至此,大概的了解了,springboot 打包为jar后的整个目录样子。

下面,探讨一下这种路径的优点,或者说为什么是这样的结构



2、SpringBoot-启动原理



2.1、为什么SpringBoot打包的jar可以直接运行

当执行

java -jar spring_lecture-0.0.1.jar

命令时,整个jar的入口是什么?

​ 在上面简单的查看了一下整个jar包的目录结构,在Java运行jar时,是根据

MATA-INF

中的

.MF

文件,寻找到

Main-Class

指定的全限定名进行加载的。

<jar ...>
    <manifest>
        <attribute name="Main-Class" value="MyApplication"/>
    </manifest>
</jar>

​ 所以,在jar文件中,

MATA-INF

中的

.MF

文件定义了启动类

image-20200524205820568


Main-class

:即为整个jar文件的启动类(入口类)。


Start-Class

:就是我们自己定义的springboot启动类了。

可以看出来,整个文件都是以

key-value

形式的数据储存的。

所以在上面,引入了

org.springframework.boot.loader.JarLauncher

jar,就是为了可以看到该jar的源码的。🙂



2.2、JarLauncher 流程详解


org.springframework.boot.loader.JarLauncher

Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.

该launcher是基于jar协议的,

它假设依赖的jar文件是位于/BOOT-INF/lib下的

工程自己的依赖的jar是位于 /BOOT-INF/classes目录下的。

这样就可以看出来对应指定的两个目录

image-20200524212131058

因此可以看出,这两个目录并不符合java对加载jar文件的标准,这是spring自己定义的两个文件目录

可以看出,在classes目录下是我们自己定义的文件,而lib目录是整个项目依赖的第三方jar包。



2.2.1、JarLauncher源码


image-20200524212635427

可以看到在

ExecutableArchiveLauncher

下有两个实现,侧面说明了,springboot 也支持将工程打包成

war

包的。

  • 现在我们就从

    JarLauncher

    类的

    Main

    方法入手,一步一步探索springboot 是如何巧妙的实现在规避java jar文件的规格下完成加载的。
	public static void main(String[] args) throws Exception {
		new JarLauncher().launch(args);
	}

该main方法对当前类进行初始化,在类加载中,会先初始化父类。先来🐱一眼在顶层类中的初始化情况。


Launcher

类初始化时,并没有做过多的初始化动作。


ExecutableArchiveLauncher

:一个可以执行的基类Launcher档案。该类在初始化时,会创建出指定的目录

	public ExecutableArchiveLauncher() {
		try {
			this.archive = createArchive();
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
	// 定位当前被执行的那个jar文件的,jar文件的绝对路径
	protected final Archive createArchive() throws Exception {
		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);
		}
		return (root.isDirectory() ? new ExplodedArchive(root)
				: new JarFileArchive(root));
	}

image-20200524214742832

在基类的初始化中,可以看到是为了将本地的jar文件读取到,并且根据文件类型创建

archive

image-20200524215156637

  • 发现一个好玩的东西:在

    Debug

    执行

    org.springframework.boot.loader.JarLauncher#main

    后,会报
  • image-20200524215311562
  • 可以看到,在创建完

    JarLauncher

    后,该对象会去指定的

    MATA-INF

    中的

    .MF

    文件中寻找

    Start-Class

    指定的类
  • 在父子 类都初始化完成后,调用

    org.springframework.boot.loader.Launcher#launch(java.lang.String[])

    方法。
	/**
	 * 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 {
		JarFile.registerUrlProtocolHandler();
		ClassLoader classLoader = createClassLoader(getClassPathArchives());
		launch(args, getMainClass(), classLoader);
	}

在doc文档中也说的很清楚了,这是一个应用的Launch,该方法是一个实现的入口,应该由该类的子类使用Main函数直接调用。

	/**
	 * 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() {
		String handlers = System.getProperty(PROTOCOL_HANDLER, "");
		System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
				: handlers + "|" + HANDLERS_PACKAGE));
		resetCachedUrlHandlers();
	}
  • image-20200524221017911

  • ClassLoader classLoader = createClassLoader(getClassPathArchives());

    创建类加载器

image-20200524221609264


  • launch(args, getMainClass(), classLoader);

    • 该方法中的

      getMainClass()

      会获取,启动清单,可以看出这里就是获取用户定义的入口类文件

      Main-Class

      类,见名知意!
    • 但是SpringBoot在其中耍了一个小聪明,将两个启动类嵌套,并且将启动本身

      Jar

      的类加载器指定去加载用户定义的

      Main-class

      并起名为

      Start-Class

      ,这样一来,完美的实现了两个启动类嵌套并且绕开了java jar协议的情况下,实现了整个springboot 打包的jar的运行环境(

      还有一个伏笔就是,为什么我们引入的lauch jar 是不会影响springboot 程序的运行的,并且官方也没有引入该依赖。

      见下文)。
    • image-20200524222347698
    • image-20200524222714764
    • 到这一步,就是将所有的准备工作全部做好了,所有类路径都初始化完毕了。



2.2.2、Springboot jar 的思想



ClassLoader classLoader = createClassLoader(getClassPathArchives());

该行就是整个springBoot运行jar的核心思想。


org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives

	@Override
	protected List<Archive> getClassPathArchives() throws Exception {
		List<Archive> archives = new ArrayList<>(
				this.archive.getNestedArchives(this::isNestedArchive)); // 方法引用
		postProcessClassPathArchives(archives);
		return archives;
	}	
@Override
	public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
		List<Archive> nestedArchives = new ArrayList<>();
		for (Entry entry : this) {
			if (filter.matches(entry)) {
				nestedArchives.add(getNestedArchive(entry));
			}
		}
		return Collections.unmodifiableList(nestedArchives);
	}
	@Override
	protected boolean isNestedArchive(Archive.Entry entry) {
		if (entry.isDirectory()) {
			return entry.getName().equals(BOOT_INF_CLASSES);
		}
		return entry.getName().startsWith(BOOT_INF_LIB);
	}

	static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

	static final String BOOT_INF_LIB = "BOOT-INF/lib/";

  • 用于匹配所有的

    BOOT-INF/classes/

    的用户文件,

    BOOT-INF/lib/

    下所有的

    jar

    文件。
  • 但是从上面第一节就知道了,在SpringBoot 的整个jar文件中顶层是有三个文件夹的,那么为什么要这样设计呢?

    • 原因就是在于Java对于jar文件的加载有协议,在前言就有说到, 对于一个

      Jar

      文件来说,被指定为入口类

      Main-Class

      的一个具体类,那么这个类连同他的包结构,一定是位于这个

      Jar

      文件的顶层目录,而不能再位于其他的子目录中。所以,Java在jar目录中是不允许jar文件嵌套的,那么拥有其他

      Main-Class

      的类是无法被启动的。
    • 所以SpringBoot就想到了一种解决方案,就是将SpringBoot的启动的Jar文件里面的内容单独复制出来一份,放在目录的根目录,这样一来,SpringBoot的入口类:

      org.springframework.boot.loader.JarLauncher

      是服务Java Jar规范的,是可以被Java的类加载器加载到的。
  • 那么就引出了一个问题,在Java jar规范中,

    org.springframework.boot.loader.JarLauncher

    被加载后,是不允许其他文件或者jar被加载的,所以,SpringBoot使用的了自己的

    SpringBootClassLoader



    LaunchedURLClassLoader

    来加载用户的入口类,和程序所依赖的所有jar包。
  • 这样一来,即符合了Java jar 的规范,又在这种协议中开辟了一条蹊径,实现了一个单独的jar,不需要单独的容器就可以实现运行了。
  • 所以基于上述的思想,必须将SpringBoot,所依赖的

    Main-Class



    org.springframework.boot.loader.JarLauncher

    类中的所有文件单独辅助出来,才能实现。
  • 如果不全部复制出来,将所有的jar 放在一个文件家中,就会出现jar文件嵌套的问题,这样一来,就完成不了多jar文件的运行。

这种多Jar运行的模式在SpringBoot中有一个专有名词就是

FatJar

意味:在SpringBoot打包的jar文件中,是一个包含多个jar包的嵌套jar,是一个大胖子 🐖。


惊叹SpringBoot 的这种设计思想。不仅实现了web程序的多样化,还简化了环境配置的麻烦还有报错的可能性。

下节将会详细介绍SpringBoot是如何使用自己的类加载器去加载这些第三方类的。

本文仅供笔者本人学习,有错误的地方还望指出,一起进步!望海涵!


转载请注明出处!

欢迎关注我的公共号,无广告,不打扰。不定时更新Java后端知识,我们一起超神。

qrcode.jpg

——努力努力再努力xLg

加油!



版权声明:本文为Aussme原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。