You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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原生的ClassLoaderJarFile来处理,手动区分运行环境:

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,避免资源泄漏。

额外注意事项

  1. 确保database/scripts目录在打包时被正确包含:Maven/Gradle默认会把src/main/resources下的所有文件和目录打包进Jar,所以把目录放在这里就没问题。
  2. 避免使用ResourceUtils.getFile():Spring官方文档明确不推荐在生产环境使用这个方法,因为它仅适用于文件系统中的资源,无法处理Jar内的资源。
  3. 性能考量:如果目录下有大量资源,扫描可能会有轻微性能影响,但一般脚本目录不会太大,这个问题可以忽略。

内容的提问来源于stack exchange,提问作者Sachin

火山引擎 最新活动