Jar 包运行时读取 Resources 目录下文件

起因是程序运行时我需要读取操作一份模板文件,但我想把这份模板文件放在这个模块的 Resources 目录下,但在 idea 启动时没错没错,部署到服务上就有问题会报 FileNotFoundException

问题原因

在 idea 启动时每个文件就是个独立的个体,在系统中有一个绝对路径的 URL。但当构建成 Jar 包启动时,在系统中只有一个绝对路径 URL 就是这个 Jar 包,原来的每个个体路径将转变为 jar:file:URL!/{entry}。但这种路径并不是文件资源定位符的格式,而 File 这个类使用的是系统的文件资源定位符的格式定位文件的,所以导致 FileNotFoundException 错误的出现。Java 自带的 JarFile 类可以识别一层 Jar 路径。像 Spring Jar 包内的 Jar 包多层嵌套情况下 JarFile 类还是不可以,是 Spring 写了自己的类加载器才能加载多层嵌套下的文件

解决办法:类加载器 ClassLoader

Jar 包跑起来,肯定需要知道包内的每一份文件信息尤其是class文件。而加载并记录这些信息就是通过ClassLoader 的实现类。通过类加载器我们可以获取到这个文件的流,一般情况我们会直接使用流处理所需要的信息,但如果实在需要 File 。可以在 Jar 包相同目录下将文件流写进这个新建的文件下。

1:获取自身的类加载器再找 vue-static.xml 文件

// 例:resources/template/vue-static.xml 文件
import java.net.URL;
URL resource = this.getClass().getClassLoader().getResource("template/vue-static.xml");
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("kubernetes/aly-dev-config");

2:获取上下文类加载器再找 vue-static.xml 文件

URL resource = Thread.currentThread().getContextClassLoader().getResource("template/vue-static.xml");

3:找一个类获取他的类加载器再找 vue-static.xml 文件

// Resources类是随便找的
URL resource = Resources.class.getClassLoader().getResource("template/vue-static.xml");

4:使用 Spring 的自带的类找 vue-static.xml 文件

import org.springframework.core.io.ClassPathResource;
InputStream resourceAsStream = new ClassPathResource("template/vue-static.xml").getInputStream();

默认使用 Thread.currentThread().getContextClassLoader() 的类加载器。可以手动指定

需要注意 new ClassPathResource().getFile() 方法。因为方法内进行了路径校验,在 Jar 包下使用依旧会报 FileNotFoundException 错误。

类加载器的变化

使用 idea 启动时,类加载器为系统默认类加载器 sun.misc.Launcher.AppClassLoader

打成 Jar 包启动时,类加载为 Spring 指定的类加载器 org.springframework.boot.loader.LaunchedURLClassLoader,值得注意的是这个类我只在 Spring Boot GitHub 上的看到过,它在 spring-boot-project 下的 spring-boot-tools 下的 spring-boot-loader 模块下。在idea启动时没看到依赖这个文件,打成 Jar 包时看依赖也没找到它。只有在启动 Jar 包通过 Idea debug 时看到它。

其它知识

1:使用 Class.forName() 反射获取类时,尽量不用指定类加载器,如需要指定可以用 Thread.currentThread().getContextClassLoader() 获取。而不是 ClassLoader.getSystemClassLoader() 因为后者在 Jar 包情况下获取到的依旧是 AppClassLoader 类加载器,从而出现找不到类问题。

2:Jar 包启动类加载器是 LaunchedURLClassLoader 的原因。

  1. 使用命令启动 Jar 包
  2. 先运行META-INF/MANIFEST.MF文件Main-Class属性对应的 class 文件 main 方法作为程序入口启动类(不是你项目的 Application )
  3. 将 BOOT-INF/classes 下的类文件和 BOOT-INF/lib 下依赖的 Jar 加入到 classpath 下,扩展一种URL协议。支持 Jar 包嵌套找到文件。
  4. 每个线程 Thread 都有 contextClassLoader ,可以用来传递类加载器,子线程默认会使用父线程的ClassLoader。在这时就将现在运行的这根主线程的 contextClassLoader 设置 LaunchedURLClassLoader 作为类加载器。
  5. 调用META-INF/MANIFEST.MF文件Start-Class属性完成应用程序的启动
  6. 更多细节可以看看这位大佬写的文章 终于搞懂了SpringBoot的jar包启动原理

3:对于任意一个类,都需要由加载它的类加载器和这个类本身一起共同确定其在Java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间

4:双亲委派机制打破原理是对于某个范围的类不向上进行委托加载,实现隔离。重点是ClassLoader 下的 loadClass 方法。Tomcat 就是打破了双亲委派机制,使多个 web 应用在一个 Tomcat 中运行,互相之间有隔离。建议阅读大佬写的 Tomcat为何要打破双亲委派机制

总结

每一个问题,当对其不断挖深后,都能看到很多知识,引申出其它问题。所以我不能放松,加油!