依賴坐標
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.8.0</version>
</dependency>
核心工具類
package com.yinhai.team.action;
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 FavFTPUtil {
/** 獲取連接並登錄 */
private static FTPClient connect(String hostname, int port, String username, String password) throws IOException {
FTPClient ftp = new FTPClient();
ftp.setControlEncoding("UTF-8"); // 解決中文目錄亂碼
ftp.connect(hostname, port);
ftp.login(username, password);
int reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
ftp.disconnect();
throw new IOException("FTP 登錄失敗,響應碼:" + reply);
}
ftp.enterLocalPassiveMode(); // 被動模式穿透防火牆
ftp.setFileType(FTPClient.BINARY_FILE_TYPE); // 二進制傳輸
return ftp;
}
/** 上傳文件 (InputStream) */
public static boolean uploadFile(String hostname, int port, String username, String password,
String remoteDir, String remoteFileName, InputStream input) {
FTPClient ftp = null;
try {
ftp = connect(hostname, port, username, password);
ftp.makeDirectory(remoteDir); // 不存在則創建,存在忽略
ftp.changeWorkingDirectory(remoteDir);
boolean success = ftp.storeFile(remoteFileName, input);
return success;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
closeQuietly(ftp);
closeQuietly(input);
}
}
/** 上傳本地文件 (支持重命名) */
public static boolean uploadLocalFile(String hostname, int port, String username, String password,
String remoteDir, String remoteFileName, String localFilePath) {
try (FileInputStream fis = new FileInputStream(localFilePath)) {
return uploadFile(hostname, port, username, password, remoteDir, remoteFileName, fis);
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
/** 下載文件 */
public static boolean downloadFile(String hostname, int port, String username, String password,
String remoteDir, String remoteFileName, String localDir) {
FTPClient ftp = null;
try {
ftp = connect(hostname, port, username, password);
ftp.changeWorkingDirectory(remoteDir);
FTPFile[] files = ftp.listFiles();
for (FTPFile file : files) {
String name = new String(file.getName().getBytes("ISO-8859-1"), "UTF-8"); // 編碼轉換
if (remoteFileName.equalsIgnoreCase(name)) {
File localFile = new File(localDir, name);
try (OutputStream os = new FileOutputStream(localFile)) {
return ftp.retrieveFile(file.getName(), os);
}
}
}
return false; // 文件不存在
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
closeQuietly(ftp);
}
}
/** 刪除文件 */
public static boolean deleteFile(String hostname, int port, String username, String password,
String remoteDir, String remoteFileName) {
FTPClient ftp = null;
try {
ftp = connect(hostname, port, username, password);
ftp.changeWorkingDirectory(remoteDir);
return ftp.deleteFile(remoteFileName);
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
closeQuietly(ftp);
}
}
/** 靜默關閉資源 */
private static void closeQuietly(FTPClient ftp) {
if (ftp != null && ftp.isConnected()) {
try { ftp.logout(); } catch (IOException ignored) {}
try { ftp.disconnect(); } catch (IOException ignored) {}
}
}
private static void closeQuietly(Closeable c) {
if (c != null) try { c.close(); } catch (IOException ignored) {}
}
}
進階:斷點續傳 (隨機訪問)
/** 斷點續傳下載 */
public static boolean downloadResume(String hostname, int port, String username, String password,
String remoteDir, String remoteFileName, String localFilePath) {
FTPClient ftp = null;
RandomAccessFile raf = null;
try {
ftp = connect(hostname, port, username, password);
ftp.changeWorkingDirectory(remoteDir);
long remoteSize = ftp.getFileSize(remoteFileName); // 需服務器支持 SIZE 指令
File localFile = new File(localFilePath);
long localSize = localFile.exists() ? localFile.length() : 0;
if (localSize >= remoteSize) return true; // 已完整
// 設置斷點
ftp.setRestartOffset(localSize);
raf = new RandomAccessFile(localFile, "rw");
raf.seek(localSize);
return ftp.retrieveFile(remoteFileName, new FileOutputStream(raf.getFD()));
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
closeQuietly(ftp);
if (raf != null) try { raf.close(); } catch (IOException ignored) {}
}
}
/** 斷點續傳上傳 (APPE 指令) */
public static boolean uploadResume(String hostname, int port, String username, String password,
String remoteDir, String remoteFileName, String localFilePath) {
FTPClient ftp = null;
FileInputStream fis = null;
try {
ftp = connect(hostname, port, username, password);
ftp.changeWorkingDirectory(remoteDir);
long remoteSize = ftp.getFileSize(remoteFileName);
File localFile = new File(localFilePath);
long localSize = localFile.length();
if (remoteSize >= localSize) return true;
fis = new FileInputStream(localFile);
fis.skip(remoteSize); // 跳過已傳部分
ftp.setRestartOffset(remoteSize);
return ftp.appendFile(remoteFileName, fis); // APPE 指令
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
closeQuietly(ftp);
closeQuietly(fis);
}
}
編碼問題終極解決
最佳實踐清單
- 所有流/連接在
finally 中靜默關閉。
- 上傳前檢查目標目錄存在性,
makeDirectory 可多級創建。
- 大文件必須使用
BINARY_FILE_TYPE,避免換行符轉換損壞數據。
- 生產環境建議封裝為 Spring Bean,注入連接池 (
PoolingFTPClientFactory) 復用連接。
- 異常處理:區分「文件不存在」、「權限不足」、「磁盤滿」、「網絡中斷」返回不同錯誤碼。