读取ClassPath下resource文件的正确姿势(转)

本文深入探讨Java中资源文件的加载机制,解释为何部分代码在不同环境下读取资源文件时会出现问题。文章剖析类加载机制,特别是类加载器的工作原理,以及如何利用类加载器确保资源文件的正确加载。

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

参考:https://blog.youkuaiyun.com/weixin_41715878/article/details/83278600

           https://www.cnblogs.com/liuzhipeng/p/7816507.html

           https://blog.youkuaiyun.com/qq_27466827/article/details/83790338

           https://www.cnblogs.com/shuimuzhushui/p/7247864.html

 

1.前言

为什么要写这篇文章?身为Java程序员你有没有过每次需要读取 ClassPath 下的资源文件的时候,都要去百度一下,然后看到下面的这种答案:

Thread.currentThread().getContextClassLoader().getResource("ss.properties").getPath();

亦或是:

Object.class.getResourceAsStream("ss.properties");

你复制粘贴一下然后放到自己的项目里运行,还真跑起来了。但是当打成 jar 包作为其它项目的依赖时,或者打成 war 包被 Tomcat 加载时,你还能保证你的resources 资源文件被读取到吗?答案是不能的。

其中的原因如何而又如何解决,究竟怎样才能写出万无一失根本不用担心任何环境的代码?个中原委,请听我一一道来。

2.再看类加载机制

看到这个标题你也许会有些意外,不是说的读取ClassPath下的文件吗?为什么要讲类加载机制。

其实你有没有想过,ClassPath下的资源文件标准存放的是什么?顾名思义,是 .class 类文件。为什么我们的类可以被正确加载到Java虚拟机(JVM),而自己添加的资源文件却加载失败呢?归根结底是你没有理解类加载机制,也就无法做到举一反三。

类加载机制与类加载器

程序员将源代码写入.Java文件中,经过(javac)编译,生成.class二进制文件。虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

从宏观上理解了类加载机制后,接下来就要从细节上说一说类加载器,以及类加载器的工作原理。

类加载器,顾名思义,是加载类的器件。JVM只存在两种不同的类加载器:启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机自身的一部分。另一种是所有其他的类加载器,使用JAVA实现,独立于JVM,并且全部继承自抽象类java.lang.ClassLoader。包括扩展类加载器、应用程序类加载器。

它我们在写代码时,总是会new很多对象,我们之所以可以new出对象,是因为该对象对应的类已经被JVM加载为Class类的对象实例。这句话有点绕,我用代码展示一下:

Obj obj = new Obj(); //Obj对象实例
Class o = obj.getClass(); //Obj类是Class类的对象实例

在JVM中,一般情况下,我们的类的类实例是唯一的,这得益于类加载机制的双亲委派模型。

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都是应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

3.类也是一种Resource

言归正传,通过上述对类加载机制的学习,我们可以得出这样的一个结论:一个类文件是由某个类加载器负责加载到JVM中的,且只会有一个类加载器去加载。反过来说,由一个类实例就可以获取到加载它到JVM中的那个类加载器。

用代码阐述我的上段话如下所示:

Obj obj = new Obj();
ClassLoader classLoader = obj.getClass().getClassLoader();

跟着我的思路继续走,该类加载器之所以可以加载这个类,是因为这个类在该类加载器的搜索范围内。类加载器既然可以加载这个类文件,那么也可以加载该类文件同级目录下的所有资源文件。

所以,我们要想确保可以读取到某个资源文件,只需调用和该资源文件在同一目录下的类的Class对象的getClassLoader()方法获取该类加载器即可

举个例子,我们有一个properties文件和Obj.class在同一个目录下, 那我们读取该properties文件的最正确的方式就是通过Obj.class.getClassLoader().getResourceAsStream()方法。

4.一个错误的例子

为了印证上面的结论,先看下 Object.class.getResourceAsStream() 的源码:

// Class.java
public InputStream getResourceAsStream(String name) {
    name = resolveName(name);
    ClassLoader cl = getClassLoader0();
    if (cl==null) {
        // A system class.
        return ClassLoader.getSystemResourceAsStream(name);
    }
    return cl.getResourceAsStream(name);
}

从 Javadoc 文档和源码中可以看出:

Class.getResourceAsStream() 代理给了加载该 class 的 ClassLoader 去实现,调用 classLoader.getResourceAsStream(),如果该类的 ClassLoader 为 null,说明该 class 一个系统 class,所以委托给 ClassLoader.getSystemResourceAsStream。

这一点也印证了之前讲解的原理:资源文件都是由ClassLoader负责加载的,类也是一种resources文件

但通过Object.class.getResourceAsStream()不一定可以搜索到指定的资源文件,原因就在于前面说过的类加载器的搜索范围,所以这种方式并不推荐使用。

### 如何在 Spring Boot 项目中读取 resources 目录下的文件 为了实现从 `resources` 文件夹下读取文件,在 Java 中可以利用类加载器来获取资源路径。下面展示一种通过 ClassLoader 来访问位于 classpath 下面的静态资源的方式[^1]。 对于简单的文本文件,可以直接使用如下方法: ```java import java.io.InputStream; import org.springframework.core.io.ClassPathResource; public class ResourceReader { public static void main(String[] args) throws Exception { // 假设要读取名为 example.txt 的文件 InputStream inputStream = new ClassPathResource("example.txt").getInputStream(); try (Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) { while (scanner.hasNextLine()) { System.out.println(scanner.nextLine()); } } catch (IOException e) { throw new RuntimeException(e); } } } ``` 如果需要处理更复杂的场景比如 XML 或者 JSON 数据,则可以根据具体需求引入相应的解析库并调整上述逻辑中的数据处理部分。 另外值得注意的是,当配置了特定的安全属性时,可能会影响某些操作的行为模式。例如设置安全相关参数可能会改变应用程序上下文初始化过程中的权限控制机制[^2]。不过这通常不会影响到基本的文件读写功能。 #### 使用 @Value 注解读取 properties 配置文件的内容 除了直接读取物理文件外,还可以借助于 Spring 提供的 `@Value` 注解轻松注入来自 application.properties 或其他自定义 property 文件里的键值对信息。这种方式特别适合用来管理常量字符串或者其他不需要频繁变动的数据项。 ```java @Component class MyComponent { private final String webServicePath; public MyComponent(@Value("${spring.webservices.path}") String webServicePath) { this.webServicePath = webServicePath; } // ... } ``` 此代码片段展示了如何将 `application.properties` 中定义的服务路径自动映射给组件内的成员变量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值