若感觉过程太冗长的话,可以直接看 “#解决方案” 快速找到答案。

需求

jar 包中复制文件或者文件夹到外部系统(这种需求可能不是特别多,但是还是有一些需要注意的点)。

问题

这里可能会面临两个问题,那就是开发的时候和打包成 jar 所使用的文件系统是不一样的。所以,在开发环境的时候,会呈现预期的效果,但一旦构建成 jar 包之后,就会出现文件找不到等恼人的问题。

分析

这里分析一下为什么会出现在 jar 包中找不到文件的问题。

对于 jar 中的文件复制来讲,大家第一时间肯定会想到用 getClass().getClassLoader().getResourceAsStream("classpath:file_name"),但这仅仅对于文件有效。

那么对于文件夹的复制呢?

我们假定需要复制的文件夹的路径为:classpath:static

  1. 首先,我们通过我们各自熟悉的方式获取到该文件夹URI

    • 开发环境中的结果如下:
    1
    file:/path/to/project/resources/static
    • jar 包中的结果如下:
    1
    jar:file:/path/to/jar_file/xxx.jar!/BOOT-INF/classes!/static
  2. 接着,我们需要读取文件信息。

这里用 Java 8Path 来获取文件信息。

1
public static Path get(URI uri);

开发环境中测试是没有任何问题的。但是只要生成为 jar 包测试后,就会出现文件找不到异常。

查看 Path#get 源码可以发现首先会判断 urischema,很显然,jar 包中的文件的 schema 都为 jar:file

也就意味着,两种环境中获取的 FileSystem 的方式是不相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static Path get(URI uri) {
String scheme = uri.getScheme();
if (scheme == null)
throw new IllegalArgumentException("Missing scheme");

// check for default provider to avoid loading of installed providers
if (scheme.equalsIgnoreCase("file"))
return FileSystems.getDefault().provider().getPath(uri);

// try to find provider
for (FileSystemProvider provider: FileSystemProvider.installedProviders()) {
if (provider.getScheme().equalsIgnoreCase(scheme)) {
return provider.getPath(uri);
}
}

throw new FileSystemNotFoundException("Provider \"" + scheme + "\" not installed");
}

通过调试可以发现,默认提供的仅仅只有两种 FileSystemProviderLinuxFileSystemProviderZipFileSystemProvider

对于 schemajar:fileURI 将会获取到 ZipFileSystemProvider

查看 ZipFileSystemProvider#getPath 源码将会发现,它会根据传入的 uri 去获取一个已经注册了的 FileSystem

由于我们此前没有关于此 uriFileSystem,故我们需要手动创建一个于此 uri 对应的 FielSystem,即可解决问题。

解决方案

手动创建一个 FileSystem 即可解决前面遇到的问题,且需要更换获取文件夹的主路径为 /BOOT-INF/classes/(这里很重要)。

1
2
FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
Path path = fileSystem.getPath("/BOOT-INF/classes/static");

但是对于 开发环境 可能不太友好。为了更好地兼容两者,需要提前判断 uri 的类型是否为 jar,代码如下:

1
2
3
4
5
6
7
Path path = null;
if(uri.getScheme().equalsIgnoreCase("jar")) {
FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
path = fileSystem.getPath("/BOOT-INF/classes/static");
} else {
path = Paths.get(uri);
}