From 54d8356830dd2b216d052662b93b6abca424550b Mon Sep 17 00:00:00 2001 From: zhaoxuyang03 Date: Sat, 29 Jan 2022 21:41:55 +0800 Subject: [PATCH] =?UTF-8?q?[5.7.21]=20[hutool-core]=20=E6=96=B0=E5=A2=9Eco?= =?UTF-8?q?pySafely=E6=96=B9=E6=B3=95=E4=B8=8EmkdirsSafely=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/cn/hutool/core/io/FileUtil.java | 37 ++++++++++++++++++ .../main/java/cn/hutool/core/io/NioUtil.java | 39 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java index 3d4b80858..6fc1f748f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java @@ -13,6 +13,7 @@ import cn.hutool.core.io.file.Tailer; import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.unit.DataSizeUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.CharsetUtil; @@ -537,6 +538,8 @@ public class FileUtil extends PathUtil { } if (file.isDirectory()) { + // TODO: 是否需要统计目录本身的大小呢? + // size += file.length(); long size = 0L; File[] subFiles = file.listFiles(); if (ArrayUtil.isEmpty(subFiles)) { @@ -607,6 +610,7 @@ public class FileUtil extends PathUtil { return null; } if (false == file.exists()) { + // TODO: {@see mkdirsSafely} 确保并发环境下的安全创建 mkParentDirs(file); try { //noinspection ResultOfMethodCallIgnored @@ -827,6 +831,39 @@ public class FileUtil extends PathUtil { return dir; } + /** + * 安全地级联创建目录 (确保并发环境下能创建成功) + * + *
+	 *     并发环境下,假设 test 目录不存在,如果线程A mkdirs "test/A" 目录,线程B mkdirs "test/B"目录,
+	 *     其中一个线程可能会失败,进而导致以下代码抛出 FileNotFoundException 异常
+	 *
+	 *     file.getParentFile().mkdirs(); // 父目录正在被另一个线程创建中,返回 false
+	 *     file.createNewFile(); // 抛出 IO 异常,因为该线程无法感知到父目录已被创建
+	 * 
+ * + * @param dir 待创建的目录 + * @return true表示创建成功,false表示创建失败 + * @since 2022-01-29 + * @author z8g + */ + public static boolean mkdirsSafely(File dir) { + if (dir == null) { + return false; + } + if (dir.isDirectory()) { + return true; + } + for (int i = 1; i <= 5; i++) { // 高并发场景下,可以看到 i 处于 1 ~ 3 之间 + dir.mkdirs(); // 如果文件已存在,也会返回 false,所以该值不能作为是否能创建的依据,因此不对其进行处理 + if (dir.exists()) { + return true; + } + ThreadUtil.sleep(1); + } + return dir.exists(); + } + /** * 创建临时文件
* 创建后的文件名为 prefix[Randon].tmp diff --git a/hutool-core/src/main/java/cn/hutool/core/io/NioUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/NioUtil.java index 2db343457..c247185b2 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/NioUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/NioUtil.java @@ -87,12 +87,49 @@ public class NioUtil { Assert.notNull(outChannel, "Out channel is null!"); try { - return inChannel.transferTo(0, inChannel.size(), outChannel); + return copySafely(inChannel, outChannel); } catch (IOException e) { throw new IORuntimeException(e); } } + /** + * 文件拷贝实现 + * + *
+	 * FileChannel#transferTo 或 FileChannel#transferFrom 的实现是平台相关的,需要确保低版本平台的兼容性
+	 * 例如 android 7以下平台在使用 ZipInputStream 解压文件的过程中,
+	 * 通过 FileChannel#transferFrom 传输到文件时,其返回值可能小于 totalBytes,不处理将导致文件内容缺失
+	 *
+	 * // 错误写法,dstChannel.transferFrom 返回值小于 zipEntry.getSize(),导致解压后文件内容缺失
+	 * try (InputStream srcStream = zipFile.getInputStream(zipEntry);
+	 * 		ReadableByteChannel srcChannel = Channels.newChannel(srcStream);
+	 * 		FileOutputStream fos = new FileOutputStream(saveFile);
+	 * 		FileChannel dstChannel = fos.getChannel()) {
+	 * 		dstChannel.transferFrom(srcChannel, 0, zipEntry.getSize());
+	 *  }
+	 * 
+ * + * @param inChannel 输入通道 + * @param outChannel 输出通道 + * @return 输入通道的字节数 + * @throws IOException 发生IO错误 + * @link http://androidxref.com/6.0.1_r10/xref/libcore/luni/src/main/java/java/nio/FileChannelImpl.java + * @link http://androidxref.com/7.0.0_r1/xref/libcore/ojluni/src/main/java/sun/nio/ch/FileChannelImpl.java + * @link http://androidxref.com/7.0.0_r1/xref/libcore/ojluni/src/main/native/FileChannelImpl.c + * @since 2022-01-29 + * @author z8g + */ + private static long copySafely(FileChannel inChannel, FileChannel outChannel) throws IOException { + long totalBytes = inChannel.size(); + for (long pos = 0, remaining = totalBytes; remaining > 0; ) { // 确保文件内容不会缺失 + long writeBytes = inChannel.transferTo(pos, remaining, outChannel); // 实际传输的字节数 + pos += writeBytes; + remaining -= writeBytes; + } + return totalBytes; + } + /** * 拷贝流,使用NIO,不会关闭channel *