1. 为什么需要读取resource目录文件?
在SpringBoot项目中,resource目录是存放静态资源和配置文件的标配位置。这里可能放着SQL脚本、Excel模板、证书文件、配置文件等各种关键资源。但很多开发者第一次尝试读取这些文件时,往往会遇到FileNotFoundException——因为SpringBoot打包后,resource下的文件会进入jar包内部,传统的File类读取方式完全失效。
上周我就帮同事排查过一个典型问题:他本机测试时用new File("src/main/resources/init.sql")能正常运行,但打成jar包部署后脚本死活找不到。这正是因为没有掌握SpringBoot环境下读取resource文件的正确姿势。下面这六种方法,覆盖了从基础到高阶的各种场景需求。
2. 六种核心方法详解
2.1 ClassLoader.getResourceAsStream(最基础方案)
java复制InputStream inputStream = this.getClass()
.getClassLoader()
.getResourceAsStream("files/test.txt");
这是最基础的读取方式,通过类加载器获取资源流。注意几个关键点:
- 路径不需要写
/开头 - 文件不存在时返回
null而不是抛异常 - 适合读取一次性使用的资源文件
警告:不要用
getResource()获取URL再转File对象,这在jar包内会报错!
2.2 Class.getResourceAsStream(相对路径方案)
java复制// 相对路径(相对于当前类所在包)
InputStream input1 = MyClass.class.getResourceAsStream("config.properties");
// 绝对路径(从classpath根开始)
InputStream input2 = MyClass.class.getResourceAsStream("/static/logo.png");
与方案1的区别在于:
- 不写
/时路径相对于当前类所在包 - 写
/时从classpath根目录开始 - 适合需要区分相对/绝对路径的场景
2.3 ResourceUtils(仅开发环境适用)
java复制File file = ResourceUtils.getFile("classpath:application.yml");
虽然写法简单,但有个致命缺陷——只能在开发环境使用。因为ResourceUtils底层还是用File类实现,打包后必然失效。建议仅用于测试代码。
2.4 ResourceLoader(Spring官方推荐)
java复制@Autowired
ResourceLoader resourceLoader;
Resource resource = resourceLoader.getResource("classpath:data.json");
InputStream input = resource.getInputStream();
这是Spring生态的标准做法,优势在于:
- 统一了各种资源定位方式(classpath/file/http等)
- 自动兼容jar包内外的读取
- 支持Ant风格路径匹配(如
classpath*:/config/*.xml)
2.5 PathMatchingResourcePatternResolver(批量扫描)
java复制Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("classpath*:/static/*.css");
当需要批量获取资源时(如读取所有配置文件),这个方案特别有用。注意:
classpath*:语法会搜索所有jar包- 支持Ant风格通配符
- 性能比单文件读取略低
2.6 通过Environment读取配置文件
java复制@Value("classpath:license.key")
Resource licenseResource;
@Autowired
Environment env;
String content = env.getProperty("spring.config.location");
严格来说这不是文件读取,但对properties/yml等配置文件,直接通过环境变量获取更符合SpringBoot哲学。
3. 实战对比与选型建议
| 方案 | 适用场景 | 是否支持jar包 | 是否支持通配符 |
|---|---|---|---|
| ClassLoader | 简单单文件读取 | 是 | 否 |
| Class | 需要相对路径的场景 | 是 | 否 |
| ResourceUtils | 仅开发环境测试使用 | 否 | 否 |
| ResourceLoader | 生产环境标准用法 | 是 | 是 |
| PathMatching | 批量文件扫描 | 是 | 是 |
| Environment | 配置文件专用 | 是 | 否 |
根据我的项目经验:
- 日常开发首选
ResourceLoader方案 - 批量操作用
PathMatchingResourcePatternResolver - 单元测试可以配合
@TestPropertySource使用Environment - 绝对不要在生产环境用
ResourceUtils
4. 高频踩坑实录
4.1 路径格式问题
常见错误写法:
java复制// 错误!不要加"src/main/resources"
getResourceAsStream("src/main/resources/file.txt");
// 错误!不要用Windows路径
getResourceAsStream("\\static\\image.jpg");
正确姿势:
- 永远使用
/作为路径分隔符 - 路径相对于classpath根目录
- 使用
classpath*:前缀时需要格外小心
4.2 资源释放问题
我见过最隐蔽的内存泄漏是这样产生的:
java复制// 错误!没有关闭流
String content = new Scanner(
getClass().getResourceAsStream("data.txt")
).useDelimiter("\\Z").next();
必须用try-with-resources:
java复制try (InputStream input = resource.getInputStream();
Scanner scanner = new Scanner(input)) {
return scanner.useDelimiter("\\Z").next();
}
4.3 文件修改监听
有个需求要热更新配置文件,有人这样写:
java复制// 错误!jar包内文件无法监听
WatchService watcher = FileSystems.getDefault()
.newWatchService();
Paths.get("classpath:config.properties")
.register(watcher, ENTRY_MODIFY);
正确做法是:
- 将可修改文件放在jar包外(如
/config目录) - 使用Spring Cloud Config等专业方案
- 或者用
ResourceLoader的FileSystemResource实现
5. 高级技巧:自定义资源加载
当标准方案不能满足需求时,可以考虑实现Resource接口。比如我们需要从数据库读取模板文件:
java复制public class DatabaseResource extends AbstractResource {
private final String templateId;
@Override
public InputStream getInputStream() {
return templateService.getTemplateStream(templateId);
}
}
// 使用时
Resource resource = new DatabaseResource("T001");
这种扩展方式在以下场景特别有用:
- 需要加密/解密的资源文件
- 存放在非标准位置的资源
- 动态生成的临时文件
6. 性能优化建议
-
缓存资源流:对于频繁读取的静态资源(如证书文件),应该缓存InputStream而不是重复打开
java复制@Component public class CertHolder { private final byte[] certData; public CertHolder(@Value("classpath:server.p12") Resource res) { this.certData = StreamUtils.copyToByteArray(res.getInputStream()); } } -
预加载验证:应用启动时检查关键资源是否存在
java复制@PostConstruct public void validateResources() { Assert.state(resource.exists(), "关键配置文件缺失"); } -
谨慎使用classpath*:通配符搜索会遍历所有jar包,在大型项目中可能影响启动速度
最后分享一个真实案例:我们曾因为误用ResourceUtils导致生产环境读取不到HTML模板,紧急回滚后改用ResourceLoader才解决问题。这些经验教训,希望你能避开。