SpringBoot JAR 启动原理
文章目录
- 版本
- 概述
- JAR 包结构
- `MANIFEST.MF` 描述文件
- JarLauncher
- `Archive` 接口
- `launch` 方法
- `Handlers.register()` 方法
- `getClassPathUrls` 方法
- `createClassLoader` 方法
- 时序图
- 参考
版本
- Java 17
- SpringBoot 3.2.4
概述
JAR 启动原理可以简单理解为“java -jar
的启动原理”
SpringBoot 提供了 Maven 插件 spring-boot-maven-plugin
,可以将 SpringBoot 项目打包成 JAR 包,这个跟普通 JAR 包有所不同
- 普通 JAR 包:可以被其他项目引用,解压后就是包名,包里就是代码
- SpringBoot 打包的 JAR 包:只能运行,不能被其他项目依赖,包里
\BOOT-INF\classes
目录才是代码
使用 maven package
指令执行打包命令,将项目打包成 JAR 包,根据 pom.xml
文件中的 name
和 version
标签作为 JAR 包名称,比如项目的
pom.xml
配置文件有 <name>springboot-demo</name>
和 <version>0.0.1-SNAPSHOT</version>
,执行 maven package
命令之后打包出来之后为 springboot-demo-0.0.1-SNAPSHOT.jar
和 springboot-demo-0.0.1-SNAPSHOT.jar.original
springboot-demo-0.0.1-SNAPSHOT.jar
之类的 JAR 包:spring-boot-maven-plugin
生成的 JAR 包。包含了应用的第三方依赖,SpringBoot 相关的类,存在嵌套的 JAR 包,称之为 executable jar 或 fat jar。也就是最终可运行的 SpringBoot 的 JAR 包。可以直接执行java -jar
指令启动springboot-demo-0.0.1-SNAPSHOT.jar.original
之类的 JAR 包:默认maven-jar-plugin
生成的 JAR 包,仅包含编译用的本地文件。也就是打包之前生成的原始 JAR 包,仅包含你项目本身的 class 文件和资源文件,不包含依赖项,也不具备 Spring Boot 的启动结构。通常由 Spring Boot Maven 插件在打包过程中中间步骤生成,Spring Boot 会在这个基础上重打包(repackage)为可运行的 JAR 文件。
JAR 包结构
springboot-demo-0.0.1-SNAPSHOT.jar
之类的 JAR 包中通常包括 BOOT-INF
,META-INF
,org
三个文件夹
META-INF
:通过MANIFEST.MF
文件提供jar
包的元数据,声明了 JAR 的启动类org
:为 SpringBoot 提供的spring-boot-loader
项目,它是java -jar
启动 Spring Boot 项目的秘密所在BOOT-INF/lib
:SpringBoot 项目中引入的依赖的 JAR 包,目的是解决 JAR 包里嵌套 JAR 的情况,如何加载到其中的类BOOT-INF/classes
:Java 类所编译的.class
、配置文件等等
应用程序类应该放在嵌套的BOOT-INF/classes目录中。依赖项应该放在嵌套的BOOT-INF/lib目录中。
├── BOOT-INF // 文件目录存放业务相关的,包括业务开发的类和配置文件,以及依赖的 JAR
│ ├── classes
│ │ ├── application.yaml
│ │ └── com
│ │ └── example
│ │ └── springbootdemo
│ │ ├── OrderProperties.class
│ │ ├── SpringbootDemoApplication.class // 启动类
│ │ ├── SpringbootDemoApplication$OrderPropertiesCommandLineRunner.class
│ │ ├── SpringbootDemoApplication$ValueCommandLineRunner.class
│ │ ├── SpringMVCConfiguration.class
│ │ └── vo
│ │ └── UserVO.class
│ ├── classpath.idx
│ ├── layers.idx
│ └── lib
│ ├── spring-aop-6.1.6.jar
│ ├── spring-beans-6.1.6.jar
│ ├── spring-boot-3.2.5.jar
│ ├── spring-boot-autoconfigure-3.2.5.jar
│ ├── spring-boot-jarmode-layertools-3.2.5.jar
├── META-INF // MANIFEST.MF 描述文件和 maven 的构建信息
│ ├── MANIFEST.MF
│ ├── maven
│ │ └── com.example
│ │ └── springboot-demo
│ │ ├── pom.properties // 配置文件
│ │ └── pom.xml
│ ├── services
│ │ └── java.nio.file.spi.FileSystemProvider
│ └── spring-configuration-metadata.json
└── org└── springframework└── boot└── loader // SpringBoot loader 相关类├── jar│ ├── ManifestInfo.class│ ├── MetaInfVersionsInfo.class├── jarmode│ └── JarMode.class├── launch│ ├── Archive.class│ ├── Archive$Entry.class├── log│ ├── DebugLogger.class│ ├── DebugLogger$DisabledDebugLogger.class│ └── DebugLogger$SystemErrDebugLogger.class├── net│ ├── protocol│ │ ├── Handlers.class│ │ ├── jar│ │ │ ├── Canonicalizer.class│ │ │ ├── Handler.class│ │ └── nested│ │ ├── Handler.class│ │ ├── NestedLocation.class│ └── util│ └── UrlDecoder.class├── nio│ └── file│ ├── NestedByteChannel.class│ ├── NestedByteChannel$Resources.class├── ref│ ├── Cleaner.class│ └── DefaultCleaner.class└── zip├── ByteArrayDataBlock.class├── CloseableDataBlock.class
MANIFEST.MF
描述文件
MANIFEST.MF
是 Java JAR(Java Archive)文件中的一个核心元数据文件,用于描述 JAR 包的配置信息和依赖关系。它位于 JAR 文件内部的 META-INF/
目录下,是 JVM 启动可执行 JAR 或加载依赖的关键依据。
java -jar
命令引导的具体启动类必须配置在 MANIFEST.MF
描述文件中的 Main-Class
属性中,该命令用来引导标准执行的 JAR 文件,读取的就是 MANIFEST.MF
文件中的 Main-Class
属性值,Main-Class
属性就是定义包含了 main
方法的类代表了应用程序执行入口类
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.3.0
Build-Jdk-Spec: 19
Implementation-Title: springboot-demo
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.springbootdemo.SpringbootDemoApplication
Spring-Boot-Version: 3.2.5
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
Main-Class
:定义程序入口类,格式为完全限定类名(含包名),JVM 通过此属性找到public static void main(String[] args)
方法启动程序。设置为spring-boot-loader
项目的 JarLauncher 类,进行 SpringBoot 应用的启动。Start-Class
:SpringBoot 规定的主启动类Class-Path
:指定依赖的 JAR 文件或目录,路径用空格分隔- 路径相对于 JAR 文件的位置(非当前工作目录)
- 依赖需放在 JAR 同级目录的指定路径下
Manifest-Version
:指定清单文件版本Created-By
:生成 JAR 的工具信息(如 JDK 版本或构建工具)Implementation-Version
:JAR 的版本号(用于版本管理)
虽然 Start-Class
已经指向了主启动类路径,但是不能直接启动
- 原因一:因为在 JAR 包中,主启动类并不在这个路径上,而是在在
BOOT-INF/classes
目录下,不符合 Java 默认的 JAR 包的加载规则。因此,需要通过 JarLauncher 启动加载。 - 原因二:Java 规定可执行器的 JAR 包禁止嵌套其它 JAR 包。但是可以看到
BOOT-INF/lib
目录下,实际有 SpringBoot 应用依赖的所有 JAR 包。因此,spring-boot-loader
项目自定义实现了 ClassLoader 实现类 LaunchedURLClassLoader,支持加载BOOT-INF/classes
目录下的.class
文件,以及BOOT-INF/lib
目录下的jar
包。
JarLauncher
JarLauncher
是 Spring Boot 框架中用于启动可执行 JAR 文件的核心类,属于 org.springframework.boot.loader
包。它的核心作用是为 Spring Boot 的“胖 JAR”(Fat JAR)提供自定义的启动机制,解决传统 JAR 无法直接加载嵌套依赖的问题。
位于 JAR 包中的 org.springframework.boot.loader.launch.JarLauncher
继承类:Launcher
-> ExecutableArchiveLauncher
-> JarLauncher
public class JarLauncher extends ExecutableArchiveLauncher {public JarLauncher() throws Exception {}protected JarLauncher(Archive archive) throws Exception {super(archive);}protected boolean isIncludedOnClassPath(Archive.Entry entry) {return isLibraryFileOrClassesDirectory(entry);}protected String getEntryPathPrefix() {return "BOOT-INF/";}static boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) {String name = entry.name();return entry.isDirectory() ? name.equals("BOOT-INF/classes/") : name.startsWith("BOOT-INF/lib/");}public static void main(String[] args) throws Exception {(new JarLauncher()).launch(args);}
}
通过 (new JarLauncher()).launch(args)
创建 JarLauncher 对象,调用 launch
方法进行启动,整体逻辑还是通过父类的父类 Laucher
所提供。
Archive
接口
根据类图可知,JarLauncher
继承于 ExecutableArchiveLauncher
类,在 ExecutableArchiveLauncher
类源码中有对 Archive
对象的构造
public abstract class ExecutableArchiveLauncher extends Launcher {public ExecutableArchiveLauncher() throws Exception {this(Archive.create(Launcher.class));}
}
Archive
接口,是 spring-boot-loader
项目抽象出来的用来统一访问资源的接口,ExplodedArchive
是针对目录的 Archive
实现类,JarFileArchive
是针对 JAR 的 Archive
实现类,所以根据 isDirectory
方法进行判断。
Archive
概念即归档文档概念,在 Linux 下比较常见- 通常就是一个 tar/zip 格式的压缩包
- JAR 是 zip 格式
public interface Archive extends AutoCloseable {static Archive create(Class<?> target) throws Exception {return create(target.getProtectionDomain());}static Archive create(ProtectionDomain protectionDomain) throws Exception {CodeSource codeSource = protectionDomain.getCodeSource();URI location = codeSource != null ? codeSource.getLocation().toURI() : null;// 拿到当前 classpath 的绝对路径String path = location != null ? location.getSchemeSpecificPart() : null;if (path == null) {throw new IllegalStateException("Unable to determine code source archive");} else {return create(new File(path));}}static Archive create(File target) throws Exception {if (!target.exists()) {throw new IllegalStateException("Unable to determine code source archive from " + target);} else {return (Archive)(target.isDirectory() ? new ExplodedArchive(target) : new JarFileArchive(target));}}
}
launch
方法
在其父类 Laucher
中可以看出,launcher
方法可以读取 JAR 包中的类加载器,保证 BOOT-INF/lib
目录下的类和 BOOT-classes
内嵌的 jar
中的类能够被正常加载到,之后执行 Spring Boot 应用的启动。
public abstract class Launcher {protected void launch(String[] args) throws Exception {// 如果当前不是解压模式(!this.isExploded()),则注册处理器(Handlers.register())if (!this.isExploded()) {Handlers.register();}try {// 创建类加载器(ClassLoader)用于加载类路径上的类ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls());// 根据系统属性 "jarmode" 判断是否使用特定的 JAR 模式运行器类名String jarMode = System.getProperty("jarmode");String mainClassName = this.hasLength(jarMode) ? JAR_MODE_RUNNER_CLASS_NAME : this.getMainClass();// 使用创建的类加载器和主类名调用 launch 方法启动应用this.launch(classLoader, mainClassName, args);} catch (UncheckedIOException var5) {UncheckedIOException ex = var5;throw ex.getCause();}}protected void launch(ClassLoader classLoader, String mainClassName, String[] args) throws Exception {Thread.currentThread().setContextClassLoader(classLoader);Class<?> mainClass = Class.forName(mainClassName, false, classLoader);Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);mainMethod.setAccessible(true);mainMethod.invoke((Object)null, args);}
}
Handlers.register()
方法
逐步分析 launcher
方法,首先方法中调用的 Handlers.register()
方法,用于动态注册自定义协议处理器包,并确保 URL 流处理器缓存被正确刷新。
public final class Handlers {private static final String PROTOCOL_HANDLER_PACKAGES = "java.protocol.handler.pkgs";private static final String PACKAGE = Handlers.class.getPackageName();private Handlers() {}public static void register() {// 获取系统属性java.protocol.handler.pkgs,该属性用于指定协议处理器的包名String packages = System.getProperty("java.protocol.handler.pkgs", "");// 如果当前包名未包含在属性中,则将其追加到属性值中(以|分隔)packages = !packages.isEmpty() && !packages.contains(PACKAGE) ? packages + "|" + PACKAGE : PACKAGE;System.setProperty("java.protocol.handler.pkgs", packages);// 清除URL流处理器缓存resetCachedUrlHandlers();}private static void resetCachedUrlHandlers() {try {// 强制JVM重新加载URL流处理器URL.setURLStreamHandlerFactory((URLStreamHandlerFactory)null);} catch (Error var1) {}}
}
getClassPathUrls
方法
分析 ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls());
中的 (Collection)this.getClassPathUrls()
方法,调用 getClassPathUrls
方法返回值作为参数,该方法为抽象方法,具体实现在 ExecutableArchiveLauncher
中
public abstract class ExecutableArchiveLauncher extends Launcher {protected Set<URL> getClassPathUrls() throws Exception {return this.archive.getClassPathUrls(this::isIncludedOnClassPathAndNotIndexed, this::isSearchedDirectory);}
}
在 ExecutableArchiveLauncher
的 getClassPathUrls
方法执行 Archive
接口定义的 getClassPathUrls
方法返回的是包含所有匹配 URL 的有序集合
class JarFileArchive implements Archive {// 通过流处理遍历JAR条目,应用过滤器筛选后转换为URLpublic Set<URL> getClassPathUrls(Predicate<Archive.Entry> includeFilter, Predicate<Archive.Entry> directorySearchFilter) throws IOException {return (Set)this.jarFile.stream().map(JarArchiveEntry::new).filter(includeFilter).map(this::getNestedJarUrl).collect(Collectors.toCollection(LinkedHashSet::new));}// 根据条目注释判断是否为解压存储的嵌套JAR,若是则调用特殊处理方法,否则直接创建标准URL// archiveEntry:BOOT-INF/classes/private URL getNestedJarUrl(JarArchiveEntry archiveEntry) {try {JarEntry jarEntry = archiveEntry.jarEntry();String comment = jarEntry.getComment();return comment != null && comment.startsWith("UNPACK:") ? this.getUnpackedNestedJarUrl(jarEntry) : JarUrl.create(this.file, jarEntry);} catch (IOException var4) {IOException ex = var4;throw new UncheckedIOException(ex);}}
}
createClassLoader
方法
分析 ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls());
中的 createClassLoader
方法
LaunchedClassLoader
是 SpringBoot 自定义的类加载器,位于 org.springframework.boot.loader.LaunchedURLClassLoader
, 专门用于加载 Spring Boot 可执行 JAR(即“胖 JAR”)中嵌套的依赖和资源。它的核心作用是解决传统 Java 类加载器无法直接加载 JAR 内嵌 JAR(如 BOOT-INF/lib/
中的依赖)的问题。且LaunchedClassLoader
在加载类时,会先尝试自己加载(从嵌套 JAR 或用户代码),若找不到再委派父类加载器。这是对传统双亲委派机制的扩展,确保优先加载应用自身的类。
public abstract class Launcher {protected ClassLoader createClassLoader(Collection<URL> urls) throws Exception {return this.createClassLoader((URL[])urls.toArray(new URL[0]));}private ClassLoader createClassLoader(URL[] urls) {ClassLoader parent = this.getClass().getClassLoader();return new LaunchedClassLoader(this.isExploded(), this.getArchive(), urls, parent);}
}
时序图
+-----------------+ +--------------+ +----------------------+ +-------------------+
| JVM | | JarLauncher | | LaunchedURLClassLoader| | MainMethodRunner |
+-----------------+ +--------------+ +----------------------+ +-------------------+| | | || 执行 java -jar app.jar | | ||--------------------->| | || | 创建 Archive 对象 | || |------------------------>| || | 解析 MANIFEST.MF | || |<------------------------| || | 调用 launch() | || |------------------------>| || | | 创建类加载器 || | |<--------------------------|| | | 加载 BOOT-INF/classes/lib|| | |-------------------------->|| | | | 反射加载 Start-Class| | | |<------------------|| | | | 调用 main()| | | |------------------>|| | | | 执行用户代码 ||<------------------------------------------------------------(结果或异常)|| JVM 退出 | | ||<--------------------| | |
参考
- 一文搞懂 Spring Boot 中 java -jar 的启动 jar 包的原理_springboot java -jar-CSDN 博客
- 芋道 Spring Boot Jar 启动原理 | 芋道源码 —— 纯源码解析博客
- Site Unreachable