在企業級應用開發中,透過 FTP 協定進行檔案傳輸是常見需求。本文提供一個基於 Apache Commons Net 庫的完整 Java FTP 下載實作範例,涵蓋連線設定、檔案列表獲取、二進位傳輸模式、資源釋放等關鍵環節,並說明生產環境部署時的注意事項。
以下範例展示了從 FTP 伺服器批次下載指定目錄下所有檔案的完整流程:
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class FtpDownloader {
private static final int DEFAULT_PORT = 21;
private static final int CONNECT_TIMEOUT = 120_000; // 120 秒
private static final int BUFFER_SIZE = 1024;
public void downloadFiles() {
FTPClient ftpClient = null;
InputStream inputStream = null;
List processedFiles = new ArrayList<>();
try {
// 1. 初始化 FTP 客戶端與連線參數
ftpClient = new FTPClient();
ftpClient.setDefaultPort(DEFAULT_PORT);
ftpClient.setConnectTimeout(CONNECT_TIMEOUT);
ftpClient.setDefaultTimeout(CONNECT_TIMEOUT);
ftpClient.setDataTimeout(CONNECT_TIMEOUT);
ftpClient.setControlEncoding("UTF-8");
// 2. 設定伺服器類型為 UNIX,避免檔案列表解析錯誤
FTPClientConfig config = new FTPClientConfig(FTPClientConfig.SYST_UNIX);
config.setServerLanguageCode("en");
config.setDefaultDateFormatStr("MMM dd HH:mm");
ftpClient.configure(config);
// 3. 連線並登入 (配置資訊建議從外部配置檔讀取)
String host = SystemConfig.getKey("scpFtpIp");
String username = SystemConfig.getKey("scpFtpUserName").trim();
String password = SystemConfig.getKey("scpFtpPassword").trim();
String remoteDir = SystemConfig.getKey("ftpHangupDir").trim();
ftpClient.connect(host);
boolean loginSuccess = ftpClient.login(username, password);
if (!loginSuccess) {
log.error("FTP 登入失敗,請檢查帳號密碼或伺服器狀態");
return;
}
// 4. 切換工作目錄
if (!ftpClient.changeWorkingDirectory(remoteDir)) {
log.error("無法切換至目標目錄: " + remoteDir);
return;
}
// 5. 獲取檔案列表並逐個下載
FTPFile[] files = ftpClient.listFiles();
if (files == null || files.length == 0) {
log.info("目標目錄無檔案,等待 5 秒後重試...");
Thread.sleep(5_000);
return;
}
log.info("偵測到 {} 個待下載檔案", files.length);
for (FTPFile file : files) {
String fileName = file.getName();
// 設定緩衝區與傳輸模式 (二進位模式適用於所有類型檔案)
ftpClient.setBufferSize(BUFFER_SIZE);
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// 取得輸入流並處理
inputStream = ftpClient.retrieveFileStream(fileName);
if (inputStream != null) {
processFileInput(ftpClient, inputStream, processedFiles, fileName);
} else {
log.warn("無法獲取檔案輸入流: {}", fileName);
}
}
} catch (Exception e) {
log.error("FTP 下載過程發生異常", e);
} finally {
// 6. 關鍵:確保資源正確釋放
closeQuietly(inputStream);
if (ftpClient != null) {
try {
// 下載成功後可選擇刪除遠端檔案
deleteRemoteFiles(ftpClient, processedFiles);
ftpClient.logout();
} catch (Exception e) {
log.error("FTP 登出或清理時發生錯誤", e);
} finally {
disconnectQuietly(ftpClient);
}
}
}
}
/**
* 處理檔案輸入流的業務邏輯 (如解析內容、存入資料庫等)
*/
private void processFileInput(FTPClient ftpClient, InputStream input,
List processedList, String fileName) {
try {
// 在此處實作具體業務邏輯,例如 callingService(input);
callingService(input);
// 確認命令完成並記錄已處理檔案
input.close();
ftpClient.completePendingCommand();
processedList.add(fileName);
} catch (Exception e) {
log.error("處理檔案 {} 時發生錯誤", fileName, e);
}
}
// 輔助方法:安全關閉流、刪除遠端檔案、斷開連線等...
}
務必呼叫 `ftpClient.setFileType(FTP.BINARY_FILE_TYPE)`。ASCII 模式會嘗試轉換換行符,導致圖片、壓縮檔、執行檔損壞。二進位模式適用於所有檔案類型,是安全的預設選擇。
FTP 操作涉及網路連線與 I/O 流,必須在 finally 區塊中釋放資源。範例中包含:關閉輸入流、執行 `completePendingCommand()` 確認傳輸完成、登出、斷開連線。忽略此步驟會導致伺服器連線數耗盡或本地檔案句柄洩漏。
範例中將成功下載的檔案名加入 `processedFiles` 列表,最後統一刪除。這比在迴圈中即時刪除更安全:若下游業務處理失敗,檔案保留在伺服器上,便於重試或人工介入。
| 現象 | 可能原因 | 解決方向 |
|---|---|---|
| 連線逾時 | 防火牆攔截、IP 錯誤、埠號錯誤 | telnet 測試連通性、檢查安全群組規則 |
| 登入失敗 | 帳號密碼錯誤、編碼問題、伺服器限制 IP | 驗證憑證、確認 `setControlEncoding`、檢查伺服器日誌 |
| 列表為空 / 解析錯誤 | 伺服器類型不匹配 (Windows vs UNIX) | 調整 `FTPClientConfig`,嘗試 `SYST_NT` 或自訂 `FTPFileEntryParser` |
| 下載檔案損壞 (大小為 0 或無法開啟) | 未設定二進位模式、流未正確關閉 | 強制 `BINARY_FILE_TYPE`、確認 `completePendingCommand()` 呼叫 |
Java FTP 檔案下載看似簡單,但生產環境需考量網路異常、資源洩漏、編碼相容、併發控制等細節。掌握上述核心 API 與最佳實踐,能構建健壯、可維護的檔案整合模組。建議在專案中將此邏輯封裝為通用工具類或 Spring Bean,搭配重試機制 (如 `@Retryable`) 與熔斷器 (如 Resilience4j) 使用。