Spring Boot读取类路径资源子目录:Jar包运行报FileNotFoundException
解决Spring Boot Jar包运行时读取Classpath目录子目录的问题
我之前也遇到过一模一样的问题!核心原因其实很简单:Jar包是一个压缩归档文件,里面的目录并不是操作系统层面的真实文件系统目录,而ResourceUtils.getFile()本质上是要获取文件系统里的File对象,所以打包成Jar后自然就找不到路径了。
针对Java 8环境,这里有两个靠谱的解决方案:
解决方案一:使用Spring官方推荐的ResourcePatternResolver(推荐)
既然是Spring Boot项目,直接用Spring提供的资源扫描工具最省心,它天然支持Jar内资源的读取,而且代码简洁:
import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import java.io.IOException; import java.util.List; import java.util.stream.Collectors; public List<String> getSubdirectories() throws IOException { ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); // 匹配scripts目录下的所有直接子目录,末尾的/是关键,用来过滤出目录 Resource[] resources = resolver.getResources("classpath:/database/scripts/*/"); // 从资源URL中解析出子目录名称 return List.of(resources).stream() .map(resource -> { try { String urlPath = resource.getURL().getPath(); // 适配Jar和本地文件系统的路径格式,提取最后一级目录名 int lastSlashIndex = urlPath.lastIndexOf('/', urlPath.length() - 2); return urlPath.substring(lastSlashIndex + 1, urlPath.length() - 1); } catch (IOException e) { throw new RuntimeException("Failed to resolve directory name", e); } }) .collect(Collectors.toList()); }
代码说明:
PathMatchingResourcePatternResolver是Spring专门用来扫描类路径资源的工具,不管是本地文件系统还是Jar包内的资源都能处理。classpath:/database/scripts/*/这个通配符模式会精准匹配scripts目录下的所有直接子目录,避免把文件也扫进来。- 通过
resource.getURL()获取资源路径后,做简单的字符串截取就能拿到子目录名称,同时兼容IDE和Jar两种运行环境。
解决方案二:纯Java原生实现(无Spring依赖)
如果不想依赖Spring的API,也可以用Java原生的ClassLoader和JarFile来处理,手动区分运行环境:
import java.io.File; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Collectors; public List<String> getSubdirectories() throws IOException { List<String> subDirNames = new ArrayList<>(); ClassLoader classLoader = getClass().getClassLoader(); URL resourceUrl = classLoader.getResource("database/scripts"); if (resourceUrl == null) { return subDirNames; } String protocol = resourceUrl.getProtocol(); if ("file".equals(protocol)) { // IDE本地运行环境,直接用文件系统API处理 File dir = new File(resourceUrl.getFile()); subDirNames = Arrays.stream(dir.listFiles(File::isDirectory)) .map(File::getName) .collect(Collectors.toList()); } else if ("jar".equals(protocol)) { // Jar包运行环境,解析Jar文件内容 String jarPath = resourceUrl.getPath().substring(5, resourceUrl.getPath().indexOf("!")); try (JarFile jarFile = new JarFile(jarPath)) { Enumeration<JarEntry> entries = jarFile.entries(); String targetDirPrefix = "database/scripts/"; while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String entryName = entry.getName(); // 筛选出scripts下的直接子目录,排除多级嵌套目录 if (entry.isDirectory() && entryName.startsWith(targetDirPrefix)) { String subDir = entryName.substring(targetDirPrefix.length()); if (!subDir.contains("/")) { subDirNames.add(subDir); } } } } } return subDirNames; }
代码说明:
- 通过
ClassLoader.getResource()获取资源URL,然后判断协议是file(本地文件系统)还是jar(Jar包内)。 - Jar环境下直接打开Jar文件,遍历所有条目,筛选出目标目录下的直接子目录,避免把嵌套目录也包含进来。
- 用
try-with-resources自动关闭JarFile,避免资源泄漏。
额外注意事项
- 确保
database/scripts目录在打包时被正确包含:Maven/Gradle默认会把src/main/resources下的所有文件和目录打包进Jar,所以把目录放在这里就没问题。 - 避免使用
ResourceUtils.getFile():Spring官方文档明确不推荐在生产环境使用这个方法,因为它仅适用于文件系统中的资源,无法处理Jar内的资源。 - 性能考量:如果目录下有大量资源,扫描可能会有轻微性能影响,但一般脚本目录不会太大,这个问题可以忽略。
内容的提问来源于stack exchange,提问作者Sachin




