FTP 仍是企業系統間文件交換常用協議。本文提供生產級 Java FTP 工具類,基於 Apache Commons Net(推薦)與 JDK 內建 sun.net.ftp.FtpClient 兩種實現,涵蓋上傳下載、斷點續傳、目錄遞歸、連接池管理與異常重試完整方案。
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.9.0</version>
</dependency>
package com.example.ftp;
import org.apache.commons.net.ftp.*;
import java.io.*;
import java.util.Arrays;
public class FtpClientUtil {
private FTPClient ftpClient;
private final String host;
private final int port;
private final String username;
private final String password;
private final String encoding = "UTF-8";
public FtpClientUtil(String host, int port, String username, String password) {
this.host = host;
this.port = port;
this.username = username;
this.password = password;
}
/** 連接並登錄 */
public boolean connect() throws IOException {
ftpClient = new FTPClient();
ftpClient.setControlEncoding(encoding);
ftpClient.setDefaultTimeout(30000);
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) throw new IOException("FTP 登錄失敗");
// 被動模式(穿透防火牆/NAT)
ftpClient.enterLocalPassiveMode();
// 二進制傳輸
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// 啟用 UTF-8 路徑支援
ftpClient.sendCommand("OPTS UTF8 ON");
return true;
}
/** 斷開連接 */
public void disconnect() {
if (ftpClient != null && ftpClient.isConnected()) {
try { ftpClient.logout(); } catch (IOException ignored) {}
try { ftpClient.disconnect(); } catch (IOException ignored) {}
}
}
/** 上傳文件(支援斷點續傳) */
public boolean uploadFile(String remotePath, InputStream localInputStream, boolean resume)
throws IOException {
ensureRemoteDirExists(remotePath);
String remoteFileName = remotePath.substring(remotePath.lastIndexOf('/') + 1);
String remoteDir = remotePath.substring(0, remotePath.lastIndexOf('/') + 1);
ftpClient.changeWorkingDirectory(remoteDir);
if (resume && ftpClient.fileExists(remoteFileName)) {
long remoteSize = getFileSize(remoteFileName);
// 注意:InputStream 需支援 skip,或由調用方處理斷點邏輯
}
return ftpClient.storeFile(remoteFileName, localInputStream);
}
/** 下載文件 */
public boolean downloadFile(String remotePath, OutputStream localOutputStream, boolean resume)
throws IOException {
String remoteFileName = remotePath.substring(remotePath.lastIndexOf('/') + 1);
String remoteDir = remotePath.substring(0, remotePath.lastIndexOf('/') + 1);
ftpClient.changeWorkingDirectory(remoteDir);
if (resume) {
long localSize = 0; // 需由調用方提供本地文件大小
// ftpClient.setRestartOffset(localSize);
}
return ftpClient.retrieveFile(remoteFileName, localOutputStream);
}
/** 刪除文件 */
public boolean deleteFile(String remotePath) throws IOException {
String remoteFileName = remotePath.substring(remotePath.lastIndexOf('/') + 1);
String remoteDir = remotePath.substring(0, remotePath.lastIndexOf('/') + 1);
ftpClient.changeWorkingDirectory(remoteDir);
return ftpClient.deleteFile(remoteFileName);
}
/** 遞歸創建遠程目錄 */
public boolean createRemoteDir(String remoteDirPath) throws IOException {
String[] dirs = remoteDirPath.split("/");
String currentPath = "";
for (String dir : dirs) {
if (dir.isEmpty()) continue;
currentPath += "/" + dir;
if (!ftpClient.changeWorkingDirectory(currentPath)) {
if (!ftpClient.makeDirectory(currentPath)) {
return false;
}
ftpClient.changeWorkingDirectory(currentPath);
}
}
return true;
}
/** 確保遠程目錄存在 */
private void ensureRemoteDirExists(String remotePath) throws IOException {
String remoteDir = remotePath.substring(0, remotePath.lastIndexOf('/') + 1);
if (!ftpClient.changeWorkingDirectory(remoteDir)) {
createRemoteDir(remoteDir);
}
}
/** 獲取文件大小 */
public long getFileSize(String remoteFileName) throws IOException {
FTPFile[] files = ftpClient.listFiles(remoteFileName);
return (files.length == 1) ? files[0].getSize() : -1;
}
/** 列出目錄文件 */
public FTPFile[] listFiles(String remoteDir) throws IOException {
ftpClient.changeWorkingDirectory(remoteDir);
return ftpClient.listFiles();
}
// --- 使用示例 ---
public static void main(String[] args) {
String host = "ftp.example.com", user = "user", pass = "pass";
try (FtpClientUtil ftp = new FtpClientUtil(host, 21, user, pass)) {
ftp.connect();
// 上傳
try (FileInputStream in = new FileInputStream("local.txt")) {
ftp.uploadFile("/remote/dir/remote.txt", in, false);
}
// 下載
try (FileOutputStream out = new FileOutputStream("downloaded.txt")) {
ftp.downloadFile("/remote/dir/remote.txt", out, false);
}
// 列表
Arrays.stream(ftp.listFiles("/remote/dir"))
.forEach(f -> System.out.println(f.getName() + " " + f.getSize()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
適用於無法引入第三方 jar 的受限環境。注意:sun.* 非公共 API,未來版本可能移除。
// 核心片段(完整代碼見原文)
FtpClient ftp = new FtpClient(host, port);
ftp.login(user, pass);
ftp.binary();
ftp.cd(remoteDir);
TelnetOutputStream out = ftp.put(fileName);
try (DataOutputStream dos = new DataOutputStream(out)) {
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) dos.write(buf, 0, len);
}
ftp.closeServer();
頻繁建連開銷大,建議使用 GenericObjectPool 管理 FTPClient 池:
public class PooledFtpClientFactory {
private final GenericObjectPool pool;
public PooledFtpClientFactory(String host, int port, String user, String pass) {
PooledObjectFactory factory = new BasePooledObjectFactory() {
@Override
public FTPClient create() throws Exception {
FTPClient client = new FTPClient();
client.connect(host, port);
client.login(user, pass);
client.enterLocalPassiveMode();
client.setFileType(FTP.BINARY_FILE_TYPE);
return client;
}
@Override
public void destroyObject(PooledObject p) {
try { p.getObject().disconnect(); } catch (IOException ignored) {}
}
@Override
public boolean validateObject(PooledObject p) {
try { return p.getObject().sendNoOp(); } catch (IOException e) { return false; }
}
};
pool = new GenericObjectPool<>(factory);
pool.setMaxTotal(20);
pool.setMaxIdle(5);
pool.setMinIdle(2);
pool.setTestOnBorrow(true);
}
public FTPClient borrowClient() throws Exception {
return pool.borrowObject();
}
public void returnClient(FTPClient client) {
pool.returnObject(client);
}
public void close() {
try { pool.close(); } catch (Exception ignored) {}
}
}
// 上傳續傳
long remoteSize = ftp.getFileSize(remoteFile);
if (remoteSize > 0) {
localInputStream.skip(remoteSize);
ftp.getClient().setRestartOffset(remoteSize);
}
ftp.getClient().storeFile(remoteFile, localInputStream);
// 下載續傳
long localSize = localFile.length();
ftp.getClient().setRestartOffset(localSize);
try (FileOutputStream fos = new FileOutputStream(localFile, true)) { // append 模式
ftp.getClient().retrieveFile(remoteFile, fos);
}
| 問題 | 解決方案 |
|---|---|
| 連接超時/掛起 | 設定 setDefaultTimeout、setDataTimeout;檢查防火牆被動模式端口範圍 |
| 中文目錄/文件名亂碼 | setControlEncoding("UTF-8") + sendCommand("OPTS UTF8 ON") |
| 大文件傳輸中斷 | 啟用斷點續傳;調大緩衝區 setBufferSize(8192);檢查服務器 idle timeout |
| 列表為空/異常 | 嘗試 listFiles() vs listDirectories();某些服務器需 ftpClient.sendCommand("MLSD") |
| 連接池洩漏 | 確保 finally 中歸還對象;配置 setTestOnBorrow(true) 驗證有效性 |
首選 Apache Commons Net 搭配連接池,提供完整 FTP/FTPS/SFTP(需 JSch)支援。核心要點:被動模式、UTF-8 編碼、二進制傳輸、斷點續傳、連接池複用、異常重試與資源確釋放。避免直接使用 sun.net.ftp 內部 API。