首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java FTP 文件上傳下載兩大方案深度對比:原生 API vs Apache Commons Net 完整實戰

     編輯:Java 相關知識

FTP 協議與 Java 實現選型

FTP (File Transfer Protocol) 仍是企業級文件交換主流協議。Java 實現兩大主流方案:

方案核心類依賴特點
JDK 原生sun.net.ftp.FtpClient無額外依賴輕量、但屬內部 API,不穩定、功能有限、中文處理麻煩
Apache Commons Netorg.apache.commons.net.ftp.FTPClientcommons-net.jar標準庫、功能完整、活躍維護、生產環境首選

強烈建議生產環境使用 Apache Commons Net。

方案一:Apache Commons Net 企業級完整實現

Maven 依賴


<dependency>
    <groupId>commons-net</groupId>
    <artifactId>commons-net</artifactId>
    <version>3.9.0</version> <!-- 最新版 -->
</dependency>

核心工具類


public class FtpUtils {
    private static final String DEFAULT_ENCODING = "UTF-8";
    
    // 連線配置
    public static FTPClient connect(String host, int port, String user, String pass, 
                                    String workingDir, boolean passiveMode) throws IOException {
        FTPClient ftp = new FTPClient();
        ftp.setControlEncoding(DEFAULT_ENCODING); // 關鍵:控制連線編碼
        ftp.connect(host, port);
        int reply = ftp.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            ftp.disconnect();
            throw new IOException("FTP 連線失敗,回應碼:" + reply);
        }
        if (!ftp.login(user, pass)) {
            ftp.disconnect();
            throw new IOException("FTP 登入失敗");
        }
        ftp.setFileType(FTP.BINARY_FILE_TYPE); // 二進制模式,支援所有文件
        if (passiveMode) ftp.enterLocalPassiveMode(); // 被動模式,穿透防火牆/NAT
        if (workingDir != null && !ftp.changeWorkingDirectory(workingDir)) {
            throw new IOException("無法切換到工作目錄:" + workingDir);
        }
        return ftp;
    }
    
    // 上傳文件
    public static boolean upload(FTPClient ftp, String remoteFileName, InputStream localInputStream) 
            throws IOException {
        // 關鍵:文件名編碼轉換,解決中文亂碼
        String encodedName = new String(remoteFileName.getBytes(DEFAULT_ENCODING), "ISO-8859-1");
        return ftp.storeFile(encodedName, localInputStream);
    }
    
    // 下載文件
    public static boolean download(FTPClient ftp, String remoteFileName, OutputStream localOutputStream) 
            throws IOException {
        String encodedName = new String(remoteFileName.getBytes(DEFAULT_ENCODING), "ISO-8859-1");
        return ftp.retrieveFile(encodedName, localOutputStream);
    }
    
    // 斷點續傳上傳
    public static boolean uploadResume(FTPClient ftp, String remoteFile, File localFile) 
            throws IOException {
        long remoteSize = getFileSize(ftp, remoteFile);
        long localSize = localFile.length();
        if (remoteSize >= localSize) return true; // 已完成
        
        try (RandomAccessFile raf = new RandomAccessFile(localFile, "r")) {
            raf.seek(remoteSize);
            ftp.setRestartOffset(remoteSize); // 設置斷點
            String encoded = new String(remoteFile.getBytes(DEFAULT_ENCODING), "ISO-8859-1");
            return ftp.storeFile(encoded, new BufferedInputStream(new FileInputStream(raf.getFD())));
        }
    }
    
    // 獲取遠程文件大小
    private static long getFileSize(FTPClient ftp, String remoteFile) throws IOException {
        FTPFile[] files = ftp.listFiles(new String(remoteFile.getBytes(DEFAULT_ENCODING), "ISO-8859-1"));
        return (files.length == 1) ? files[0].getSize() : 0;
    }
    
    // 安全關閉
    public static void closeQuietly(FTPClient ftp) {
        if (ftp != null && ftp.isConnected()) {
            try { ftp.logout(); } catch (IOException ignored) {}
            try { ftp.disconnect(); } catch (IOException ignored) {}
        }
    }
}

使用範例


// 上傳
try (FTPClient ftp = FtpUtils.connect("192.168.1.100", 21, "user", "pass", "/upload", true);
     FileInputStream in = new FileInputStream("/local/path/report_中文.pdf")) {
    boolean ok = FtpUtils.upload(ftp, "report_中文.pdf", in);
    System.out.println("上傳:" + (ok ? "成功" : "失敗"));
}

