springboot之ClassLoader

上篇《ClassLoader#getResource与Class#getResource的差别》了解原生java获取资源方式以及方式之间的区别。

这篇介绍一下springboot的加载方式。

要想调试springboot加载方式,不能直接在idea中运行主程序,要使用真实场景下的java -jar方式运行,需要做两件事:

1、需要打包springboot应用程序

2、在IDEA中用java -jar springboot.jar来运行才能debug

springboot使用maven plugin打包成可运行的jar文件

1
2
3
4
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

SpringBoot打出的jar包,可以直接通过解压的方式查看内部的构造。一般情况下有三个目录。

  1. BOOT-INF:这个文件夹下有两个文件夹classes用来存放用户类,也就是原始jar.original里的类;还有一个是lib,就是这个原始jar.original引用的依赖。
  2. META-INF:这里是通过java -jar启动的入口信息,记录了入口类的位置等信息。
  3. org:Springboot loader的代码,通过它来启动。

MANIFEST.MF文件的内容:

1
2
3
4
5
6
7
8
9
10
Manifest-Version: 1.0
Implementation-Title: springboot-test
Implementation-Version: 1.0-SNAPSHOT
Start-Class: com.zhuxingsheng.App
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.1.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

里面有两个重要的参数:

1
2
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.zhuxingsheng.App

Main-Class:记录了java -jar的启动入口,当使用该命令启动时就会调用这个入口类的main方法,显然可以看出,Springboot转移了启动的入口,不是应用自身的com.zhuxingsheng.App入口类。

Start-Class:应用自身的com.zhuxingsheng.App入口类,当内嵌的jar包加载完成之后,会使用LaunchedURLClassLoader线程加载类来加载这个用户编写的入口类。

在IDEA中正常启动应用程序,整个类加载体系与直接使用java -jar springboot.jar是不一样的,想
要在IDEA里面debug springboot应用程序

先引入loader依赖:

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

再对应用程序通过maven package打包成jar

再在IDEA中设置:

并指定Path to JAR.

启动之后,先进入JarLauncher:

debug进入后,会使用springboot自定义的LaunchedURLClassLoader加载应用程序,LaunchedURLClassLoader类体系:

加载资源的过程如在《Classloader加载资源的方式》中提到的一样。

与之前的做个小实验,但这次做点小变动,在依赖的jar中也放一个META-INF/app.properties文件。

并在工程本身的resources里面也放一个META-INF/app.properties

此时系统中有两个META-INF/app.properties,通过下面的四种情况来加载资源文件,会获取到哪一个文件?

1
2
3
4
5
6
7
8
9
10
11
//第一种场景
final URL resource = Thread.currentThread().getContextClassLoader().getResource("/META-INF/app.properties");

//第二种场景
final URL resource = Thread.currentThread().getContextClassLoader().getResource("META-INF/app.properties");

//第三种场景
final URL resource1 = App.class.getResource("/META-INF/app.properties");

//第四种场景
final URL resource1 = App.class.getResource("META-INF/app.properties");

第一种 ClassLoader绝对路径

按《Classloader加载资源的方式》结论,应该会返回null。

然而实事并非无此:

这不得不提到在URLClassPath里面有两个内部Loader:

FileLoader 是加载文件夹中的文件

JarLoader 是加载jar中的文件

在《Classloader加载资源的方式》中的结论是基于FileLoader加载的,而现在的方式是使用JarLoader。

使用ClassLoader.getResource时,都是基于根节点查找,这点是没错的,只是根节点是BOOT-INF下的lib和classes:

去加载每一个jar中的文件,判断是不是存在:

可以看出,因为根节点不同,所以文件没有加载到,项目根目录里面的META-INF/app.properties,是在整体工程根目录的META-INF/app.properties中。

此时,找到的文件目录是在:

jar:file:/Users/zhuxingsheng/workspace/springboot-demo/springboot-test/target/app.jar!/BOOT-INF/lib/general-tool-utils-1.1.0-SNAPSHOT.jar!/META-INF/app.properties

第二种 ClassLoader 相对路径

可以看出使用的是AppClassLoader,加载的路径为

jar:file:/Users/zhuxingsheng/workspace/springboot-demo/springboot-test/target/app.jar!/META-INF/app.properties

可以看出 相对路径 与绝对路径的区别,以及与FileLoader的区别:

1、绝对路径是LaunchedURLClassLoader从classpath根节点查找;相对路径是AppClassLoader从当前jar为根目录查找

2、FileLoader绝对路径是:file:/META-INF/app.properties,而JarLoader的绝对路径则不同了,会带上整个classpath

第三种 Class 绝对路径

《Classloader加载资源的方式》知道,这种方式与第二种场景效果一致:

1
2
//第二种场景
final URL resource = Thread.currentThread().getContextClassLoader().getResource("META-INF/app.properties");

路径地址在:

jar:file:/Users/zhuxingsheng/workspace/springboot-demo/springboot-test/target/app.jar!/META-INF/app.properties

第三种 Class 相对路径

类似于:

1
final URL resource = Thread.currentThread().getContextClassLoader().getResource("com/zhuxingsheng/META-INF/app.properties");

总结

此篇一是介绍了怎么在IDEA中debug出运行java -jar springboot.jar的效果。二是介绍springboot类加载机制,以及绝对路径与相对路径的区别。

当依赖jar包中有与工程目录下有同路径同名资源文件时,为了不必要的冲突,在classloader#getResource时,不要使用绝对路径。

如在apollo的源码中:

也会特意使用substring处理掉绝对路径。保证加载资源的正确性。

公众号:码农戏码
欢迎关注微信公众号『码农戏码』