JAR文件是如何启动的?

SpringBoot构建的结果中,尝尝会以JAR文件的形式提供,众所周知,JAR文件本质上是一个特殊的压缩文件,这里我们创建一个简单的SpringBoot项目,使用Maven 或者Gradle 构建,然后解压,其内容格式如下:

  1. ├── BOOT-INF
  2. ├── classes
  3. ├── application.properties
  4. ├── com
  5. ├── static
  6. └── templates
  7. └── lib
  8. ├── classmate-1.5.1.jar
  9. | |—— 这里由于篇幅忽略一写第三方的依赖JAR
  10. └── tomcat-embed-websocket-9.0.33.jar
  11. ├── META-INF
  12. └── MANIFEST.MF
  13. └── org
  14. └── springframework
  15. └── boot

可以很明显的看到,JAR中包含了第三方的JAR包,这显然使用默认的应用类加载器是无法加载编译生成的class的以及我们生成的目录classes中的的class文件的。

尝试查看 MAINFEST.MF 文件,其内容如下:

  1. $ cat ./build/libs/springboot-0.0.1-SNAPSHOT/META-INF/MANIFEST.MF
  2. Manifest-Version: 1.0
  3. Start-Class: com.zhoutao123.spring.springboot.SpringbootApplication
  4. Spring-Boot-Classes: BOOT-INF/classes/
  5. Spring-Boot-Lib: BOOT-INF/lib/
  6. Spring-Boot-Version: 2.2.6.RELEASE
  7. Main-Class: org.springframework.boot.loader.JarLauncher

Main-Class 指定了JVM 在运行JAR文件的是时候,所启动的类,这个类必须包含一个共有的静态的main方法,这也是为什么SpringBoot的在打包的时候,会将org.springframework.boot.loader 的包给放到最外层,这样,其启动类 **Launcher.class** 就可以正常的被加载到系统类加载器, 在其他位置的加载器中,系统类加载器无法加载的JAR文件或者BOOT-INFO/classes 文件下的 class文件,则使用自定义的类加载器加载。可以通过下面的代码验证,SpringBoot APP的启动类并不是系统类加载器加载,而是自定义的类加载器加载的。

  1. @SpringBootApplication
  2. public class SpringbootApplication {
  3. public static void main(String[] args) {
  4. System.out.println("当前类的加载器为:" + SpringbootApplication.class.getClassLoader());
  5. SpringApplication.run(SpringbootApplication.class, args);
  6. }
  7. }

请不要尝试在IDE中尝试验证,如果在IDE中运行的话,你可能得到的结果如下,这里因为在集成开发环境中运行,class文件是松散的保存在classPath目录中的,这时候并没有启动自定义的加载类,因为仍然是默认的系统类加载器加载。

  1. 当前类的加载器为:sun.misc.Launcher$AppClassLoader@18b4aac2

构建成新的JAR文件之后,使用 java -jar xxxx.jar 命令启动后,可以看到输出如下,分析可知,系统类加载器为 org.springframework.boot.loader.LaunchedURLClassLoader 这就是我们将要深入学习的SpringBoot的自定义类加载器。

  1. 当前类的加载器为:org.springframework.boot.loader.LaunchedURLClassLoader@5197848c

使用JDWP来远程调试

上面的JAR文件的类加载器是JVM启动的才会调用,在IDE中调试并不是使用SpringBoot的自定义类加载器加载,因此我们这里需要使用JDK的远程调试功能来调试代码。在命令行输入如下格式的语法命令,来启动JAR,这里不再对JDWP过多的赘述,有兴趣的可以在网上查一下。

  1. java -agentlib:jdwp=transport=dt_socket, \
  2. server=y,suspend=y,address=9999 \
  3. -jar xxxx.jar

然后,在IDEA中,创建远程调试,信息如下:

image.png

然后在Gradle依赖中添加 SpringBootLoader的依赖

  1. implementation 'org.springframework.boot:spring-boot-loader'

或者Maven的pom文件中添加

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-loader</artifactId>
  4. </dependency>

找到 JarLauncher 类添加断点,然后以Degbug模式启动在IDEA创建的远程调试,即可看到正确进入断点。
image.png