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

问题根源:为什么文件会传成空的?
几乎可以肯定,问题的根源在于 文件流没有正确关闭。
在 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 不能保证文件流关闭?

ftpClient.disconnect() 关闭的是与 FTP 服务器的网络连接,但它不会自动关闭你代码中创建的 FileInputStream,这个 input 流对象的生命周期只在你创建它的 try 块内,如果在上传过程中抛出异常,input.close() 可能根本不会被执行,导致文件流一直开着,但数据可能已经不完整地发送了,或者更糟,流被垃圾回收器回收时,文件被截断。
解决方案:如何确保文件完整上传?
解决方案的核心思想是:确保 InputStream 在使用完毕后被正确关闭,并且最好是在一个资源管理结构中自动完成。
使用 try-with-resources (Java 7+ 推荐)
这是最现代、最安全、最推荐的写法。try-with-resources 语句会自动实现 AutoCloseable 接口的资源(如 InputStream, OutputStream, FTPClient 等)在 try 块执行完毕后自动关闭,即使发生了异常。
关键点:
- 将
FTPClient和InputStream都声明在try-with-resources语句中。 - 这样可以保证无论上传成功还是失败,网络连接和文件流都会被正确关闭。
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 块中手动关闭所有资源。
关键点:
InputStream的关闭逻辑必须放在finally块中。- 关闭时也要处理
IOException。 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();
}
}
}
}
其他可能导致文件为空的原因
除了文件流未关闭,还要检查以下几点:
- 文件路径错误:检查
localPath是否指向一个真实存在的文件,并且你有读取权限。remotePath的目录在 FTP 服务器上是否存在且有写入权限。 - FTP 模式问题:
- 主动模式:客户端主动连接服务器数据端口,如果客户端在防火墙/NAT 之后,连接可能会被阻止。
- 被动模式:服务器告诉客户端一个端口,客户端去连接这个端口,这是目前最常用的模式,能更好地穿透防火墙。
- 解决方案:在登录后调用
ftpClient.enterLocalPassiveMode();。
- 文件类型问题:
- 如果你上传的是图片、压缩包、可执行文件等二进制文件,但 FTP 客户端被设置为
ASCII模式,文件内容可能会被错误地解释和转换,导致损坏或大小异常。 - 解决方案:在登录后调用
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);。
- 如果你上传的是图片、压缩包、可执行文件等二进制文件,但 FTP 客户端被设置为
- 权限问题:FTP 用户对目标目录是否有写入权限?对目标文件名是否已存在且是只读文件?
- 网络问题:传输过程中网络中断,但客户端没有捕获到异常,导致部分文件已发送,连接断开,最终得到一个不完整的文件。
总结与最佳实践
- 首选
try-with-resources:这是解决资源泄漏最优雅、最可靠的方式,是现代 Java 开发的标准实践。 - 总是设置被动模式:
ftpClient.enterLocalPassiveMode();应该成为你的 FTP 连接代码中的标配。 - 总是设置二进制模式:
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);除非你明确知道你在传输纯文本文件。 - 检查返回值和应答码:
storeFile()返回boolean表示成功与否。ftpClient.getReplyString()或ftpClient.getReplyCode()可以提供更详细的错误信息,对于调试至关重要。 - 使用成熟的库:
Apache Commons Net是一个非常稳定和广泛使用的 FTP 客户端库,推荐使用。
遵循以上原则,你就能彻底解决 "Java FTP 上传文件为空" 的问题,并写出健壮、可靠的 FTP 客户端代码。
