本文提供基於 Apache Commons Net 的 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.FTPReply;
import java.io.*;
public class FtpUtil {
private FTPClient ftpClient;
private String host;
private int port;
private String username;
private String password;
private int timeout = 5000;
public FtpUtil(String host, int port, String username, String password) {
this.host = host;
this.port = port;
this.username = username;
this.password = password;
this.ftpClient = new FTPClient();
this.ftpClient.setControlEncoding("UTF-8");
this.ftpClient.setDefaultTimeout(timeout);
this.ftpClient.setConnectTimeout(timeout);
}
/**
* 連接並登錄 FTP 服務器
*/
public boolean connect() throws IOException {
ftpClient.connect(host, port);
int reply = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
disconnect();
throw new IOException("FTP 服務器拒絕連接: " + reply);
}
boolean login = ftpClient.login(username, password);
if (!login) {
disconnect();
throw new IOException("FTP 登錄失敗: 用戶名或密碼錯誤");
}
// 設置二進制傳輸模式
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
// 被動模式 (穿透防火牆/NAT)
ftpClient.enterLocalPassiveMode();
return true;
}
/**
* 斷開連接
*/
public void disconnect() {
if (ftpClient != null && ftpClient.isConnected()) {
try {
ftpClient.logout();
} catch (IOException ignored) {}
try {
ftpClient.disconnect();
} catch (IOException ignored) {}
}
}
/**
* 上傳文件 (InputStream 方式,支援重命名)
*/
public boolean upload(String remoteDir, String remoteFileName, InputStream inputStream) {
boolean success = false;
try {
ensureConnected();
createDirectories(remoteDir);
ftpClient.changeWorkingDirectory(remoteDir);
success = ftpClient.storeFile(remoteFileName, inputStream);
} catch (IOException e) {
throw new RuntimeException("FTP 上傳失敗", e);
} finally {
closeQuietly(inputStream);
}
return success;
}
/**
* 上傳本地文件 (支援重命名)
*/
public boolean uploadFile(String remoteDir, String remoteFileName, String localFilePath) {
try (FileInputStream fis = new FileInputStream(localFilePath)) {
return upload(remoteDir, remoteFileName, fis);
} catch (IOException e) {
throw new RuntimeException("讀取本地文件失敗", e);
}
}
/**
* 上傳本地文件 (保持原文件名)
*/
public boolean uploadFile(String remoteDir, String localFilePath) {
File file = new File(localFilePath);
return uploadFile(remoteDir, file.getName(), localFilePath);
}
/**
* 下載文件
*/
public boolean download(String remoteDir, String remoteFileName, String localDir) {
boolean success = false;
try {
ensureConnected();
ftpClient.changeWorkingDirectory(remoteDir);
FTPFile[] files = ftpClient.listFiles(remoteFileName);
if (files.length == 0) {
throw new FileNotFoundException("FTP 文件不存在: " + remoteFileName);
}
File localFile = new File(localDir, remoteFileName);
localFile.getParentFile().mkdirs();
try (FileOutputStream fos = new FileOutputStream(localFile)) {
success = ftpClient.retrieveFile(remoteFileName, fos);
}
} catch (IOException e) {
throw new RuntimeException("FTP 下載失敗", e);
}
return success;
}
/**
* 刪除文件
*/
public boolean deleteFile(String remoteDir, String remoteFileName) {
try {
ensureConnected();
ftpClient.changeWorkingDirectory(remoteDir);
return ftpClient.deleteFile(remoteFileName);
} catch (IOException e) {
throw new RuntimeException("FTP 刪除失敗", e);
}
}
/**
* 列出目錄文件
*/
public FTPFile[] listFiles(String remoteDir) {
try {
ensureConnected();
ftpClient.changeWorkingDirectory(remoteDir);
return ftpClient.listFiles();
} catch (IOException e) {
throw new RuntimeException("FTP 列表失敗", e);
}
}
/**
* 創建多級目錄 (遞歸)
*/
private void createDirectories(String remoteDir) throws IOException {
if (remoteDir == null || remoteDir.isEmpty() || "/".equals(remoteDir)) return;
String[] dirs = remoteDir.split("/");
StringBuilder path = new StringBuilder();
for (String dir : dirs) {
if (dir.isEmpty()) continue;
path.append("/").append(dir);
if (!ftpClient.changeWorkingDirectory(path.toString())) {
boolean created = ftpClient.makeDirectory(path.toString());
if (!created) {
throw new IOException("創建目錄失敗: " + path);
}
ftpClient.changeWorkingDirectory(path.toString());
}
}
// 返回根目錄
ftpClient.changeWorkingDirectory("/");
}
private void ensureConnected() throws IOException {
if (!ftpClient.isConnected()) {
connect();
}
}
private void closeQuietly(Closeable closeable) {
if (closeable != null) {
try { closeable.close(); } catch (IOException ignored) {}
}
}
// 測試主方法
public static void main(String[] args) {
String host = "127.0.0.1";
int port = 21;
String user = "testuser";
String pass = "testpass";
String remoteDir = "/upload/test";
String localFile = "D:/bigfile.zip";
FtpUtil ftp = new FtpUtil(host, port, user, pass);
try {
// 上傳
boolean up = ftp.uploadFile(remoteDir, "renamed.zip", localFile);
System.out.println("上傳: " + (up ? "成功" : "失敗"));
// 下載
boolean down = ftp.download(remoteDir, "renamed.zip", "D:/download");
System.out.println("下載: " + (down ? "成功" : "失敗"));
// 列表
for (FTPFile f : ftp.listFiles(remoteDir)) {
System.out.println(f.getName() + " " + f.getSize() + " " + f.getTimestamp().getTime());
}
} finally {
ftp.disconnect();
}
}
}
| 設置 | 代碼 | 作用 |
|---|---|---|
| 控制編碼 | setControlEncoding("UTF-8") | 解決中文目錄/文件名亂碼 |
| 傳輸模式 | setFileType(BINARY_FILE_TYPE) | 二進制模式,避免文本模式損壞圖片/壓縮包 |
| 被動模式 | enterLocalPassiveMode() | 客戶端主動發起數據連接,穿透防火牆/NAT |
| 超時設置 | setConnectTimeout/setDefaultTimeout | 避免長時間卡死 |
| 問題 | 原因 | 解決 |
|---|---|---|
| 連接超時 / Connection refused | IP/端口錯誤、防火牆攔截、FTP 服務未啟動 | telnet 測試端口、檢查安全組/防火牆、確認服務狀態 |
| 530 Login incorrect | 用戶名/密碼錯誤、用戶被鎖定、IP 白名單限制 | 核對憑證、檢查 FTP 服務器日誌 |
| 上傳/下載卡住 | 主動模式下數據連接被防火牆阻擋 | 必須啟用被動模式 enterLocalPassiveMode() |
| 中文文件名亂碼 | 編碼不一致 | setControlEncoding("UTF-8"),服務端也需支援 UTF-8 |
| 550 Permission denied | 目錄寫入權限不足、磁盤配額滿 | 檢查 FTP 用戶權限、磁盤空間 |
| 大文件傳輸失敗 | 超時、連接中斷、服務器限制單文件大小 | 增加超時、啟用斷點續傳 (REST/APPE 指令) |
// 續傳上傳 (APPE 指令)
public boolean appendUpload(String remoteDir, String remoteFileName,
String localFilePath, long resumeOffset) {
try (RandomAccessFile raf = new RandomAccessFile(localFilePath, "r")) {
raf.seek(resumeOffset);
ftpClient.changeWorkingDirectory(remoteDir);
// Commons Net 3.6+ 支援 appendFile
return ftpClient.appendFile(remoteFileName, raf);
} catch (IOException e) {
throw new RuntimeException("續傳上傳失敗", e);
}
}
// 續傳下載 (REST 指令)
public boolean resumeDownload(String remoteDir, String remoteFileName,
String localFilePath, long resumeOffset) {
try (RandomAccessFile raf = new RandomAccessFile(localFilePath, "rw")) {
raf.seek(resumeOffset);
ftpClient.changeWorkingDirectory(remoteDir);
ftpClient.setRestartOffset(resumeOffset);
return ftpClient.retrieveFile(remoteFileName, new FileOutputStream(raf.getFD()));
} catch (IOException e) {
throw new RuntimeException("續傳下載失敗", e);
}
}
FTPClientPool (Commons Pool) 管理連接複用FTPClient ftps = new FTPSClient("TLS") 加密傳輸Apache Commons Net 提供標準、穩定的 FTP 客戶端實現。關鍵最佳實踐:UTF-8 編碼、二進制模式、被動模式、超時設置、資源釋放。工具類封裝連接管理、目錄遞歸創建、異常轉換,可直接投入生產使用。大文件、斷點續傳、加密傳輸等進階需求可在基礎上擴展。