目录
1、介绍
1.1、流程介绍
1.2、流程时序图
2、组件
2.1、 spring-boot-maven-plugin
2.2、 MANIFEST.MF 规范
2.3、 类加载机制
2.4、 嵌套 Jar 资源加载
3、原理
3.1、特殊的打包结构
3.2、自定义的JarLoader
3.3、核心加载机制代码解析
4、与传统Jar的区别
5、加载方式
5.1、传统 Jar 的类加载
5.2、Spring Boot 的加载:
6、启动方式
6.1. 开发环境启动方式
6.2. 生产环境启动方式
6.3. Linux 环境后台启动
1、介绍
大纲如下所示:
1.1、流程介绍
1、加载自定义 ClassLoader
Spring Boot 使用了定制化的类加载器来替代默认的 AppClassLoader。它引入了 LaunchedURLClassLoader 类,这是一个扩展了java.net.URLClassLoader 的子类。
打破了传统的双亲委派机制:
此类重新实现了 loadClass() 方法,并调整了传统的双亲委派模型(Parent Delegation Model)。这种调整允许优先从当前 JAR 中加载类,而不是完全依赖父级类加载器。
2、 执行 Main 方法
根据 MANIFEST.MF 中指定的主类 (JarLauncher),Java 虚拟机会调用它的 main(String[] args)方法。随后,控制权被传递给真正的应用入口点——即带有 @SpringBootApplication 注解的类中的 public static void main(String[] args) 函数。
@SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }此处的关键在于 SpringApplication.run(...) 方法,它是整个 Spring Boot 应用的核心启动逻辑所在位置。
3、初始化上下文环境
在进入核心业务之前,Spring Boot 需要完成一系列准备工作:
解析配置文件(如 application.properties 或者 application.yml)并将其映射成对应的属性对象;
自动扫描项目路径下的组件(Component Scanning),识别那些标注有特定注解(比如 @Controller, @Service, etc.) 的候选 Bean;
基于条件化配置原则决定哪些增强特性应该激活(Conditional Configuration Mechanism);
组件扫描只扫自己项目下的包;
自动装配:
@EnableAutoConfiguration 从 META-INF/spring.factories(3.x) 为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)读出一批配置类,再由@Conditional*筛出真正生效的。
4、构建嵌入式容器实例
对于 Web 应用而言,还需要设置好一个合适的 Servlet 容器(Tomcat、Jetty 或 Undertow 默认情况下均内置支持)。一旦所有必要的服务都已就绪,则正式开启监听端口等待请求到来。
1.2、流程时序图
如下所示:
ClassLoader 就是后文的 LaunchedURLClassLoader,它比默认多一层,专门负责 BOOT-INF/classes 和 BOOT-INF/lib下的嵌套 jar。
解析成代码大概如下:
sequenceDiagram participant JVM participant JarLauncher participant ClassLoader participant MyApplication JVM->>JarLauncher: main() JarLauncher->>JarLauncher: createArchive() JarLauncher->>ClassLoader: new LaunchedURLClassLoader() ClassLoader-->>JarLauncher: loader JarLauncher->>Thread: setContextClassLoader(loader) JarLauncher->>ClassLoader: loadClass("com.example.MyApplication") ClassLoader->>ClassLoader: findClass() (BOOT-INF/classes) ClassLoader->>ClassLoader: loadDependencyJars() (BOOT-INF/lib) ClassLoader-->>JarLauncher: Class<MyApplication> JarLauncher->>MyApplication: main() MyApplication->>SpringApplication: run()下面可以根据每个过程中具体是什么组件来进行具体的介绍。
2、组件
2.1、 spring-boot-maven-plugin
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.MyApplication</mainClass> <layout>JAR</layout> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>简单介绍下:
1.spring-boot-maven-plugin项目存在于spring-boot-tools目录中。
2.spring-boot-maven-plugin默认有5个goals:
repackage、
run、
start、
stop、
build-info。
在打包的时候默认使用的是repackage。
3.repackage能够将mvn package生成的软件包,再次打包为可执行的软件包,并将mvn package生成的软件包重命名为*.original。
组成:
spring-learn-0.0.1-SNAPSHOT.jar spring-learn-0.0.1-SNAPSHOT.jar.originalOriginal 文件:
这个文件通常是指未经过 Spring Boot 处理的原始 JAR 文件,名字一般是 ${artifactId}-${version}-original.jar。这个文件包含了项目的类文件和依赖项,并且没有经过 Spring Boot 的重打包或包括嵌入式服务器的处理。
1:Maven/Gradle 先打一个“普通 JAR”
结构如下:
BOOT-INF/ ├── classes/ ← 你的代码(同上) META-INF/ └── MANIFEST.MF ← 可能没有可执行信息这个 JAR 只包含你写的代码和资源文件(/classes),不包含依赖。
文件名:xxx.jar.original,它是标准的 Java 构建输出。
2:Spring Boot Plugin 对其“再包装”
结构如下:
BOOT-INF/ ├── classes/ ← 你的代码 ├── lib/ ← 所有依赖 jar(spring、tomcat 等) org/ └── springframework/ └── boot/ └── loader/ ← Spring Boot 的启动类(Launcher) META-INF/ └── MANIFEST.MF ← 指定 Main-Class: org.springframework.boot.loader.JarLauncher把这个原始 JAR 当作 内部的模块;
将所有第三方依赖(如 spring-core、tomcat 等)打包进去;
添加启动器(Launcher)、嵌入式容器支持、MANIFEST.MF 等;
最终生成一个 可直接运行的 fat jar(也叫 uber-jar)
如果不需要这个文件,可以这样设置:
skip=true 跳过的是整个 repackage,结果是连 fat jar 都没有;只想不产生 .original,就不要配 repackage execution。
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> <configuration> <skip>true</skip> <!-- 禁用原始 JAR 的生成 --> </configuration> </execution> </executions> </plugin> </plugins> </build>3.repackage在代码层面调用了RepackageMojo的execute方法,而在该方法中又调用了repackage方法。
代码示例:
private void repackage() throws MojoExecutionException { // maven生成的jar,最终的命名将加上.original后缀 Artifact source = getSourceArtifact(); // 最终为可执行jar,即fat jar File target = getTargetFile(); // 获取重新打包器,将maven生成的jar重新打包成可执行jar Repackager repackager = getRepackager(source.getFile()); // 查找并过滤项目运行时依赖的jar Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters())); // 将artifacts转换成libraries Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog()); try { // 获得Spring Boot启动脚本 LaunchScript launchScript = getLaunchScript(); // 执行重新打包,生成fat jar repackager.repackage(target, libraries, launchScript); }catch (IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } // 将maven生成的jar更新成.original文件 updateArtifact(source, target, repackager.getBackupFile()); }1、 物理文件结构(字节级分析)
使用 zipinfo -v命令查看完整的 ZIP 结构:
Zip file size: 28765 bytes, number of entries: 127 -rw-r--r-- 3.0 unx 1345 bx defN 23-Jan-01 00:00 META-INF/MANIFEST.MF drwxr-xr-x 3.0 unx 0 bx stor 23-Jan-01 00:00 BOOT-INF/ drwxr-xr-x 3.0 unx 0 bx stor 23-Jan-01 00:00 BOOT-INF/classes/ drwxr-xr-x 3.0 unx 0 bx stor 23-Jan-01 00:00 BOOT-INF/lib/ -rw-r--r-- 3.0 unx 1234567 bx defN 23-Jan-01 00:00 BOOT-INF/lib/spring-core-6.0.9.jar drwxr-xr-x 3.0 unx 0 bx stor 23-Jan-01 00:00 org/springframework/boot/loader/ -rw-r--r-- 3.0 unx 5678 bx defN 23-Jan-01 00:00 org/springframework/boot/loader/JarLauncher.class关键特征:
ZIP 文件使用存储模式(STORED)而非压缩模式(DEFLATED)存储 loader 类
目录条目使用 0 字节存储
采用 ZIP64 格式支持大于 4GB 的 fat jar
2.2、 MANIFEST.MF 规范
完整示例(包含所有 Spring Boot 特定属性),如下所示:
Manifest-Version: 1.0 Spring-Boot-Version: 3.1.0 Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.example.MyApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Built-By: maven Implementation-Title: demo Implementation-Version: 1.0.0特殊属性说明:
Spring-Boot-Classpath-Index:类路径索引文件,优化启动速度
Spring-Boot-Layers-Index:用于分层 Docker 镜像构建
classpath.idx 预先记录了 lib 的顺序,启动时直接按索引拼出每个嵌套 jar 的 URL,省掉逐个扫描 zip 条目去判断哪些是依赖这件事。
2.3、 类加载机制
1、LaunchedURLClassLoader 完整实现
代码如下所示:
public class LaunchedURLClassLoader extends URLClassLoader { // 特殊处理的包前缀 private static final String[] DEFAULT_HIDDEN_PACKAGES = new String[] { "java.", "javax.", "jakarta.", "org.springframework.boot.loader." }; // 覆盖的类加载逻辑 @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 检查是否已加载 Class<?> loadedClass = findLoadedClass(name); if (loadedClass != null) { return loadedClass; } // 2. 检查是否在隐藏包中 if (isHidden(name)) { return super.loadClass(name, resolve); } // 3. 尝试从BOOT-INF/classes加载 try { Class<?> localClass = findClass(name); if (localClass != null) { return localClass; } } catch (ClassNotFoundException ex) {} // 4. 尝试从父加载器加载 try { return Class.forName(name, false, getParent()); } catch (ClassNotFoundException ex) {} // 5. 最后尝试从BOOT-INF/lib加载 return findClass(name); } } private boolean isHidden(String className) { for (String hiddenPackage : DEFAULT_HIDDEN_PACKAGES) { if (className.startsWith(hiddenPackage)) { return true; } } return false; } }2.4、 嵌套 Jar 资源加载
JarURLConnection 的实现关键部分。
代码如下所示:
public class JarURLConnection extends java.net.JarURLConnection { private JarFile jarFile; public InputStream getInputStream() throws IOException { // 处理嵌套jar的路径格式:jar:nested:/path.jar!/nested.jar!/ String nestedPath = getEntryName(); if (nestedPath.contains("!/")) { String[] parts = nestedPath.split("!/", 2); JarFile outerJar = new JarFile(parts[0]); JarEntry nestedEntry = outerJar.getJarEntry(parts[1]); return new NestedJarInputStream(outerJar, nestedEntry); } return super.getInputStream(); } }3、原理
Spring Boot项目的Jar包可以直接运行的原因在于其特殊的打包结构和启动机制。
如下所示:
3.1、特殊的打包结构
Spring Boot的可执行Jar实际上是一个"Fat Jar",它包含:
BOOT-INF/classes:存放项目的编译类文件
BOOT-INF/lib:存放项目依赖的所有第三方库
META-INF:包含MANIFEST.MF等元数据
org/springframework/boot/loader:包含Spring Boot的Jar加载器
普通 jar =BOOT-INF/classes+ MAINFEST;
fat jar = 前者 +BOOT-INF/lib+org/springframework/boot/loader+ 带 Main-Class 的 MAINFEST。
一个典型的 Spring Boot 可执行 Jar 结构如下:MAINFEST
example-app.jar ├── META-INF/ │ └── MANIFEST.MF ├── BOOT-INF/ │ ├── classes/ # 你的应用类文件 │ └── lib/ # 所有依赖的 Jar │ ├── spring-boot.jar │ ├── spring-core.jar │ └── ...其他依赖 └── org/ └── springframework/ └── boot/ └── loader/ # Spring Boot 的加载器类 ├── JarLauncher.class ├── LaunchedURLClassLoader.class └── ...3.2、自定义的JarLoader
Spring Boot使用JarLauncher作为主类(在MANIFEST.MF中指定),这个加载器能够:
加载嵌套在Jar中的Jar文件(BOOT-INF/lib下的依赖)
正确处理类路径和资源加载
示例:
Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.example.MyApplication Spring-Boot-Version: 3.1.0 Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/3.3、核心加载机制
执行java -jar时,JVM读取MANIFEST.MF中的Main-Class(通常是org.springframework.boot
.loader.JarLauncher)。
JarLauncher 核心代码:
public class JarLauncher extends ExecutableArchiveLauncher { @Override protected String getMainClass() throws Exception { // 从 MANIFEST.MF 读取 Start-Class return getArchive().getManifest().getMainAttributes() .getValue("Start-Class"); } public static void main(String[] args) throws Exception { new JarLauncher().launch(args); } }代码如下所示:
2.JarLauncher启动后,会设置特殊的类加载器。
类加载器实现 (LaunchedURLClassLoader)代码示例:
public class LaunchedURLClassLoader extends URLClassLoader { public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 特殊处理嵌套 Jar 的类加载 if (name.startsWith("org.springframework.boot.loader")) { //调整了传统的双亲委派模型),允许优先从当前 JAR 中加载类,而不是完全依赖父级类加载器。 return super.loadClass(name, resolve); } // 优先从 BOOT-INF/classes 加载 try { return findClass(name); } catch (ClassNotFoundException ex) { // 然后从 BOOT-INF/lib 加载 return super.loadClass(name, resolve); } } }3.加载并执行BOOT-INF/classes中真正的应用主类(由MANIFEST.MF中的Start-Class指定)
4.Spring Boot应用由此启动。
4、与传统Jar的区别
传统Jar:
只能包含扁平结构的类文件
不能嵌套其他Jar文件
依赖需要外部提供
Spring Boot Fat Jar:
可以嵌套依赖Jar
自带类加载机制
完全自包含,无需外部依赖
Spring Boot 使用自定义的 JarFile 实现来加载嵌套 Jar:
public class JarFile extends java.util.jar.JarFile { private final RandomAccessDataFile rootFile; public JarFile(File file) throws IOException { super(file); this.rootFile = new RandomAccessDataFile(file); } public URL getUrl() throws MalformedURLException { // 返回特殊的 URL 格式:jar:nested:/path.jar!/nested.jar!/ return new URL("jar:nested:" + this.rootFile.getFile() + "!/"); } }5、加载方式
5.1、传统 Jar 的类加载
// 标准 Jar 加载方式 URLClassLoader loader = new URLClassLoader(new URL[] {myJar.toURI().toURL()}); Class<?> mainClass = loader.loadClass("com.example.Main"); Method mainMethod = mainClass.getMethod("main", String[].class); mainMethod.invoke(null, new Object[] {args});详细的代码 如下图所示:
public class JarLauncher { public static void main(String[] args) { // 假设 jarPath 是项目中 JAR 文件的路径 Path jarPath = Paths.get("myapp.jar"); // 读取 META-INF/MANIFEST.MF Manifest manifest = readManifest(jarPath); // 获取主类名称 String mainClassName = manifest.getMainAttributes().getValue("Main-Class"); // 使用类加载器加载主类 try { Class<?> mainClass = Class.forName(mainClassName); Method mainMethod = mainClass.getMethod("main", String[].class); String[] mainArgs = {}; // 可以传入应用参数 mainMethod.invoke(null, (Object) mainArgs); } catch (Exception e) { e.printStackTrace(); } } private static Manifest readManifest(Path jarPath) { try (JarFile jarFile = new JarFile(jarPath.toFile())) { return jarFile.getManifest(); } catch (IOException e) { e.printStackTrace(); return null; } } }5.2、Spring Boot 的加载
代码示例:
// Spring Boot 的加载方式 JarFile.registerUrlProtocolHandler(); LaunchedURLClassLoader loader = new LaunchedURLClassLoader(getNestedJars()); Thread.currentThread().setContextClassLoader(loader); Class<?> mainClass = loader.loadClass(getStartClass()); Method mainMethod = mainClass.getMethod("main", String[].class); mainMethod.invoke(null, new Object[] {args});可以看下下面这个详细逻辑:
使用LaunchedURLClassLoader类加载器。
import java.net.URL; import java.net.URLClassLoader; public class LaunchedURLClassLoader extends URLClassLoader { public LaunchedURLClassLoader(URL[] urls) { super(urls); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 先尝试在当前类加载器中加载 try { return findClass(name); } catch (ClassNotFoundException e) { return super.loadClass(name, resolve); } } }代码如下所示:
import java.io.IOException; import java.lang.reflect.Method; import java.nio.file.Path; import java.nio.file.Paths; import java.util.jar.JarFile; import java.util.jar.Manifest; public class JarLauncher { public static void main(String[] args) { // 假设 jarPath 是项目中 JAR 文件的路径 Path jarPath = Paths.get("myapp.jar"); // 读取 META-INF/MANIFEST.MF Manifest manifest = readManifest(jarPath); // 获取主类名称 String mainClassName = manifest.getMainAttributes().getValue("Main-Class"); // 使用自定义类加载器加载主类 try { // 创建 URL 数组,指向 JAR 文件 URL[] urls = { jarPath.toUri().toURL() }; LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(urls); // 加载主类 Class<?> mainClass = classLoader.loadClass(mainClassName); Method mainMethod = mainClass.getMethod("main", String[].class); String[] mainArgs = {}; // 可以传入应用参数 mainMethod.invoke(null, (Object) mainArgs); // 关闭加载器 classLoader.close(); } catch (Exception e) { e.printStackTrace(); } } private static Manifest readManifest(Path jarPath) { try (JarFile jarFile = new JarFile(jarPath.toFile())) { return jarFile.getManifest(); } catch (IOException e) { e.printStackTrace(); return null; } } }经过上面的代码示例,进行详细的整理:
1、java -jar启动过程中,会首先Paths类找到对应的启动jar的位置信息。
2、读取mainfest.mf文件里面的Main-class,启动jarlaunched。
3、使用LaunchedURLClassLoader类加载器,完成对当前类依赖的加载,如果当前类不存在,则去super(打破了双亲委派)。
4、加载主类start-class,并且执行main方法。
这种设计使得Spring Boot应用可以像普通可执行文件一样运行,简化了部署和分发过程。
6、启动方式
6.1. 开发环境启动方式
6.1.1.IDEA 直接启动
在 IDE 中直接运行 @SpringBootApplication注解的主类
支持实时热部署(需添加 devtools 依赖)
<!-- pom.xml 添加热部署支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>6.1.2 使用 Maven 插件启动
# 普通启动 mvn spring-boot:run # 带环境变量启动 mvn spring-boot:run -Dspring-boot.run.profiles=prod # 调试模式启动 mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"6.2. 生产环境启动方式
6.2.1 打包后运行
# 打包项目 mvn clean package # 基本启动 java -jar target/your-app.jar # 指定配置文件 java -jar your-app.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties # 指定JVM参数 java -Xms512m -Xmx1024m -jar your-app.jar6.2.2 使用启动脚本
#!/bin/bash APP_NAME="your-app.jar" JAVA_OPTS="-server -Xms512m -Xmx1024m -XX:+UseG1GC" SPRING_OPTS="--spring.profiles.active=prod" nohup java $JAVA_OPTS -jar $APP_NAME $SPRING_OPTS > app.log 2>&1 & echo $! > pid.file6.3. Linux 环境后台启动
6.3.1. nohup 方式
nohup java -jar your-app.jar > app.log 2>&1 &nohup:忽略挂断信号> app.log:重定向标准输出到文件2>&1:将标准错误重定向到标准输出&:后台运行
6.3.2. 高级进程管理方案
创建/etc/systemd/system/springboot-app.service:
[Unit] Description=Spring Boot Application After=syslog.target network.target [Service] User=appuser Group=appgroup ExecStart=/usr/bin/java -jar /opt/app/your-app.jar SuccessExitStatus=143 Restart=always RestartSec=30 Environment="SPRING_PROFILES_ACTIVE=prod" Environment="JAVA_OPTS=-Xms512m -Xmx1024m" [Install] WantedBy=multi-user.target管理命令:
# 重载配置 sudo systemctl daemon-reload # 启动服务 sudo systemctl start springboot-app # 查看状态 sudo systemctl status springboot-app # 设置开机启动 sudo systemctl enable springboot-app总结
通过以上的介绍、可以使你在 Linux 环境中部署和管理 Spring Boot 应用程序,确保其稳定、高效地运行。
参考文章:
1、SpringBoot 的jar包为什么可以直接运行https://blog.csdn.net/weixin_46768610/article/details/109377144?ops_request_misc=&request_id=&biz_id=102&utm_term=springboot%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8jar%E6%97%B6%E5%80%99%E7%9A%84%E8%BF%87%E7%A8%8B%EF%BC%8C%E5%8F%8A%E4%B8%BA%E4%BB%80%E4%B9%88%E5%8F%AF%E4%BB%A5%E8%BF%90&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-109377144.142^v102^pc_search_result_base1&spm=1018.2226.3001.4187
2、字节二面:为什么SpringBoot的 jar可以直接运行?https://blog.csdn.net/jcc4261/article/details/144454049?ops_request_misc=&request_id=&biz_id=102&utm_term=springboot%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8jar%E6%97%B6%E5%80%99%E7%9A%84%E8%BF%87%E7%A8%8B%EF%BC%8C%E5%8F%8A%E4%B8%BA%E4%BB%80%E4%B9%88%E5%8F%AF%E4%BB%A5%E8%BF%90&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-144454049.142^v102^pc_search_result_base1&spm=1018.2226.3001.4187