// 下載
try (FTPClient ftp = FtpUtils.connect("192.168.1.100", 21, "user", "pass", "/download", true);
     FileOutputStream out = new FileOutputStream("/local/path/report_中文.pdf")) {
    boolean ok = FtpUtils.download(ftp, "report_中文.pdf", out);
    System.out.println("下載:" + (ok ? "成功" : "失敗"));
}

// 斷點續傳大文件上傳
File bigFile = new File("/data/backup_10GB.tar.gz");
try (FTPClient ftp = FtpUtils.connect("192.168.1.100", 21, "user", "pass", "/backup", true)) {
    FtpUtils.uploadResume(ftp, "backup_10GB.tar.gz", bigFile);
}

方案二:JDK 原生 API(僅供理解,不建議生產使用)


// 僅展示核心差異,完整代碼見原文
FtpClient ftp = new FtpClient("host");
ftp.login("user", "pass");
ftp.binary(); // 設置二進制
TelnetOutputStream os = ftp.put("remote.txt"); // 上傳
TelnetInputStream is = ftp.get("remote.txt");  // 下載
// 缺點:無被動模式支援、無編碼控制、無斷點續傳、屬 sun.* 內部 API 可能隨版本變動

中文亂碼終極解決方案

根因:FTP 協議 (RFC 959) 規定路徑/文件名使用 ISO-8859-1 (Latin-1)。伺服器檔案系統實際編碼可能是 UTF-8、GBK、BIG5 等。

統一解法:

  1. 客戶端發送命令時:將文件名按伺服器預期編碼(通常 UTF-8)編碼為 bytes,再按 ISO-8859-1 解碼為 String 發送
  2. 客戶端接收列表時:按 ISO-8859-1 讀取 bytes,再按伺服器實際編碼解碼

// 發送端(上傳/下載/刪除/重命名等命令參數)
String encoded = new String(originalName.getBytes(serverCharset), "ISO-8859-1");

// 接收端(列表解析)
String decoded = new String(receivedBytes, "ISO-8859-1").getBytes(serverCharset);

FTPClient.setControlEncoding("UTF-8") 僅處理控制連線(命令/回應文本),不處理文件名編碼,需手動轉換。

生產環境檢查清單

項目建議配置
連線池使用 GenericObjectPool 複用連線,避免頻繁建立/關閉開銷
被動模式強制 enterLocalPassiveMode(),適應防火牆/NAT/雲環境
超時設置setConnectTimeout(10000)、setDefaultTimeout(30000)、setDataTimeout(60000)
二進制模式強制 setFileType(FTP.BINARY_FILE_TYPE),避免文本模式損壞二進制文件
大文件支援啟用 ftpClient.setRestartOffset() 斷點續傳,分片上傳合併
安全協議敏感數據必須用 FTPSClient (FTP over SSL/TLS) 或 SFTP (SSH)
異常重試網絡抖動時指數退避重試 3 次,記錄失敗入死信隊列人工介入
監控指標上傳/下載成功率、平均耗時、併發連線數、錯誤碼分布

Spring Boot 整合範例


@ConfigurationProperties(prefix = "ftp")
@Data
public class FtpProperties {
    private String host; private int port = 21;
    private String username; private String password;
    private String workingDir; private boolean passiveMode = true;
    private String encoding = "UTF-8";
}

@Component
public class FtpTemplate {
    @Autowired private FtpProperties props;
    private final ObjectPool pool;
    
    public FtpTemplate(FtpProperties props) {
        this.props = props;
        this.pool = new GenericObjectPool<>(new FtpClientFactory(props));
    }
    
    public boolean upload(String remoteName, InputStream in) {
        try (FTPClient ftp = pool.borrowObject()) {
            return FtpUtils.upload(ftp, remoteName, in);
        } catch (Exception e) { throw new FtpException(e); }
    }
    // download, list, delete, mkdir 等封裝...
}

總結

FTP 看似簡單,實則坑多(編碼、被動模式、斷點續傳、連線管理、安全性)。棄用 JDK 原生 API,全面擁抱 Apache Commons Net + 連線池 + 統一編碼轉換 + 連線池監控,才是企業級穩健之道。對於新專案,優先考慮 SFTP (JSch/Apache MINA SSHD) 或 對象存儲 (S3/MinIO/OSS) 替代傳統 FTP。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved