终于搞懂了SpringBoot jar包启动的原理

SpringBoot生成的jar包

Spring Boot的可执行jar包又称作“fat jar”,是包含所有三方依赖的jar。它与传统jar包最大的不同是包含了一个lib目录和内嵌了web容器。

可执行jar包的目录结构

通过maven命令打包后,会有2个jar包,一个为application-name.version-SNAPSHOT.jar,一个为application-name.version-SNAPSHOT.jar.original。后者仅包含应用编译后的本地资源,而前者引入了相关的第三方依赖。

将前者解压后的目录结构如下:

在这里插入图片描述

该目录比使用传统jar命令打包结构更复杂一些,目录含义如下:

  • BOOT-INF/classes:目录存放应用编译后的class文件。
  • BOOT-INF/lib:目录存放应用依赖的第三方JAR包文件。
  • META-INF:目录存放应用打包信息(Maven坐标、pom文件)和MANIFEST.MF文件。
  • org:目录存放SpringBoot相关class文件。

配置文件:MANIFEST.MF

MANIFEST.MF文件位于jar包的META-INF文件夹内,内容如下:

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: cindy
Start-Class: com.shinemo.wangge.web.MainApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.3.3.RELEASE
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_212
Main-Class: org.springframework.boot.loader.JarLauncher

Main-Class这个属性对应的class的main方法是作为程序入口启动应用的。

Start-Class这个属性定义了我们项目的启动类。

可执行jar包启动器:JarLauncher

当使用java -jar命令执行Spring Boot应用的可执行jar文件时,该命令引导标准可执行的jar文件,读取在jar中META-INF/MANIFEST.MF文件的Main-Class属性值,该值代表应用程序执行入口类也就是包含main方法的类。

从MANIFEST.MF文件内容可以看到,Main-Class这个属性定义了org.springframework.boot.loader.JarLauncher,JarLauncher就是对应Jar文件的启动器。而我们项目的启动类MainApplication定义在Start-Class属性中,

JarLauncher会将BOOT-INF/classes下的类文件和BOOT-INF/lib下依赖的jar加入到classpath下,然后调用META-INF/MANIFEST.MF文件Start-Class属性完成应用程序的启动。

Launcher的继承关系如下:

image-20200901174219024

启动器实现原理

启动类:JarLauncher

//JarLauncher.java

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

}

JarLauncher默认构造函数实现为空,它父类ExecutableArchiveLauncher会调用再上一级父类Launcher的createArchive方法加载jar包, 加载了jar包之后,我们就能获取到里面所有的资源。

	//JarLauncher.java
	
	//JarLauncher默认构造函数
	public JarLauncher() {
	}
	
//ExecutableArchiveLauncher.java	

public ExecutableArchiveLauncher() {
		try {
      //开始加载jar包
			this.archive = createArchive();
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
//Launcher.java	

protected final Archive createArchive() throws Exception {
    //通过获取当前Class类的信息,查找到当前归档文件的路径
		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);
		}
    //如果是目录,则创建ExplodedArchive,否则创建JarFileArchive
		return (root.isDirectory() ? new ExplodedArchive(root)
				: new JarFileArchive(root));
	}

核心方法:launch(String[] args)

launch方法实际上是调用父类Launcher的launch方法

// Launcher.java	

protected void launch(String[] args) throws Exception {
    //注册 Spring Boot 自定义的 URLStreamHandler 实现类,用于 jar 包的加载读取, 可读取到内嵌的jar包
		JarFile.registerUrlProtocolHandler();
    //创建自定义的 ClassLoader 实现类,用于从 jar 包中加载类。
		ClassLoader classLoader = createClassLoader(getClassPathArchives());
    //执行我们声明的 Spring Boot 启动类,进行 Spring Boot 应用的启动。
		launch(args, getMainClass(), classLoader);
	}

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

方法一:registerUrlProtocolHandler
JarFile.registerUrlProtocolHandler();
// JarFile.java

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

该方法的目的就是通过将 org.springframework.boot.loader 包设置到 "java.protocol.handler.pkgs" 环境变量,从而使用到自定义的 URLStreamHandler 实现类 Handler,处理 jar: 协议的 URL。

利用java url协议实现扩展原理,自定义jar协议
将org.springframework.boot.loader包 追加到java系统 属性java.protocol.handler.pkgs中,实现自定义jar协议

java会在java.protocol.handler.pkgs系统属性指定的包中查找与协议同名的子包和名为Handler的类,
即负责处理当前协议的URLStreamHandler实现类必须在 <包名>.<协议名定义的包> 中,并且类名称必须为Handler
例如:
org.springframework.boot.loader.jar.Handler这个类 将用于处理jar协议

