You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java FTP客户端无法完整下载文件的问题及流式传输解决方案诉求

FTP下载图片仅返回部分内容的问题解决

问题描述

通过Java客户端从FTP服务器下载图片返回给Web应用时,仅能获取图片前几KB(约64KB)内容,剩余部分缺失,UI仅显示图片上部;无异常信息,日志也无报错。该功能需支持大文件流式转发,不能一次性读取全部内容。

现有代码

FTP客户端封装类

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;

import java.io.IOException;
import java.io.InputStream;

@Slf4j
public class FtpClientWrapper implements AutoCloseable {
    private final FTPClient client;

    @SneakyThrows
    public FtpClientWrapper(FtpClientProperties properties) {
        FTPClient client = new FTPClient();

        client.connect(properties.getServer(), properties.getPort());

        client.enterLocalPassiveMode();

        int reply = client.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            client.disconnect();
            throw new IOException("Exception in connecting to FTP Server. ReplyCode: " + reply);
        }

        boolean loginSuccess = client.login(properties.getUsername(), properties.getPassword());

        if (!loginSuccess) {
            throw new RuntimeException("FTP login failed.");
        }

        this.client = client;
    }

    @SneakyThrows
    public InputStream downloadFile(String fileName) {
        client.setFileType(FTP.BINARY_FILE_TYPE);
        return client.retrieveFileStream(fileName);
    }

    // 新增获取client的方法,供后续命令完成使用
    public FTPClient getClient() {
        return client;
    }

    @Override
    public void close() throws Exception {
        if (client.isConnected()) {
            try {
                if (client.hasPendingCommand()) {
                    client.completePendingCommand();
                }
                client.logout();
                client.disconnect();
            } catch (IOException e) {
                log.error("关闭FTP客户端失败", e);
            }
        }
    }
}

Controller方法(原代码)

@Override
public ResponseEntity<InputStreamResource> downloadFile(String fileName) {
    InputStream result = ftpClient.downloadFile(fileName);

    return ResponseEntity.ok()
        .contentLength(<correct file size>)
        .contentType(<proper content type>)
        .body(new InputStreamResource(result));
}

问题原因

使用Apache Commons Net的FTPClient.retrieveFileStream()获取输入流后,必须调用completePendingCommand()完成FTP协议的命令收尾。若未执行此操作,FTP服务器会认为传输未完成,提前断开连接,仅发送缓冲区中的部分数据(通常为64KB左右)。

修复方案

改用StreamingResponseBody控制流传输过程,确保数据全部发送后完成FTP命令;同时完善FTP客户端的关闭逻辑。

修改后的Controller代码

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

@Override
public ResponseEntity<StreamingResponseBody> downloadFile(String fileName) {
    FTPClient ftpClient = ftpClientWrapper.getClient();
    InputStream ftpInputStream = ftpClient.downloadFile(fileName);

    StreamingResponseBody responseBody = outputStream -> {
        try (ftpInputStream) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            // 流式传输数据到客户端
            while ((bytesRead = ftpInputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            // 完成FTP命令,确认传输结束
            if (!ftpClient.completePendingCommand()) {
                throw new IOException("FTP文件传输未完成");
            }
        } catch (IOException e) {
            log.error("下载FTP文件失败", e);
            throw new RuntimeException("文件下载失败", e);
        }
    };

    // 正确获取FTP文件大小
    long fileSize;
    try {
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
        fileSize = ftpClient.getFileSize(fileName);
    } catch (IOException e) {
        log.error("获取FTP文件大小失败", e);
        fileSize = -1;
    }

    return ResponseEntity.ok()
        .contentLength(fileSize)
        .contentType(MediaType.IMAGE_JPEG) // 根据实际图片类型调整,如IMAGE_PNG
        .body(responseBody);
}

关键说明

  1. completePendingCommand()的必要性:该方法用于接收FTP服务器的传输完成响应,若不调用,连接会被服务器强制断开,导致数据传输不完整。
  2. StreamingResponseBody的优势:Spring提供的此接口支持流式输出,避免一次性加载大文件到内存,同时能在流传输完成后执行收尾逻辑(如FTP命令确认)。
  3. 文件大小获取:需在二进制模式下调用getFileSize(),确保获取的文件大小准确,避免客户端因Content-Length不匹配提前终止接收。

内容的提问来源于stack exchange,提问作者Saphyra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 05:55:30