Spring Boot 项目启动原理彻底解剖分析

本文详细阐述了如何通过Spring Boot Maven插件打包项目,解释了源码启动和命令行启动的区别,以及解剖JAR包后发现的启动机制,包括MANIFEST.MF文件、启动类和依赖管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、场景介绍
  • spring-boot 项目搭建以后启动方式一般有两种:

    • 源码方式启动

      @SpringBootApplication
      public class ServiceApplication {
          public static void main(String[] args) {
              SpringApplication.run(ServiceApplication.class, args);
              CommonUtil.info();
          }
      }
      
    • 命令方式启动

      java -jar example-service.jar
      
  • 采用 spring-boot-maven 插件打包且包含主启动类的 spring-boot 项目,为什么通过 main 方法就能启动呢,让我们一起来探讨吧?

二、项目搭建
  1. 新建 Maven 父子工程

    example
        - example-common
            - CommonUtil.java
            - pom.xml
        - example-service
            - ServiceApplication.java
            - pom.xml
    - pom.xml        
    

    P.S

    • exampleexample-commonexample-service 的父工程

    • example-service 依赖 example-common 模块

    • example-service 模块包含主启动类,为标准都 spring boot 工程,example-common 模块无启动类

    • 父工程 pom

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
      
          <!-- 模块坐标信息 GAV -->
          <groupId>com.rambo</groupId>
          <artifactId>example</artifactId>
          <packaging>pom</packaging>
          <version>V1.0.0.1</version>
      
          <name>${project.artifactId}</name>
          <description>父子模块示例工程 —— 基础父工程</description>
      
          <!-- 子模块列表 -->
          <modules>
              <module>example-common</module>
              <module>example-service</module>
          </modules>
      
          <properties>
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
              <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
              <java.version>1.8</java.version>
              <maven.compiler.source>1.8</maven.compiler.source>
              <maven.compiler.target>1.8</maven.compiler.target>
              <spring-boot.version>2.5.2</spring-boot.version>
          </properties>
          
          <dependencyManagement>
              <dependencies>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-dependencies</artifactId>
                      <version>${spring-boot.version}</version>
                      <type>pom</type>
                      <scope>import</scope>
                  </dependency>
              </dependencies>
          </dependencyManagement>
      
          <build>
              <finalName>${project.artifactId}</finalName>
          </build>
      </project>
      
    • example-common 模块 pom

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
      
          <!-- 父工程 GAV -->
          <parent>
              <artifactId>example</artifactId>
              <groupId>com.rambo</groupId>
              <version>V1.0.0.1</version>
          </parent>
      
          <!-- 本工程模块 AV -->
          <artifactId>example-common</artifactId>
          <version>V1.0.0.1</version>
          <packaging>jar</packaging>
      
          <name>${project.artifactId}</name>
          <description>无启动类的示例通用模块 —— 通用工具</description>
          
          <dependencies>
              <dependency>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
              </dependency>
          </dependencies>
      </project>
      
    • example-service 模块 pom

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
      
          <!-- 父工程 GAV -->
          <parent>
              <artifactId>example</artifactId>
              <groupId>com.rambo</groupId>
             <version>V1.0.0.1</version>
          </parent>
      
          <!-- 本工程模块 AV -->
          <artifactId>example-service</artifactId>
          <version>V1.0.0.1</version>
          <packaging>jar</packaging>
      
          <name>example-service</name>
          <description>有启动类的示例服务模块 —— 示例服务</description>
      
          <dependencies>
              <!-- Spring Boot Web -->
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
      
              <!-- Spring Boot Test -->
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-test</artifactId>
                  <scope>test</scope>
              </dependency>
              
              <!-- 示例通用工具模块 -->
              <dependency>
                  <groupId>com.rambo</groupId>
                  <artifactId>example-common</artifactId>
                  <version>V1.0.0.1</version>
              </dependency>
              
              <!-- 方便解读编译后的字节码 -->
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-loader</artifactId>
                  <scope>provided</scope>
              </dependency>
          </dependencies>
          
          <build>
              <plugins>
                  <!-- spring boot maven 打包插件 -->
                  <plugin>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-maven-plugin</artifactId>
                      <version>${spring-boot.version}</version>
                      <executions>
                          <execution>
                              <id>repackage</id>
                              <goals>
                                  <goal>repackage</goal>
                              </goals>
                          </execution>
                      </executions>
                  </plugin>
              </plugins>
          </build>
      </project>
      
  2. example-commonexample-service 的示例代码如下

    • example-common 模块

      public class CommonUtil {
          public static void info() {
              System.out.println("This is info from CommonUtil.class");
          }
      }
      
    • example-service 模块

      @SpringBootApplication
      public class ServiceApplication {
          public static void main(String[] args) {
              SpringApplication.run(ServiceApplication.class, args);
              CommonUtil.info();
          }
      }
      
