Spring Boot中如何读取static目录下的文件

需求

设在一个Spring Boot项目中,static目录的结构如下:

1
2
3
4
5
static/
|-- the_dir/
|-- sub_dir/
|-- file1
|-- file2

现在想拿到the_dir目录中的文件列表(不含子目录),读取每个文件的内容,并计算每个文件相对于static目录的路径(即浏览器访问路径)。

获取文件列表

思路一:操作文件系统(不可行)

通过Spring的工具类ResourceUtils.getFile("classpath:static/the_dir")拿到the_dir目录的File对象,再通过theDir.listFiles()拿到文件列表,文件列表中可以通过file.isDirectory()判断是普通文件还是目录。

结果:IDE中开发、Maven测试阶段都正常,但打包后运行jar包时报错:

1
2
Caused by: java.io.FileNotFoundException: class path resource [static/the_dir] cannot be resolved to absolute file path because it does not reside in the file system: jar:nested:/C:/.../target/xxx.jar/!BOOT-INF/classes/!/static/the_dir
at org.springframework.util.ResourceUtils.getFile(ResourceUtils.java:218) ~[spring-core-6.1.2.jar!/:6.1.2]

原因:运行jar包时,static目录及其下属各文件都在jar包中,而不在文件系统上,所以ResourceUtils抛出了FileNotFoundException异常。即,不能按文件系统的方式用Java文件API去操作。

思路二:操作资源(可行)

查询Spring Framework的文档可知,Spring抽象了一个称为**“资源(Resource)”**的概念,资源既可以是文件系统中的文件,也可以是jar包中的文件。

通过资源解析器org.springframework.core.io.support.PathMatchingResourcePatternResolver,可以拿到符合指定路径模式的资源列表(相当于文件列表)。

1
2
3
4
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// `*`表示仅获取直接子目录/文件,`**`表示获取所有后代目录/文件
// 在文件系统上运行时,每个资源的类型为`FileSystemResource`;在jar包中运行时,每个资源的类型为`ClassPathResource`
Resource[] resources = resolver.getResources("classpath:static/the_dir/*");

每个资源可以通过resource.isReadable()(是否可读)判断是普通文件还是普通,true表示是普通文件,false表示是目录。

读取文件内容

Spring Boot 3可直接通过resource.getContentAsString(StandardCharsets.UTF-8)读取文件内容,Spring Boot 2则需要通过resource.getInputStream()获取输入流来读取文件内容。

计算访问路径

资源可以通过resource.isFile()判断是否在文件系统中,进而通过不同的方式拿到路径。

每个文件的路径去掉static目录的路径,就是该文件相对于static目录的访问路径。

1
2
3
4
5
6
7
8
9
Resource staticDirectoryResource = resolver.getResource("classpath:static");
String staticDirectoryPath = staticDirectoryResource.isFile()
? staticDirectoryResource.getFile().getAbsolutePath() // 文件系统的绝对路径,形如"C:\..."
: ((ClassPathResource) staticDirectoryResource).getPath(); // 相对于类目录的路径,形如"static/..."
...
String filePath = fileResource.isFile()
? fileResource.getFile().getAbsolutePath()
: ((ClassPathResource) fileResource).getPath();
String accessPath = filePath.replace(staticDirectoryPath, "").replace(File.separator, "/");