杰瑞科技汇

Java FTP上传文件为何为空?

下面我将从 问题根源解决方案完整代码示例最佳实践 四个方面来详细解释这个问题。

Java FTP上传文件为何为空?-图1
(图片来源网络,侵删)

问题根源:为什么文件会传成空的?

几乎可以肯定,问题的根源在于 文件流没有正确关闭

在 Java 中,当你使用 InputStream(如 FileInputStream)读取本地文件并上传到 FTP 服务器时,数据是像水流一样通过流管道传输的,如果这个管道在你传完所有数据之前就被关闭了,那么服务器端自然就只收到了一个空的文件。

一个典型的错误代码示例:

// 错误示范!会导致文件为空!
public void uploadWrong(String server, int port, String user, String pass, String localPath, String remotePath) {
    FTPClient ftpClient = new FTPClient();
    try {
        ftpClient.connect(server, port);
        ftpClient.login(user, pass);
        InputStream input = new FileInputStream(localPath); // 创建输入流
        // 上传文件
        boolean done = ftpClient.storeFile(remotePath, input);
        if (done) {
            System.out.println("File uploaded successfully.");
        } else {
            System.out.println("File upload failed. Reply: " + ftpClient.getReplyString());
        }
        // !!! 错误在这里 !!!
        // 如果在这里关闭流,或者因为异常流没有被关闭,文件传输可能不完整。
        // input.close(); // 即使在这里手动关闭,也可能因为异常导致未执行
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (ftpClient.isConnected()) {
                ftpClient.logout();
                ftpClient.disconnect();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

为什么 finally 块里的 disconnect 不能保证文件流关闭?

Java FTP上传文件为何为空?-图2
(图片来源网络,侵删)

ftpClient.disconnect() 关闭的是与 FTP 服务器的网络连接,但它不会自动关闭你代码中创建的 FileInputStream,这个 input 流对象的生命周期只在你创建它的 try 块内,如果在上传过程中抛出异常,input.close() 可能根本不会被执行,导致文件流一直开着,但数据可能已经不完整地发送了,或者更糟,流被垃圾回收器回收时,文件被截断。


解决方案:如何确保文件完整上传?

解决方案的核心思想是:确保 InputStream 在使用完毕后被正确关闭,并且最好是在一个资源管理结构中自动完成。

使用 try-with-resources (Java 7+ 推荐)

这是最现代、最安全、最推荐的写法。try-with-resources 语句会自动实现 AutoCloseable 接口的资源(如 InputStream, OutputStream, FTPClient 等)在 try 块执行完毕后自动关闭,即使发生了异常。

关键点:

  1. FTPClientInputStream 都声明在 try-with-resources 语句中。
  2. 这样可以保证无论上传成功还是失败,网络连接和文件流都会被正确关闭。
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import java.io.FileInputStream;
import java.io.IOException;
public void uploadWithTryWithResources(String server, int port, String user, String pass, 
                                       String localPath, String remotePath) {
    // 将 FTPClient 和 FileInputStream 都放在 try-with-resources 中
    try (FTPClient ftpClient = new FTPClient();
         FileInputStream input = new FileInputStream(localPath)) {
        ftpClient.connect(server, port);
        int reply = ftpClient.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            ftpClient.disconnect();
            System.err.println("FTP server refused connection.");
            return;
        }
        if (!ftpClient.login(user, pass)) {
            System.err.println("FTP login failed.");
            return;
        }
        // 设置为被动模式,避免在防火墙后出现问题
        ftpClient.enterLocalPassiveMode();
        // 设置文件类型为二进制,防止文本模式(如ASCII)导致文件损坏
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
        System.out.println("Starting upload of " + localPath);
        boolean done = ftpClient.storeFile(remotePath, input);
        if (done) {
            System.out.println("File uploaded successfully to " + remotePath);
        } else {
            System.out.println("File upload failed. Reply: " + ftpClient.getReplyString());
        }
    } catch (IOException e) {
        System.err.println("Error during FTP upload: " + e.getMessage());
        e.printStackTrace();
    }
    // ftpClient 和 input 都会自动被关闭,无需手动调用 close()
}

手动在 finally 块中关闭 (Java 7 以前)

如果你使用的 Java 版本低于 7,或者因为某些原因不能使用 try-with-resources,那么你必须在 finally 块中手动关闭所有资源。

关键点:

  1. InputStream 的关闭逻辑必须放在 finally 块中。
  2. 关闭时也要处理 IOException
  3. input.close() 必须在 ftpClient.disconnect() 之前调用。
public void uploadWithFinallyBlock(String server, int port, String user, String pass, 
                                  String localPath, String remotePath) {
    FTPClient ftpClient = new FTPClient();
    FileInputStream input = null;
    try {
        ftpClient.connect(server, port);
        ftpClient.login(user, pass);
        ftpClient.enterLocalPassiveMode();
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
        input = new FileInputStream(localPath);
        boolean done = ftpClient.storeFile(remotePath, input);
        if (done) {
            System.out.println("File uploaded successfully.");
        } else {
            System.out.println("File upload failed. Reply: " + ftpClient.getReplyString());
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 1. 首先关闭文件流
        if (input != null) {
            try {
                input.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 2. 然后关闭 FTP 连接
        if (ftpClient.isConnected()) {
            try {
                ftpClient.logout();
                ftpClient.disconnect();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

其他可能导致文件为空的原因

除了文件流未关闭,还要检查以下几点:

  1. 文件路径错误:检查 localPath 是否指向一个真实存在的文件,并且你有读取权限。remotePath 的目录在 FTP 服务器上是否存在且有写入权限。
  2. FTP 模式问题
    • 主动模式:客户端主动连接服务器数据端口,如果客户端在防火墙/NAT 之后,连接可能会被阻止。
    • 被动模式:服务器告诉客户端一个端口,客户端去连接这个端口,这是目前最常用的模式,能更好地穿透防火墙。
    • 解决方案:在登录后调用 ftpClient.enterLocalPassiveMode();
  3. 文件类型问题
    • 如果你上传的是图片、压缩包、可执行文件等二进制文件,但 FTP 客户端被设置为 ASCII 模式,文件内容可能会被错误地解释和转换,导致损坏或大小异常。
    • 解决方案:在登录后调用 ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
  4. 权限问题:FTP 用户对目标目录是否有写入权限?对目标文件名是否已存在且是只读文件?
  5. 网络问题:传输过程中网络中断,但客户端没有捕获到异常,导致部分文件已发送,连接断开,最终得到一个不完整的文件。

总结与最佳实践

  1. 首选 try-with-resources:这是解决资源泄漏最优雅、最可靠的方式,是现代 Java 开发的标准实践。
  2. 总是设置被动模式ftpClient.enterLocalPassiveMode(); 应该成为你的 FTP 连接代码中的标配。
  3. 总是设置二进制模式ftpClient.setFileType(FTP.BINARY_FILE_TYPE); 除非你明确知道你在传输纯文本文件。
  4. 检查返回值和应答码storeFile() 返回 boolean 表示成功与否。ftpClient.getReplyString()ftpClient.getReplyCode() 可以提供更详细的错误信息,对于调试至关重要。
  5. 使用成熟的库Apache Commons Net 是一个非常稳定和广泛使用的 FTP 客户端库,推荐使用。

遵循以上原则,你就能彻底解决 "Java FTP 上传文件为空" 的问题,并写出健壮、可靠的 FTP 客户端代码。

分享:
扫描分享到社交APP
上一篇
下一篇