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

Java FTP 客戶端完整實作:上傳、下載、刪除、目錄操作與連接池優化

     編輯:Java 相關知識

前言

FTP 仍是企業系統間文件交換常用協議。本文提供生產級 Java FTP 工具類,基於 Apache Commons Net(推薦)與 JDK 內建 sun.net.ftp.FtpClient 兩種實現,涵蓋上傳下載、斷點續傳、目錄遞歸、連接池管理與異常重試完整方案。

方案一:Apache Commons Net(推薦,功能豐富)

Maven 依賴

<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();
        }
    }
}

方案二:JDK 內建 sun.net.ftp.FtpClient(零依賴,功能受限)

適用於無法引入第三方 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。

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