目录
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);
以上步骤共同构成了所谓的“自动装配”(Auto-Configuration)。
4、构建嵌入式容器实例
对于 Web 应用而言,还需要设置好一个合适的 Servlet 容器(Tomcat、Jetty 或 Undertow 默认情况下均内置支持)。一旦所有必要的服务都已就绪,则正式开启监听端口等待请求到来。
1.2、流程时序图
解析成代码大概如下:
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.original
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());
}
2.1.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 镜像构建
2.3、 类加载机制
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加载器
一个典型的 Spring Boot 可执行 Jar 结构如下:
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、加载方式
传统 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});
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});
这种设计使得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.jar
6.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.file
6.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 应用程序,确保其稳定、高效地运行。
参考文章: