文件複製/移動是基礎操作,但大目錄、符號鏈接、權限保留、原子性、進度回調等細節決定生產可用性。本文對比三種主流方案,提供生產級工具類。
| 方案 | API | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| Apache Commons IO | FileUtils.copyFile(), copyDirectory() | 一行代碼、自動處理目錄遞歸、保留時間戳 | 依賴第三方、大文件無進度、無原子性保證 | 中小文件、快速開發 |
| Java NIO.2 (Java 7+) | Files.copy(), move(), walkFileTree() | 零依賴、支援原子移動、屬性複製、流式處理 | API 較繁瑣、需自行實現遞歸/過濾 | 生產環境、大文件、跨文件系統 |
| 手工流 (InputStream/OutputStream) | 自定義緩衝區 | 完全控制、可插入加密/壓縮/進度 | 易出錯、重複造輪子 | 特殊需求 (加密複製、斷點續傳) |
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.util.EnumSet;
public class FileCopyMoveUtil {
/** 複製單文件:支援覆蓋、屬性複製、原子性 (同文件系統) */
public static void copyFile(Path src, Path dest, boolean overwrite) throws IOException {
CopyOption[] options = overwrite ?
new CopyOption[]{StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES} :
new CopyOption[]{StandardCopyOption.COPY_ATTRIBUTES};
Files.copy(src, dest, options);
}
/** 移動文件:同文件系統原子,跨文件系統複製+刪除 */
public static void moveFile(Path src, Path dest, boolean overwrite) throws IOException {
CopyOption[] options = overwrite ?
new CopyOption[]{StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE} :
new CopyOption[]{StandardCopyOption.ATOMIC_MOVE};
try {
Files.move(src, dest, options);
} catch (AtomicMoveNotSupportedException e) {
// 跨文件系統退回複製+刪除
copyFile(src, dest, overwrite);
Files.deleteIfExists(src);
}
}
/** 遞歸複製目錄:支援過濾、進度回調 */
public static void copyDirectory(Path srcDir, Path destDir,
DirectoryStream.Filter filter,
CopyProgressCallback callback) throws IOException {
Files.walkFileTree(srcDir, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
new SimpleFileVisitor() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetDir = destDir.resolve(srcDir.relativize(dir));
if (!Files.exists(targetDir)) Files.createDirectories(targetDir);
if (callback != null) callback.onDirectory(dir, targetDir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (filter == null || filter.accept(file)) {
Path targetFile = destDir.resolve(srcDir.relativize(file));
copyFile(file, targetFile, true);
if (callback != null) callback.onFile(file, targetFile, attrs.size());
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
if (callback != null) callback.onError(file, exc);
return FileVisitResult.CONTINUE; // 或 TERMINATE
}
});
}
@FunctionalInterface
public interface CopyProgressCallback {
void onDirectory(Path srcDir, Path destDir);
void onFile(Path srcFile, Path destFile, long size);
void onError(Path file, IOException e);
}
}
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
// 複製文件
FileUtils.copyFile(srcFile, destFile); // 自動建立父目錄
FileUtils.copyFile(srcFile, destFile, true); // 保留時間戳
// 複製目錄 (遞歸)
FileUtils.copyDirectory(srcDir, destDir);
FileUtils.copyDirectory(srcDir, destDir, file -> file.getName().endsWith(".jar")); // 過濾
// 移動 (實為複製+刪除)
FileUtils.moveFile(srcFile, destFile);
FileUtils.moveDirectory(srcDir, destDir);
public static void copyWithProgress(Path src, Path dest, ProgressListener listener) throws IOException {
long total = Files.size(src);
long copied = 0;
try (FileChannel in = FileChannel.open(src, StandardOpenOption.READ);
FileChannel out = FileChannel.open(dest, StandardOpenOption.CREATE, StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
long position = 0;
final int BUFFER_SIZE = 1024 * 1024; // 1MB 直接緩衝區
while (position < total) {
long transferred = in.transferTo(position, BUFFER_SIZE, out);
if (transferred == 0) break; // EOF
position += transferred;
copied += transferred;
if (listener != null) listener.onProgress(copied, total);
}
}
if (listener != null) listener.onComplete();
}
@FunctionalInterface
public interface ProgressListener {
void onProgress(long copied, long total);
default void onComplete() {}
}
Files.copy(src, dest, LinkOption.NOFOLLOW_LINKS):複製鏈接本身而非目標。StandardCopyOption.COPY_ATTRIBUTES:複製權限、時間戳、擁有者 (需權限)。@Test
void testCopyDirectoryWithFilter(@TempDir Path tempDir) throws IOException {
Path src = tempDir.resolve("src");
Path dest = tempDir.resolve("dest");
Files.createDirectories(src.resolve("sub"));
Files.writeString(src.resolve("a.txt"), "A");
Files.writeString(src.resolve("sub/b.jar"), "B");
Files.writeString(src.resolve("sub/c.log"), "C");
FileCopyMoveUtil.copyDirectory(src, dest,
p -> p.toString().endsWith(".jar"), // 只複製 jar
null);
assertTrue(Files.exists(dest.resolve("sub/b.jar")));
assertFalse(Files.exists(dest.resolve("a.txt")));
assertFalse(Files.exists(dest.resolve("sub/c.log")));
}
Files.move(ATOMIC_MOVE) 實現同盤原子重命名。FileChannel.transferTo() 零拷貝 (OS 支援時)。Files.move 依賴 fallback。walkFileTree 而非 listFiles(),避免深度遞歸棧溢出。