携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
起因
这个问题是在一个项目中的有一个压缩文件的功能,其服务逻辑比较复杂,如下:
- 生成压缩文件的路径。
- 调用ZipFile进行压缩。
- 确保文件夹是否存在,如果不存在就新建。
- 看文件是否存在,如果存在就先删除。
- 新建ZipFile对象。
- 新建ZipParameters对象。
- 为zipFile添加文件。
- 关闭zipFile文件。
- 为文件生成hash值。
- 利用hash值生成新的文件名并重命名。
主要是第4步这里一直不成功,即重命名返回失败。下面就是排查过程
排除过程
文件锁定
对于文件的操作失败,首先应当想到的是文件锁定。然后利用工具查看,确实如此。文件被jdk锁定了。
流的排查
一般文件锁定都是文件对应的流没有关闭导致的,因为文件流需要从文件中读取数据,所以都会将文件锁定。
由于这里是新建的压缩文件,所以我首先看是否在新建文件的时候是否锁定了文件。
新建文件部分
值得注意的是,使用new File()时确实会锁定文件,因为这里是对文件的写操作。所以我单步debug。
但是发现并不是这里的问题,一般只要new File()成功,那么就会解除锁定了,因为文件生成了。
压缩库文件
压缩工具类如下:
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方法。我之前是没有加上的。所以在此加上该方法:
zipFile.close();
应该就可行了。
但是结果还是被占用。
我点进了close方法内部,发现其实就是关闭所有的流,包括文件整体的,以及压缩文件内部的子文件:
public void close() throws IOException {
Iterator var1 = this.openInputStreams.iterator();
while(var1.hasNext()) {
InputStream inputStream = (InputStream)var1.next();
inputStream.close();
}
this.openInputStreams.clear();
}
那么理论上调用这个方法只要不抛出错误,那么流应该就被关闭完了。那就排除了库文件的占用问题。
绝对不要使用匿名流
经过上面的过程,我就已经感觉可能不是压缩这一部分的问题了。然后就对着服务代码重新看了一会:
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);
然后我就突然发现了问题所在(可能认真的话,一眼就看出来了):
String fileHash = FileUtils.generateAvailableHash(new FileInputStream(path));
没错,这里使用了匿名流!
这里就是问题所在,由于计算hash就必须要读文件,所以我这里就直接传入了一个流。但这里传入的是一个匿名流,所以最后没有关闭。
解决方法1-主动调用垃圾回收
在找到这个问题之前,我寻找了很多资料,然后发现了一个解决办法:主动调用垃圾回收。
System.gc();
这样能解决问题也很容易理解,上面看到是一个匿名流问题,所以如果主动调用垃圾回收,那么这个匿名流就会被回收。当然文件的占用就会被解除了。
但这是著表不治本的方法。相当于是先产生问题,然后去修补它,而没有去找到生成问题的原因。
解决方法1-主动调用垃圾回收
因为产生问题的关键在匿名流,那么找到这个问题后,解决就很简单了。那就是将匿名流改成具名流,然后在完成hash计算后,将其关闭。
FileInputStream fileInputStream = new FileInputStream(path);
String fileHash = FileUtils.generateAvailableHash(fileInputStream);
fileInputStream.close();
收获
从这次问题中,收获主要有两点:
- 永远不要使用匿名流,特别是文件流,因为不关的话,资源会一直被占用。
- 做事要仔细,其实如果早一点仔细看代码,可能就直接看出来问题所在了。