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

tech2026-04-17  2

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的继承关系如下:

启动器实现原理

启动类: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文件的一个标准规范

最新回复(0)