採用 Apache Commons Net 库(org.apache.commons.net.ftp.FTPClient),為 Java FTP 操作的標準選擇,功能完整、線程安全、廣泛用於生產環境。
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.9.0</version> <!-- 或更新版本 -->
</dependency>
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPClientConfig;
import java.io.*;
public class FtpDownloader {
private static final int DEFAULT_PORT = 21;
private static final int TIMEOUT_MS = 120_000; // 2分鐘
private static final int BUFFER_SIZE = 1024;
public void downloadFiles(String host, String user, String password, String remoteDir, String localDir) {
FTPClient ftpClient = new FTPClient();
try {
// 1. 連接配置
ftpClient.setDefaultPort(DEFAULT_PORT);
ftpClient.setConnectTimeout(TIMEOUT_MS);
ftpClient.setDefaultTimeout(TIMEOUT_MS);
ftpClient.setDataTimeout(TIMEOUT_MS);
ftpClient.setControlEncoding("UTF-8"); // 關鍵:中文文件名支持
// 2. 連接並登錄
ftpClient.connect(host);
if (!ftpClient.login(user, password)) {
throw new IOException("FTP 登錄失敗: " + ftpClient.getReplyString());
}
// 3. 設置被動模式(穿透防火牆/NAT)
ftpClient.enterLocalPassiveMode();
// 4. 切換遠程目錄
if (!ftpClient.changeWorkingDirectory(remoteDir)) {
throw new IOException("無法切換到遠程目錄: " + remoteDir);
}
// 5. 列出文件並下載
FTPFile[] files = ftpClient.listFiles();
if (files == null || files.length == 0) {
System.out.println("遠程目錄無文件");
return;
}
for (FTPFile file : files) {
if (!file.isFile()) continue; // 跳過目錄
String remoteFileName = file.getName();
File localFile = new File(localDir, remoteFileName);
localFile.getParentFile().mkdirs();
// 6. 設置二進制傳輸類型
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.setBufferSize(BUFFER_SIZE);
try (InputStream in = ftpClient.retrieveFileStream(remoteFileName);
FileOutputStream out = new FileOutputStream(localFile)) {
if (in == null) {
System.err.println("無法獲取文件流: " + remoteFileName);
continue;
}
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
}
// 7. 完成待處理命令(必須調用,否則連接卡死)
if (!ftpClient.completePendingCommand()) {
System.err.println("文件傳輸確認失敗: " + remoteFileName);
} else {
System.out.println("下載成功: " + remoteFileName);
// 可選:下載後刪除遠程文件
// ftpClient.deleteFile(remoteFileName);
}
}
} catch (IOException e) {
throw new RuntimeException("FTP 下載異常", e);
} finally {
// 8. 安全斷開連接
try {
if (ftpClient.isConnected()) {
ftpClient.logout();
ftpClient.disconnect();
}
} catch (IOException ignored) {}
}
}
}
setControlEncoding("UTF-8") 解決中文文件名亂碼。enterLocalPassiveMode() 適應多數防火牆/NAT 環境。setFileType(BINARY_FILE_TYPE) 避免文本模式損壞二進制文件。// 設置斷點續傳起始位置
long localSize = localFile.length();
if (localSize > 0) {
ftpClient.setRestartOffset(localSize); // 告知服務器從該偏移量開始傳輸
try (FileOutputStream out = new FileOutputStream(localFile, true); // 追加模式
InputStream in = ftpClient.retrieveFileStream(remoteFileName)) {
// 寫入邏輯同前
}
}
| 現象 | 原因 | 解決 |
|---|---|---|
| 連接超時 | 防火牆阻擋、IP 錯誤、端口非 21 | telnet 測試連通性、確認端口、設置被動模式 |
| 中文文件名亂碼/404 | 編碼不一致 | setControlEncoding("UTF-8")、服務器也需支持 UTF-8 |
| 下載大文件卡死 | 未調用 completePendingCommand() | 每次 retrieveFileStream 後必須調用 |
| 空文件/損壞 | ASCII 模式傳輸二進制文件 | 強制 BINARY_FILE_TYPE |