资讯动态

SpringBoot读取resource目录文件的六种方法及避坑指南

发布时间:2026/10/16 19:43:57 来源:尧图企业网站定制
我之前在做一个内部管理系统的报表模块时遇到一个特别基础却让我卡了一整个下午的需求在SpringBoot项目里读取resource目录下的一个模板Excel文件。文件明明就在项目里躺着代码用new File()怎么都读不到不是报路径不存在就是空指针。后来和同事一聊才发现这个问题不只是新手会碰见很多写了两三年Java的人也经常在打包部署后踩坑。这篇就结合我当时排查和实测的经过把SpringBoot中读取resource目录文件的六种方法完整拆开讲一遍顺便把各自的坑和适用场景也一并交代清楚。1. 为什么要写这六种方法1.1 一个看似简单却总出错的场景先还原一下当时的场景项目里有一个src/main/resources/templates/目录下面放着若干JSON模板文件业务启动时要把这些模板加载进内存供后续接口动态渲染。本地IDE里跑SpringBoot一切正常一旦用Maven打成jar包部署到服务器程序就报FileNotFoundException。这事的根源在于resource目录下的文件在编译期会被打包进jar包内部它不再是一个操作系统层级上看得见的普通文件路径。你在本地用相对路径src/main/resources/xxx.json能读到是因为IDE把src/main/resources配成了classpath目录而打包后就完全变了形态。因此读resource文件不能想当然用new File()必须通过classpath机制来定位。1.2 六种方法的适用场景对比我把项目里实际用过的、以及调研过程中确认可行的方案整理成了六种方法核心工具类适用场景主要风险点ClassPathResourceSpring自带通用性最好框架项目首选需要Spring环境ResourceLoader注入SpringIOC需要读取多个动态路径时依赖注入容器ClassLoader流读取JDK原生轻量、不带Spring时用拿不到文件实体只能取流ResourceUtils的File方式Spring工具类本地文件系统调试jar包里容易失效Hutool/Commons-IO封装第三方工具想少写样板代码引入额外依赖ServletContext读取Web容器传统Servlet部署场景仅限Web环境一句话总结读取resource目录文件的本质不是找路径而是定位classpath资源。先懂这个底层逻辑下面每个方法看起来就会非常顺。2. 读resource文件的底层原理2.1 classpath到底是什么很多新手搞不懂classpath我习惯用一个生活化类比来解释。你去快递站取件不需要知道快递车走哪条路、货架在几号区只需要给取件码程序找类文件、找配置资源也一样classpath就是一套统一的逻辑取件码规则。Maven项目里src/main/resources目录下的所有文件最终会被拷贝到target/classes下和编译后的.class文件站在同一层。运行Java程序时JVM把target/classes或jar内的BOOT-INF/classes作为classpath根目录之一。所以当你说读取classpath:data/template.json时程序是在classpath根目录下找data/template.json而不是在操作系统的绝对路径里找。这个思维转换是解决所有resource读取失败问题的第一步。2.2 三类加载器的本质差异结合上面的逻辑可以看三组关键对象ClassLoaderJDK提供的类加载器负责从classpath中加载类和资源。通过getResourceAsStream可以直接返回资源的输入流。Resource和ResourceLoaderSpring在JDK之上封装的一套资源抽象用classpath:前缀统一描述资源位置。FileJDK的文件对象直接对应文件系统路径和classpath没有直接关系这也是new File(classpath:xxx)这种写法错误的原因。在实际编码中ClassLoader更底层Resource更方便File更适合处理文件系统里的临时产物。三者的选择顺序取决于你是否需要拿到文件实体以及你的代码运行在什么环境里。3. 六种方法的完整代码实现3.1 方法一ClassPathResource InputStream这是我在SpringBoot项目里最常用的一种。它读取的是classpath:前缀对应的资源API简洁并且拿到了Spring的Resource对象后可以自由地转成InputStream、File或者URL后续扩展性最好。import org.springframework.core.io.ClassPathResource; import org.springframework.util.StreamUtils; import java.nio.charset.StandardCharsets; public class ReadResourceDemo1 { public static String readByClassPathResource() { ClassPathResource resource new ClassPathResource(data/template.json); try (InputStream inputStream resource.getInputStream()) { return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(读取资源失败, e); } } }这里有三个细节值得注意。第一ClassPathResource默认以classpath根目录为基准如果文件在data子目录下就必须写全路径data/template.json。第二resource.getInputStream()拿到的是输入流大多数场景下用流就够了不需要把资源转成File。第三流用完必须关闭我习惯用try-with-resources省去手动关流的麻烦。3.2 方法二ResourceLoader注入读取ResourceLoader是Spring提供的一个更抽象的接口它的好处是你可以通过字符串形式的路径前缀来动态指定资源类型比如classpath:、file:、url:。在SpringBoot里直接注入即可Spring容器会自动装配。import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; import org.springframework.util.StreamUtils; Component public class ReadResourceDemo2 { private final ResourceLoader resourceLoader; public ReadResourceDemo2(ResourceLoader resourceLoader) { this.resourceLoader resourceLoader; } public String readByResourceLoader() { Resource resource resourceLoader.getResource(classpath:data/template.json); try (InputStream inputStream resource.getInputStream()) { return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(读取资源失败, e); } } }这个方法和方法一的本质区别在于方法一直接new ClassPathResource硬编码了资源类型方法二把路径字符串交给ResourceLoader解析如果你想统一管理配置或者同一份代码要读取多种来源的资源用ResourceLoader更灵活。当时项目里我做一个多租户配置模块时就是靠这个动态前缀能力既能读模板目录下的文件也能读外部挂载盘的文件。3.3 方法三ClassLoader.getResourceAsStream如果不依赖Spring纯JDK环境下也有办法。通过Thread.currentThread().getContextClassLoader()拿到类加载器直接调getResourceAsStream获取classpath资源的输入流。import java.nio.charset.StandardCharsets; public class ReadResourceDemo3 { public static String readByClassLoader() { ClassLoader classLoader Thread.currentThread().getContextClassLoader(); try (InputStream inputStream classLoader.getResourceAsStream(data/template.json)) { if (inputStream null) { throw new RuntimeException(资源不存在: data/template.json); } return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(读取资源失败, e); } } }这里有个很容易踩的空指针坑getResourceAsStream如果找不到资源返回的是null而不是抛异常。所以调用后先判空再使用。另一个细节是获取ClassLoader的方式不止一种我推荐用线程上下文类加载器因为它在SpringBoot这种多类加载器环境下更可靠能正确加载到Web容器或框架里动态装配的资源。3.4 方法四ResourceUtils File对象这个方法前后遇到的分歧比较大因为它在IDE里跑很顺打包成jar就失效不少人因此断言它不能用。先说结论它适合开发阶段调试或者部署方式是解压目录而非jar包形态。它的本质是把classpath资源通过file:协议转成文件系统上的真实File对象。import org.springframework.util.ResourceUtils; import java.io.File; import java.io.FileInputStream; import java.nio.charset.StandardCharsets; public class ReadResourceDemo4 { public static void readByResourceUtils() { try { File file ResourceUtils.getFile(classpath:data/template.json); String content new String(new FileInputStream(file).readAllBytes(), StandardCharsets.UTF_8); System.out.println(content); } catch (IOException e) { throw new RuntimeException(读取资源失败, e); } } }为什么jar包环境下这个方法会失效因为jar包里的内容不是操作系统的平铺文件FileInputStream没法直接打开一个位于jar内部的路径。如果你只是本地开发时读取配置文件做测试这个方法简单直接完全没问题如果是打成jar跑生产就尽量别碰它。3.5 方法五Hutool / Commons-IO封装读取第三方工具类能帮你省掉大量样板代码。Hutool的ResourceUtil一行搞定Commons-IO配合Guava的Resources也能做到。它们的底层实现和我上面说的方法三大同小异只是封装了判空、编码处理等细节。// Hutool import cn.hutool.core.io.resource.ResourceUtil; String content ResourceUtil.readUtf8Str(data/template.json); // Commons-IO Guava import org.apache.commons.io.IOUtils; import com.google.common.io.Resources; import java.nio.charset.StandardCharsets; byte[] bytes IOUtils.toByteArray(Resources.getResource(data/template.json)); String content new String(bytes, StandardCharsets.UTF_8);Hutool的readUtf8Str方便之处在于读文本时自动处理了UTF-8编码不需要手动new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)。不过引入这类工具之前要考虑团队规范如果项目本身已经有Commons-IO就没必要再加Hutool如果两个都没有加一个Hutool确实能提升日常开发效率。3.6 方法六ServletContext读取Web环境这个方法我是在做传统Servlet项目时才派上用场的。如果你的SpringBoot应用仍以war包形式部署在外部容器里ServletContext是Web容器提供的全局上下文对象可以直接用getResourceAsStream读取classpath下的资源路径以/WEB-INF/classes/开头。import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; public class ReadResourceDemo6 { public static String readByServletContext(HttpServletRequest request) { ServletContext servletContext request.getServletContext(); try (InputStream inputStream servletContext.getResourceAsStream(/WEB-INF/classes/data/template.json)) { if (inputStream null) { throw new RuntimeException(资源不存在); } return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(读取资源失败, e); } } }用这个方法时最需要注意的是环境约束只有Web应用运行在外置Servlet容器里ServletContext才可用SpringBoot默认用内嵌Tomcat以jar方式启动时虽然也存在ServletContext但资源路径的解析逻辑和war包不同直接用这个方式容易吃哑巴亏。现在多数项目都以内嵌容器为主这个方法出场率不高了解即可。4. 实际项目中的问题排查与避坑实录4.1 打包成jar后ClassPathResource读不到文件我遇到过一次很典型的问题代码本地一切正常部署到测试服务器后抛FileNotFoundException。排查时先在本地执行mvn clean package -DskipTests然后用jar tf target/xxx.jar查看包结构发现data/template.json被打到了BOOT-INF/classes/data/template.json。这是因为SpringBoot的可执行jar内部自带装载器classpath根目录实际是BOOT-INF/classes。很多人在代码里写死绝对路径或者用了ClassLoader.getResource()拿到URL后又new File(url.toURI())都会在jar环境下出问题。正确做法是始终用流的方式读取资源不要去尝试把jar包内部的资源转成File。如果非得转成File唯一的方案是将流先拷贝到系统临时目录再在临时目录生成File对象。4.2 路径带空格或中文的问题还有一次项目在移动端展示PDF我把模板文件放在了带空格的目录下比如resource/demo files/template.pdf。开发环境能读但Linux服务器上某些环节会莫名报路径错误查下来才发现是空格在URL解码时出了问题。如果路径里有空格用ClassPathResource时相对安全因为Spring内部做了URL解码而直接用ClassLoader.getResource()返回的URL再手动URLDecoder时就要格外小心编码格式。经验是资源文件命名尽量避免中文字符和空格宁可多建一层目录也不要给后续埋坑。4.3 想读目录下的所有文件怎么办有时候不只是读单个文件而是要遍历resource/templates目录下所有JSON。如果直接用ClassLoader.getResource(templates)拿URL再通过File遍历目录在jar包环境下会非常痛苦因为目录不存在于文件系统中。我当时的解决思路有两条。第一维护一个清单文件把需要加载的文件名写在一个list.properties里程序按清单逐个通过ClassPathResource读取简单可靠。第二引入可处理嵌套jar的目录扫描工具比如Hutool的FileUtil.loopFiles不能直接扫jar内部需要借助JarFile手动遍历jar条目。从成本和稳定性考虑小规模场景用清单文件最稳。4.4 编码问题中文内容读出来乱码读text、json资源最常见的坑是编码。我见过不少同事用new String(inputStream.readAllBytes())结果Windows上文件是UTF-8代码运行在服务器上默认GBK一读就乱码。这个问题没有技术含量纯粹是踩坑经验读取文本资源时永远显式指定字符集。// 错误示例依赖平台默认编码极易乱码 String content new String(inputStream.readAllBytes()); // 正确示例 String content new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);Hutool的readUtf8Str内部就是固定UTF-8。如果你自己写一定不要偷懒。这个坑在Windows下开发、Linux下部署的项目里非常常见属于那种报错不难查、但很耗时间的低级问题。5. 不同环境下的选择建议5.1 SpringBoot jar包部署在这种最常见的方式下我的选择很固定优先方法一ClassPathResource或方法二ResourceLoader。两者都能安全读取jar内部资源且编码、关闭流这些细节都有Spring帮忙兜底。如果只是简单的工具类调用方法一更简洁如果追求可复用性和路径可配置性方法二更合适。5.2 本地调试与单元测试本地调试时方法四ResourceUtilsFile阅读起来更直观因为可以拿到真实的File对象方便IDE里看路径。但单元测试里我不建议依赖File因为测试环境同样可能打成jar运行稳定性会受影响。改用方法三ClassLoader.getResourceAsStream写测试最为稳妥。5.3 嵌入式容器的特殊写法有朋友问我既然SpringBoot默认用内嵌Tomcat是不是也可以用ServletContext。理论上可以但你没法确定容器何时初始化也不方便在非Web层调用所以在绝大多数SpringBoot场景下直接用ClassPathResource就够用了没必要绕道ServletContext。6. 六种方法的对比与我的个人体会6.1 公开对比表选型一目了然方法是否依赖Springjar包内可用文本读取体验推荐指数ClassPathResource是是配合StreamUtils很顺手五星ResourceLoader是是同上且动态性更强五星ClassLoader流否是需自行处理编码和判空四星ResourceUtilsFile是否直观但只在本地好用三星Hutool/Commons-IO否是体验最好需引入依赖四星半ServletContext是war包内可用场景受限两星6.2 一个六种方法都能跑通的完整示例如果你想把六种方案放在同一个类里对比验证可以按下面这个结构组织测试代码。这里我以读取data/template.json为例统一门面方法输入原始路径。public interface ResourceReader { String read(); } Component public class ClasspathReaderFacade { private final ResourceLoader resourceLoader; public ClasspathReaderFacade(ResourceLoader resourceLoader) { this.resourceLoader resourceLoader; } public String readByClassPathResource() { ClassPathResource resource new ClassPathResource(data/template.json); try (InputStream is resource.getInputStream()) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(e); } } public String readByResourceLoader() { Resource resource resourceLoader.getResource(classpath:data/template.json); try (InputStream is resource.getInputStream()) { return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(e); } } public String readByClassLoader() throws IOException { try (InputStream is Thread.currentThread().getContextClassLoader() .getResourceAsStream(data/template.json)) { if (is null) { return null; } return new String(is.readAllBytes(), StandardCharsets.UTF_8); } } public String readByFileForLocalDebug() throws IOException { File file ResourceUtils.getFile(classpath:data/template.json); return new String(new FileInputStream(file).readAllBytes(), StandardCharsets.UTF_8); } public String readByHutool() { return ResourceUtil.readUtf8Str(data/template.json); } }运行这个Facade时建议在IDE里先跑一遍再mvn clean package后从jar包运行一遍你就能直观体会到哪些方法在两种环境下都能用哪些只能本地跑。这个对比经验比单纯背结论更有用。6.3 我踩过坑之后的几个习惯第一代码里固定写ClassPathResource除非明确需要动态路径才换ResourceLoader避免在团队里引入乱七八糟的读取风格。第二所有读取文本的地方强制显式指定UTF-8不接受默认编码。第三凡是打包部署后出现资源找不到的报错第一反应不是改代码而是先用jar tf查看包内结构确认文件是否真的在预期位置。第四遇到列举目录下多个文件的场景尽早切换到清单驱动模式不依赖目录扫描。这几个习惯帮我省了非常多排查时间尤其是项目多、环境杂的时候效果特别明显。另外如果你的项目里对配置文件做了profile切换比如application-prod.yml读取资源时也要注意资源路径可能随环境变化。我曾经在开发环境用dev/template.json生产环境用的是prod/template.json结果因为路径写死导致上线后读了个空。后来统一改成通过配置项传入资源路径前缀再拼上文件名问题就彻底没了。7. 最后再补一句这类问题的解法本身不复杂复杂的是很多人不愿意花时间弄懂classpath和文件系统路径的区别结果换个部署环境就抓瞎。我经历过那个下午的排查后最大的收获不是记住了六个方法的名字而是养成了先确认资源在包里的位置再决定用哪种方式读的思考习惯。如果你读完这篇能动手在本地跑一个对比Demo把jar包打包前后各验证一遍这类问题以后就很难再坑到你了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