起因
这个问题是在一个项目中的有一个压缩文件的功能,其服务逻辑比较复杂,如下:
- 生成压缩文件的路径。
- 调用ZipFile进行压缩。
- 确保文件夹是否存在,如果不存在就新建。
- 看文件是否存在,如果存在就先删除。
- 新建ZipFile对象。
- 新建ZipParameters对象。
- 为zipFile添加文件。
- 关闭zipFile文件。
- 为文件生成hash值。
- 利用hash值生成新的文件名并重命名。
主要是第4步这里一直不成功,即重命名返回失败。下面就是排查过程
排除过程
文件锁定
对于文件的操作失败,首先应当想到的是文件锁定。然后利用工具查看,确实如此。文件被jdk锁定了。
流的排查
一般文件锁定都是文件对应的流没有关闭导致的,因为文件流需要从文件中读取数据,所以都会将文件锁定。
由于这里是新建的压缩文件,所以我首先看是否在新建文件的时候是否锁定了文件。
新建文件部分
值得注意的是,使用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方法。我之前是没有加上的。所以在此加上该方法:
应该就可行了。
但是结果还是被占用。
我点进了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-主动调用垃圾回收
因为产生问题的关键在匿名流,那么找到这个问题后,解决就很简单了。那就是将匿名流改成具名流,然后在完成hash计算后,将其关闭。
1 2 3
| FileInputStream fileInputStream = new FileInputStream(path); String fileHash = FileUtils.generateAvailableHash(fileInputStream); fileInputStream.close();
|
收获
从这次问题中,收获主要有两点:
- 永远不要使用匿名流,特别是文件流,因为不关的话,资源会一直被占用。
- 做事要仔细,其实如果早一点仔细看代码,可能就直接看出来问题所在了。