三、解体 JAR 包
  1. 在父工程的 pom 文件同级执行 Maven 打包命令

    • 打包命令

      mvn package
      
    • example-service 模块打出来的 JAR

      # 该 JAR 也成为 Fat JAR,采用 ZIP 压缩格式存储
      example-service.jar
      example-service.jar.original
      
  2. 解压 example-service.jar 到指定文件夹分析打出来的内容

    unzip example-service.jar -d example-service
    
    • BOOT-INFO/class 目录存放应用编译后的 class 文件

    • BOOT-INFO/lib 目录存放应用依赖的 JAR

    • META-INF/ 目录存放应用相关的元信息,如:MANIFEST.MF

    • org/ 目录存放 Spring Boot 相关的 class 文件

    • example-service 模块的 MANIFEST.MF 文件内容如下

      Manifest-Version: 1.0
      Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
      Archiver-Version: Plexus Archiver
      Built-By: rambo
      Spring-Boot-Layers-Index: BOOT-INF/layers.idx
      Start-Class: com.rambo.service.ServiceApplication
      Spring-Boot-Classes: BOOT-INF/classes/
      Spring-Boot-Lib: BOOT-INF/lib/
      Spring-Boot-Version: 2.5.2
      Created-By: Apache Maven 3.6.3
      Build-Jdk: 1.8.0_271
      Main-Class: org.springframework.boot.loader.JarLauncher
      
  3. 启动测试

    • example-service.jar 运行

      java -jar example-service.jar

       ~/WorkSpace/example/example-service/target/ java -jar example-service.jar
      
        .   ____          _            __ _ _
       /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
      ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
       \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
        '  |____| .__|_| |_|_| |_\__, | / / / /
       =========|_|==============|___/=/_/_/_/
       :: Spring Boot ::                (v2.5.2)
      
      2021-07-13 16:42:38.813  INFO 38536 --- [           main] com.rambo.service.ServiceApplication     : Starting ServiceApplication using Java 1.8.0_271 on Rambos-MacBook-Pro.local with PID 38536 (/Users/rambo/WorkSpace/example/example-service/target/example-service.jar started by rambo in /Users/rambo/WorkSpace/example/example-service/target)
      2021-07-13 16:42:38.815  INFO 38536 --- [           main] com.rambo.service.ServiceApplication     : No active profile set, falling back to default profiles: default
      2021-07-13 16:42:39.950  INFO 38536 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 9999 (http)
      2021-07-13 16:42:39.967  INFO 38536 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
      2021-07-13 16:42:39.967  INFO 38536 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.48]
      2021-07-13 16:42:40.040  INFO 38536 --- [           main] o.a.c.c.C.[.[localhost].[/example]       : Initializing Spring embedded WebApplicationContext
      2021-07-13 16:42:40.040  INFO 38536 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1176 ms
      2021-07-13 16:42:40.660  INFO 38536 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9999 (http) with context path '/example'
      2021-07-13 16:42:40.678  INFO 38536 --- [           main] com.rambo.service.ServiceApplication     : Started ServiceApplication in 3.217 seconds (JVM running for 3.66)
      This is info from CommonUtil.class
      
    • 解压后执行主启动类 Jarlaunch

      java org.springframework.boot.loader.JarLauncher

       ~/WorkSpace/example/example-service/target/example-service/ java org.springframework.boot.loader.JarLauncher
      
        .   ____          _            __ _ _
       /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
      ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
       \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
        '  |____| .__|_| |_|_| |_\__, | / / / /
       =========|_|==============|___/=/_/_/_/
       :: Spring Boot ::                (v2.5.2)
      
      2021-07-13 16:43:55.120  INFO 38587 --- [           main] com.rambo.service.ServiceApplication     : Starting ServiceApplication using Java 1.8.0_271 on Rambos-MacBook-Pro.local with PID 38587 (/Users/rambo/WorkSpace/example/example-service/target/example-service/BOOT-INF/classes started by rambo in /Users/rambo/WorkSpace/example/example-service/target/example-service)
      2021-07-13 16:43:55.122  INFO 38587 --- [           main] com.rambo.service.ServiceApplication     : No active profile set, falling back to default profiles: default
      2021-07-13 16:43:55.982  INFO 38587 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 9999 (http)
      2021-07-13 16:43:55.994  INFO 38587 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
      2021-07-13 16:43:55.995  INFO 38587 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.48]
      2021-07-13 16:43:56.071  INFO 38587 --- [           main] o.a.c.c.C.[.[localhost].[/example]       : Initializing Spring embedded WebApplicationContext
      2021-07-13 16:43:56.071  INFO 38587 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 879 ms
      2021-07-13 16:43:56.438  INFO 38587 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9999 (http) with context path '/example'
      2021-07-13 16:43:56.451  INFO 38587 --- [           main] com.rambo.service.ServiceApplication     : Started ServiceApplication in 1.749 seconds (JVM running for 2.102)
      This is info from CommonUtil.class
      
四、原理分析
  1. MANIFEST.MF 文件分析

    Main-Class: org.springframework.boot.loader.JarLauncher

    • 通过 spring-boot-maven 插件打包出来的项目,主启动类设置为 JarLauncher 或者 WarLauncher

    Spring-Boot-Classes: BOOT-INF/classes/

    • 通过 Spring-Boot-Classes 来定义应用代码的二进制文件路径

    Spring-Boot-Lib: BOOT-INF/lib/

    • 通过 Spring-Boot-Lib 来定义应用程序所需依赖位置

    Start-Class: com.rambo.service.ServiceApplication

    • 项目编写的启动类通过 Start-Class 来定义,并指定主启动类的包全路径
  2. 代理启动类分析

    Mark

    JarLauncher 、WarLauncher 、PropertiesLauncher代表了Spring Boot的三种代理启动方式

  3. JarLauncher 启动类分析

    • JarLauncher.java

      public static void main(String[] args) throws Exception {
          // 构造 JarLauncher 对象,同时执行父类 ExecutableArchiveLauncher 的构造方法,再调用父类的 launch 方法
          (new JarLauncher()).launch(args);
      }
      
      // 获取 classpath.idx 中记录的依赖信息
      protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
          if (archive instanceof ExplodedArchive) {
              String location = this.getClassPathIndexFileLocation(archive);
              return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
          } else {
              return super.getClassPathIndex(archive);
          }
      }
      
    • ExecutableArchiveLauncher.java

      // 父类构造函数
      public ExecutableArchiveLauncher() {
          try {
              this.archive = this.createArchive();
              this.classPathIndex = this.getClassPathIndex(this.archive);
          } catch (Exception var2) {
              throw new IllegalStateException(var2);
          }
      }
      
      // Archive的getMainClass方法
      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);
          } else {
              return mainClass;
          }
      }
      
    • Launcher.java

      // 判断文件路径和归档文件是否正确
      protected final Archive createArchive() throws Exception {
          ProtectionDomain protectionDomain = this.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");
          } else {
              File root = new File(path);
              if (!root.exists()) {
                  throw new IllegalStateException("Unable to determine code source archive from " + root);
              } else {
                  return (Archive)(root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
              }
          }
      }
      
      // 父类 launch 方法
      protected void launch(String[] args) throws Exception {
          if (!this.isExploded()) {
              // 在系统属性中设置注册了自定义的URL处理器:org.springframework.boot.loader.jar.Handler。如果URL中没有指定处理器,会去系统属性中查询
              JarFile.registerUrlProtocolHandler();
          }
          // getClassPathArchives方法在会去找lib目录下对应的第三方依赖JarFileArchive,同时也会项目自身的JarFileArchive
          // 根据getClassPathArchives得到的JarFileArchive集合去创建类加载器ClassLoader。这里会构造一个LaunchedURLClassLoader类加载器,这个类加载器继承URLClassLoader,并使用这些JarFileArchive集合的URL构造成URLClassPath
          // LaunchedURLClassLoader类加载器的父类加载器是当前执行类JarLauncher的类加载器
          ClassLoader classLoader = this.createClassLoader(this.getClassPathArchivesIterator());
          
          String jarMode = System.getProperty("jarmode");
          String launchClass = jarMode != null && !jarMode.isEmpty() ? "org.springframework.boot.loader.jarmode.JarModeLauncher" : this.getMainClass();
          
          // 执行重载方法
          this.launch(args, launchClass, classLoader);
      }
      
      // launch 重载方法
      protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
          Thread.currentThread().setContextClassLoader(classLoader);
          this.createMainMethodRunner(launchClass, args, classLoader).run();
      }
      
      // 
      protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
          return new MainMethodRunner(mainClass, args);
      }
      
    • MainMethodRunner.java

      public MainMethodRunner(String mainClass, String[] args) {
          this.mainClassName = mainClass;
          this.args = args != null ? (String[])args.clone() : null;
      }
      
      public void run() throws Exception {
          Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
          Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
          mainMethod.setAccessible(true);
          mainMethod.invoke((Object)null, this.args);
      }
      
  4. 总结

    • spring-boot-maven 插件,在打包过程中,将 Spring Boot 定义的一套规则导入到了打出来的 JAR 包中

    • JAR 包的 URL 路径使用自定义的规则并且这个规则需要使用 org.springframework.boot.loader.jar.Handler 处理器处理

    • 它的 Main-Class 使用 JarLauncher,如果是 war 包,使用 WarLauncher 执行

    • 这些 Launcher 内部都会另起一个线程启动自定义的 SpringApplication

    • 这些特性通过 spring-boot-maven-plugin 插件打包完成

    • SpringBoot 通过扩展 JarFileJarURLConnectionURLStreamHandler,实现了 jar in jar 中资源的加载

    • SpringBoot 通过扩展 URLClassLoader--LauncherURLClassLoader,实现了 jar in jarclass 文件的加载

    • JarLauncher 通过加载 BOOT-INF/classes 目录及 BOOT-INF/lib 目录下 jar 文件,实现了 fat jar 的启动

    • WarLauncher 通过加载 WEB-INF/classes 目录及 WEB-INF/libWEB-INF/lib-provided 目录下的 jar 文件,实现了 war 文件的直接启动及 web容器中的启动

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值