Springboot项目jar包启动原理及类型

目录

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、加载方式

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

以上步骤共同构成了所谓的“自动装配”(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、核心加载机制代码解析

  1. 执行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 应用程序,确保其稳定、高效地运行。

参考文章:

1、SpringBoot 的jar包为什么可以直接运行

2、字节二面:为什么SpringBoot的 jar可以直接运行?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值