这个jar协议实现作用:
默认情况下,JDK提供的ClassLoader只能识别jar中的class文件以及加载classpath下的其他jar包中的class文件。
对于jar包中的jar包是无法加载的
所以spring boot 自己定义了一套URLStreamHandler实现类和JarURLConnection实现类,用来加载jar包中的jar包的class类文件

举个例子:

jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/

jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class

我们看到如果有 jar 包中包含 jar,或者 jar 包中包含 jar 包里面的 class 文件,那么会使用 !/分隔开,这种方式只有 org.springframework.boot.loader.jar.Handler 能处理,它是 SpringBoot 内部扩展出来一种URL协议.

通常,jar里的资源分隔符是!/,在JDK提供的JarFile URL只支持一层“!/”,而Spring Boot扩展了该协议,可支持多层“!/”。

方法二:createClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchives());
getClassPathArchives()
// ExecutableArchiveLauncher.java

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

<1> 处,this::isNestedArchive 代码段,创建了 EntryFilter 匿名实现类,用于过滤 jar 包不需要的目录。目的就是过滤获得,BOOT-INF/classes/ 目录下的类,以及 BOOT-INF/lib/ 的内嵌 jar 包。

// JarLauncher.java

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

<1>处getNestedArchives()方法实现

	//JarFileArchive.java
	
	@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);
	}
createClassLoader(List archives)
// ExecutableArchiveLauncher.java

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
 // 获得所有 Archive 的 URL 地址
    List<URL> urls = new ArrayList<>(archives.size());
 for (Archive archive : archives) {
  urls.add(archive.getUrl());
 }
 // 创建加载这些 URL 的 ClassLoader
 return createClassLoader(urls.toArray(new URL[0]));
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
 return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

基于获得的 Archive 数组,创建自定义 ClassLoader 实现类 LaunchedURLClassLoader,通过它来加载 BOOT-INF/classes 目录下的类,以及 BOOT-INF/lib 目录下的 jar 包中的类。

方法三:launch(String[] args, String mainClass, ClassLoader classLoader)
launch(args, getMainClass(), classLoader);
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
  throws Exception {
    // <1> 设置 LaunchedURLClassLoader 作为类加载器
 Thread.currentThread().setContextClassLoader(classLoader);
 // <2> 创建 MainMethodRunner 对象,并执行 run 方法,启动 Spring Boot 应用
 createMainMethodRunner(mainClass, args, classLoader).run();
}

<1> 处:设置 LaunchedURLClassLoader 作为类加载器,从而保证能够从 jar 包中加载到相应的类。

getMainClass()
// ExecutableArchiveLauncher.java

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

jar 包的 MANIFEST.MF 文件的 Start-Class 配置项,,获得我们设置的 Spring Boot 的启动类。

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

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

SpringBoot自定义的类加载器: LaunchedURLClassLoader

LaunchedURLClassLoader 是 spring-boot-loader 项目自定义的类加载器,实现对 jar 包中 META-INF/classes 目录下的META-INF/lib 内嵌的 jar 包中的加载

该ClassLoader继承自UrlClassLoader。UrlClassLoader加载class就是依靠初始参数传入的Url数组,并且尝试从Url指向的资源中加载Class文件

//LaunchedURLClassLoader.java

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException {
   Handler.setUseFastConnectionExceptions(true);
   try {
      try {
          //尝试根据类名去定义类所在的包,即java.lang.Package,确保jar in jar里匹配的manifest能够和关联的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.

            //这里异常表明,definePackageIfNecessary方法的作用实际上是预先过滤掉查找不到的包
            throw new AssertionError("Package " + name + " has already been "
                  + "defined but it could not be found");
         }
      }
      return super.loadClass(name, resolve);
   }
   finally {
      Handler.setUseFastConnectionExceptions(false);
   }
}

方法super.loadClass(name, resolve)实际上会回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循双亲委派机制进行查找类,而Bootstrap ClassLoader和Extension ClassLoader将会查找不到fat jar依赖的类,最终会来到Application ClassLoader,调用java.net.URLClassLoader#findClass

为什么要引入自定义类加载器

因为SpringBoot实现了Jar包的嵌套,一个Jar包就可以完成整个程序的运行。

引入自定义类加载器就是为了能解决jar包嵌套jar包的问题,系统自带的AppClassLoarder不支持读取嵌套jar包

为什么SpringBoot要将Loader 类下的所有文件复制出来呢

因为程序毕竟要有一个启动入口,这个入口要由应用类加载器加载,先将SpringBoot Class Loader加载到内存中,然后通过后续的一些操作创建线程上下文加载器,去加载第三方jar。

如果将SpringBoot Class Loader 也放到lib文件下,是根本无法被加载到的,因为它根本不符合jar文件的一个标准规范

  • 11
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值