记录一次Java文件占用的故障排除

起因

这个问题是在一个项目中的有一个压缩文件的功能,其服务逻辑比较复杂,如下:

  1. 生成压缩文件的路径。
  2. 调用ZipFile进行压缩。
    1. 确保文件夹是否存在,如果不存在就新建。
    2. 看文件是否存在,如果存在就先删除。
    3. 新建ZipFile对象。
    4. 新建ZipParameters对象。
    5. 为zipFile添加文件。
    6. 关闭zipFile文件。
  3. 为文件生成hash值。
  4. 利用hash值生成新的文件名并重命名。

主要是第4步这里一直不成功,即重命名返回失败。下面就是排查过程

排除过程

文件锁定

对于文件的操作失败,首先应当想到的是文件锁定。然后利用工具查看,确实如此。文件被jdk锁定了。

lock

流的排查

一般文件锁定都是文件对应的流没有关闭导致的,因为文件流需要从文件中读取数据,所以都会将文件锁定。

由于这里是新建的压缩文件,所以我首先看是否在新建文件的时候是否锁定了文件。

新建文件部分

值得注意的是,使用new File()时确实会锁定文件,因为这里是对文件的写操作。所以我单步debug。

但是发现并不是这里的问题,一般只要new File()成功,那么就会解除锁定了,因为文件生成了。

压缩库文件

压缩工具类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private static void zipFiles(List<XPanFile> files, String filePath, Integer compressLevel, Boolean encrypt, String password) throws IOException {

// 判断外围文件夹是否存在,如果不存在则创建
String filePathWithoutName = FileUtils.getFilePathWithoutName(filePath);
File pathFile = new File(filePathWithoutName);
if(!pathFile.exists()){
pathFile.mkdirs();
}

// 判断源文件存在,则删除
File tempFile = new File(filePath);
if(tempFile.exists()){
tempFile.delete();
}
ZipFile zipFile;
if(encrypt){
zipFile = new ZipFile(filePath, password.toCharArray());
}else{
zipFile = new ZipFile(filePath);
}

ZipParameters zipParameters = new ZipParameters();
zipParameters.setEncryptFiles(encrypt);
zipParameters.setCompressionLevel(pairLevel(compressLevel));
zipParameters.setEncryptionMethod(EncryptionMethod.AES);
zipParameters.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_128);

for (XPanFile xpanFile : files) {
String curFileName = xpanFile.getFile_name();
String absolutePath = FileUtils.getAbsolutePath(xpanFile.getUrl(), true);
File curFile = new File(absolutePath);
zipFile.addFile(curFile, zipParameters);
zipFile.renameFile(curFileName, xpanFile.getUser_file_name());
}
}

由于Java自代的zip压缩库并没有加密功能,所以我采用了zip4j进行压缩。那么我就怀疑是不是库文件在写入后没有关闭流文件。

然后我发现了zip4j生成文件有一个close方法。我之前是没有加上的。所以在此加上该方法:

1
zipFile.close();

应该就可行了。

但是结果还是被占用。

我点进了close方法内部,发现其实就是关闭所有的流,包括文件整体的,以及压缩文件内部的子文件:

1
2
3
4
5
6
7
8
9
10
public void close() throws IOException {
Iterator var1 = this.openInputStreams.iterator();

while(var1.hasNext()) {
InputStream inputStream = (InputStream)var1.next();
inputStream.close();
}

this.openInputStreams.clear();
}

那么理论上调用这个方法只要不抛出错误,那么流应该就被关闭完了。那就排除了库文件的占用问题。

绝对不要使用匿名流

经过上面的过程,我就已经感觉可能不是压缩这一部分的问题了。然后就对着服务代码重新看了一会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String basePath = FileUtils.generateAvailableFilePath(fileName, true);
String path = basePath + fileName;
// 压缩文件
CompressUtils.compressFiles(files, path, compressType, compressLevel, encrypt, password);
LocalDateTime now = LocalDateTime.now();
String suffix = FileUtils.getSuffix(fileName);
String fileHash = FileUtils.generateAvailableHash(new FileInputStream(path));
Integer integer = fileMapper.isFileExistsByHash(fileHash);
FileUtils.renameFile(path, fileHash + "." +suffix);
if(integer == 0){
XPanFile compressedFile = new XPanFile();
compressedFile.setFile_name(fileHash + "." + suffix);
compressedFile.setHash(fileHash);
compressedFile.setPid(pid);
compressedFile.setGmt_update(now);
compressedFile.setGmt_create(now);
String realRelativePath = FileUtils.getRelativePath(FileUtils.getFilePathWithoutName(path) + fileHash + "." +suffix, true);
compressedFile.setUrl(realRelativePath);
compressedFile.setType(FileUtils.getType(suffix));
fileMapper.createFile(compressedFile);
}
// 链接用户文件表
fileMapper.createUserFile(fileName, userId, fileHash, pid, now, now);

然后我就突然发现了问题所在(可能认真的话,一眼就看出来了):

1
String fileHash = FileUtils.generateAvailableHash(new FileInputStream(path));

没错,这里使用了匿名流!

这里就是问题所在,由于计算hash就必须要读文件,所以我这里就直接传入了一个流。但这里传入的是一个匿名流,所以最后没有关闭。

解决方法1-主动调用垃圾回收

在找到这个问题之前,我寻找了很多资料,然后发现了一个解决办法:主动调用垃圾回收。

1
System.gc();

这样能解决问题也很容易理解,上面看到是一个匿名流问题,所以如果主动调用垃圾回收,那么这个匿名流就会被回收。当然文件的占用就会被解除了。

但这是著表不治本的方法。相当于是先产生问题,然后去修补它,而没有去找到生成问题的原因。

解决方法1-主动调用垃圾回收

因为产生问题的关键在匿名流,那么找到这个问题后,解决就很简单了。那就是将匿名流改成具名流,然后在完成hash计算后,将其关闭。

1
2
3
FileInputStream fileInputStream = new FileInputStream(path);
String fileHash = FileUtils.generateAvailableHash(fileInputStream);
fileInputStream.close();

收获

从这次问题中,收获主要有两点:

  • 永远不要使用匿名流,特别是文件流,因为不关的话,资源会一直被占用。
  • 做事要仔细,其实如果早一点仔细看代码,可能就直接看出来问题所在了。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :