Java | 基于 Feign 流式传输操作SFTP文件传输

关注CodingTechWork

引言

在企业级应用开发中,跨服务文件传输是一个高频场景。典型的业务场景包括:财务报表上传、对账单下载、批量数据处理等。这些场景往往涉及大文件传输,如果处理不当,很容易导致内存溢出。

本文介绍一种生产级架构设计:A 服务通过 Feign 客户端调用 B 服务,由 B 服务负责与 SFTP 服务器进行文件交互 ,执行文件的上传、下载和删除操作。核心目标是实现大文件的安全、高效传输,同时保证内存占用可控。

  1. SFTP 原理:基于 SSH 的安全文件传输协议,支持密码和公钥两种认证方式
  2. 架构设计:A 服务通过 Feign 调用 B 服务,B 服务负责与 SFTP 服务器交互
  3. 内存优化:使用流式传输,避免将整个文件加载到内存
  4. Feign 配置 :返回类型使用 feign.Response,配置足够长的读取超时
  5. 资源管理:使用 try-with-resources 确保连接和临时文件正确释放

SFTP 原理与认证

什么是 SFTP

SFTP(SSH File Transfer Protocol,SSH 文件传输协议)是一种基于 SSH 协议的安全文件传输协议。与传统的 FTP 不同,SFTP 在 SSH 加密通道上运行,对命令和数据都进行加密传输。

SFTP 的核心特性:

特性 说明
安全性 所有数据(包括用户名、密码、文件内容)都经过加密
单端口 只需开放 22 端口,防火墙配置简单
功能丰富 支持文件上传/下载、目录操作、文件权限管理等
平台兼容 支持断点续传、文件列表获取等高级功能

SFTP 协议架构

SFTP 认证方式

SFTP 支持多种认证方式,以下是常用的两种:

密码认证

最基础的认证方式,使用用户名和密码进行身份验证。

复制代码
客户端 ──► SSH 服务器
         │
         ├── 1. 建立 TCP 连接
         ├── 2. SSH 协议版本协商
         ├── 3. 服务器发送公钥
         ├── 4. 客户端加密密码并发送
         └── 5. 服务器验证并返回结果

优点 :配置简单,易于集成
缺点:密码在网络中传输(虽然加密),存在被破解风险

公钥认证

使用 RSA 或 DSA 密钥对进行认证,安全性更高。

复制代码
客户端 ──► SSH 服务器
         │
         ├── 1. 客户端发送公钥指纹
         ├── 2. 服务器检查 authorized_keys
         ├── 3. 服务器生成随机挑战码
         ├── 4. 客户端用私钥签名
         └── 5. 服务器用公钥验证签名

优点 :安全性高,无需传输密码
缺点:需要预先配置密钥

认证方式对比

认证方式 安全性 配置复杂度 适用场景
密码认证 内部网络、测试环境
公钥认证 生产环境、自动化脚本
键盘交互 需要双因素认证的场景

需求场景

业务场景

假设我们有一个金融系统,需要处理以下文件操作需求:

  1. 对账单上传:A 服务生成每日对账单,需要上传到 SFTP 服务器供合作方下载
  2. 数据同步:A 服务需要从 SFTP 服务器下载合作方上传的批量数据文件
  3. 文件清理:处理完成后删除 SFTP 服务器上的历史文件

架构设计

核心挑战

挑战 描述 解决方案
内存溢出 大文件加载到内存导致 OOM 流式传输,分块处理
超时控制 大文件传输时间长 合理设置 Feign 超时
资源泄漏 连接和临时文件未释放 使用 try-with-resources
并发控制 多请求同时传输 连接池管理

Java 实现

技术选型

组件 技术 版本 说明
SFTP 客户端 JSch 0.1.55 成熟稳定的 SSH2 实现
微服务框架 Spring Boot + Cloud 2.7.x 服务治理与 Feign 支持
HTTP 客户端 Feign - 声明式 HTTP 调用
连接池 Apache Commons Pool2 2.11.x SFTP 连接复用

时序图

Maven 依赖

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
    </parent>
    
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Cloud OpenFeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.1.5</version>
        </dependency>
        
        <!-- SFTP 客户端 -->
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.55</version>
        </dependency>
        
        <!-- 连接池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.11.1</version>
        </dependency>
        
        <!-- 工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

B 服务实现

SFTP 配置类

java 复制代码
package com.example.sftp.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "sftp")
public class SftpProperties {
    
    /** SFTP 服务器地址 */
    private String host;
    
    /** SFTP 端口,默认 22 */
    private int port = 22;
    
    /** 用户名 */
    private String username;
    
    /** 密码认证方式使用 */
    private String password;
    
    /** 私钥路径(公钥认证方式使用) */
    private String privateKeyPath;
    
    /** 私钥密码 */
    private String privateKeyPassphrase;
    
    /** 连接超时(毫秒) */
    private int connectionTimeout = 30000;
    
    /** 缓冲区大小(字节),默认 4MB */
    private int bufferSize = 4 * 1024 * 1024;
    
    /** 临时文件目录 */
    private String tempDirectory = "/tmp/sftp";
    
    /** 是否跳过主机密钥检查(生产环境应设为 false) */
    private boolean strictHostKeyChecking = false;
}
yaml 复制代码
# application.yml
sftp:
  host: sftp.example.com
  port: 22
  username: sftp_user
  password: ${SFTP_PASSWORD:}
  private-key-path: /keys/id_rsa
  buffer-size: 4194304
  temp-directory: /data/sftp-temp
  strict-host-key-checking: false

server:
  port: 8081

feign:
  client:
    config:
      default:
        connectTimeout: 30000
        readTimeout: 600000

SFTP 连接管理器

java 复制代码
package com.example.sftp.connection;

import com.example.sftp.config.SftpProperties;
import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Properties;

@Slf4j
@Component
public class SftpConnectionManager {
    
    private final SftpProperties properties;
    private Session session;
    private ChannelSftp channelSftp;
    
    public SftpConnectionManager(SftpProperties properties) {
        this.properties = properties;
    }
    
    /**
     * 获取 SFTP 通道
     * 采用懒加载方式,首次调用时建立连接
     */
    public synchronized ChannelSftp getChannel() throws JSchException {
        if (isConnected()) {
            return channelSftp;
        }
        
        close();
        establishConnection();
        return channelSftp;
    }
    
    /**
     * 建立连接
     */
    private void establishConnection() throws JSchException {
        JSch jsch = new JSch();
        
        // 配置私钥认证(优先使用)
        configurePrivateKey(jsch);
        
        // 创建会话
        session = jsch.getSession(
            properties.getUsername(),
            properties.getHost(),
            properties.getPort()
        );
        
        // 配置密码认证
        configurePassword();
        
        // 配置会话属性
        configureSession();
        
        // 连接会话
        session.connect(properties.getConnectionTimeout());
        
        // 打开 SFTP 通道
        channelSftp = (ChannelSftp) session.openChannel("sftp");
        channelSftp.connect();
        
        log.info("SFTP 连接成功: {}:{}", properties.getHost(), properties.getPort());
    }
    
    /**
     * 配置私钥认证
     */
    private void configurePrivateKey(JSch jsch) throws JSchException {
        if (properties.getPrivateKeyPath() != null && !properties.getPrivateKeyPath().isEmpty()) {
            if (properties.getPrivateKeyPassphrase() != null) {
                jsch.addIdentity(
                    properties.getPrivateKeyPath(),
                    properties.getPrivateKeyPassphrase()
                );
            } else {
                jsch.addIdentity(properties.getPrivateKeyPath());
            }
            log.info("使用私钥认证: {}", properties.getPrivateKeyPath());
        }
    }
    
    /**
     * 配置密码认证
     */
    private void configurePassword() {
        if (properties.getPassword() != null && !properties.getPassword().isEmpty()) {
            session.setPassword(properties.getPassword());
            log.info("使用密码认证");
        }
    }
    
    /**
     * 配置会话属性
     */
    private void configureSession() {
        Properties config = new Properties();
        
        // 主机密钥检查(生产环境应开启)
        if (!properties.isStrictHostKeyChecking()) {
            config.put("StrictHostKeyChecking", "no");
        }
        
        session.setConfig(config);
        session.setTimeout(properties.getConnectionTimeout());
    }
    
    /**
     * 检查连接是否有效
     */
    private boolean isConnected() {
        return channelSftp != null 
            && channelSftp.isConnected() 
            && session != null 
            && session.isConnected();
    }
    
    /**
     * 关闭连接
     */
    public synchronized void close() {
        if (channelSftp != null && channelSftp.isConnected()) {
            channelSftp.disconnect();
            log.debug("SFTP 通道已关闭");
        }
        if (session != null && session.isConnected()) {
            session.disconnect();
            log.debug("SSH 会话已关闭");
        }
    }
}

SFTP 文件操作服务

java 复制代码
package com.example.sftp.service;

import com.example.sftp.config.SftpProperties;
import com.example.sftp.connection.SftpConnectionManager;
import com.jcraft.ChannelSftp;
import com.jcraft.SftpException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;

@Slf4j
@Service
public class SftpFileService {
    
    private final SftpProperties properties;
    private final SftpConnectionManager connectionManager;
    
    public SftpFileService(SftpProperties properties, SftpConnectionManager connectionManager) {
        this.properties = properties;
        this.connectionManager = connectionManager;
    }
    
    /**
     * 上传文件到 SFTP
     * 
     * @param remotePath 远程文件路径(如 /data/file.txt)
     * @param inputStream 文件输入流
     */
    public void uploadFile(String remotePath, InputStream inputStream) {
        ChannelSftp channelSftp = null;
        
        try {
            channelSftp = connectionManager.getChannel();
            
            // 创建远程目录(如果不存在)
            createRemoteDirectory(channelSftp, getParentPath(remotePath));
            
            // 流式上传
            try (BufferedInputStream bis = new BufferedInputStream(inputStream)) {
                channelSftp.put(bis, remotePath, ChannelSftp.OVERWRITE);
            }
            
            log.info("文件上传成功: {}", remotePath);
            
        } catch (Exception e) {
            log.error("文件上传失败: {}", remotePath, e);
            throw new RuntimeException("文件上传失败: " + e.getMessage(), e);
        }
    }
    
    /**
     * 从 SFTP 下载文件到临时文件
     * 
     * @param remotePath 远程文件路径
     * @return 临时文件
     */
    public File downloadFileToTemp(String remotePath) {
        ChannelSftp channelSftp = null;
        File tempFile = null;
        
        try {
            channelSftp = connectionManager.getChannel();
            
            // 检查文件是否存在
            checkFileExists(channelSftp, remotePath);
            
            // 创建临时文件
            tempFile = createTempFile();
            
            // 分块下载
            downloadWithBuffer(channelSftp, remotePath, tempFile);
            
            log.info("文件下载完成: {} -> {} ({} bytes)", 
                     remotePath, tempFile.getAbsolutePath(), tempFile.length());
            return tempFile;
            
        } catch (Exception e) {
            cleanupTempFile(tempFile);
            log.error("文件下载失败: {}", remotePath, e);
            throw new RuntimeException("文件下载失败: " + e.getMessage(), e);
        }
    }
    
    /**
     * 分块下载文件
     */
    private void downloadWithBuffer(ChannelSftp channelSftp, String remotePath, File tempFile) 
            throws SftpException, IOException {
        
        int bufferSize = properties.getBufferSize();
        byte[] buffer = new byte[bufferSize];
        
        try (InputStream inputStream = channelSftp.get(remotePath);
             FileOutputStream fos = new FileOutputStream(tempFile);
             BufferedOutputStream bos = new BufferedOutputStream(fos, bufferSize)) {
            
            int bytesRead;
            long totalBytes = 0;
            
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
                totalBytes += bytesRead;
                
                // 每传输 10MB 记录一次进度
                if (totalBytes % (10 * 1024 * 1024) == 0) {
                    log.debug("下载进度: {} bytes - {}", totalBytes, remotePath);
                }
            }
            
            bos.flush();
        }
    }
    
    /**
     * 删除 SFTP 文件
     * 
     * @param remotePath 远程文件路径
     * @return 是否删除成功
     */
    public boolean deleteFile(String remotePath) {
        ChannelSftp channelSftp = null;
        
        try {
            channelSftp = connectionManager.getChannel();
            
            // 检查文件是否存在
            if (!fileExists(channelSftp, remotePath)) {
                log.warn("文件不存在,无需删除: {}", remotePath);
                return false;
            }
            
            channelSftp.rm(remotePath);
            log.info("文件删除成功: {}", remotePath);
            return true;
            
        } catch (Exception e) {
            log.error("文件删除失败: {}", remotePath, e);
            throw new RuntimeException("文件删除失败: " + e.getMessage(), e);
        }
    }
    
    /**
     * 创建临时文件
     */
    private File createTempFile() throws IOException {
        Path tempDir = Path.of(properties.getTempDirectory());
        if (!Files.exists(tempDir)) {
            Files.createDirectories(tempDir);
        }
        return File.createTempFile("sftp_", ".tmp", tempDir.toFile());
    }
    
    /**
     * 检查文件是否存在
     */
    private boolean fileExists(ChannelSftp channelSftp, String remotePath) {
        try {
            channelSftp.stat(remotePath);
            return true;
        } catch (SftpException e) {
            if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                return false;
            }
            throw new RuntimeException("检查文件存在性失败", e);
        }
    }
    
    /**
     * 检查文件存在,不存在则抛出异常
     */
    private void checkFileExists(ChannelSftp channelSftp, String remotePath) throws SftpException {
        try {
            channelSftp.stat(remotePath);
        } catch (SftpException e) {
            if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                throw new RuntimeException("远程文件不存在: " + remotePath);
            }
            throw e;
        }
    }
    
    /**
     * 创建远程目录
     */
    private void createRemoteDirectory(ChannelSftp channelSftp, String remoteDir) throws SftpException {
        if (remoteDir == null || remoteDir.isEmpty()) {
            return;
        }
        
        try {
            channelSftp.cd(remoteDir);
            return;
        } catch (SftpException e) {
            // 目录不存在,继续创建
        }
        
        String[] dirs = remoteDir.split("/");
        StringBuilder path = new StringBuilder();
        
        for (String dir : dirs) {
            if (dir.isEmpty()) {
                continue;
            }
            
            path.append("/").append(dir);
            
            try {
                channelSftp.cd(path.toString());
            } catch (SftpException e) {
                channelSftp.mkdir(path.toString());
                channelSftp.cd(path.toString());
            }
        }
    }
    
    /**
     * 获取父目录路径
     */
    private String getParentPath(String path) {
        int lastSlash = path.lastIndexOf('/');
        if (lastSlash <= 0) {
            return "";
        }
        return path.substring(0, lastSlash);
    }
    
    /**
     * 清理临时文件
     */
    private void cleanupTempFile(File tempFile) {
        if (tempFile != null && tempFile.exists()) {
            tempFile.delete();
            log.debug("临时文件已清理: {}", tempFile.getAbsolutePath());
        }
    }
}

Controller 实现

java 复制代码
package com.example.sftp.controller;

import com.example.sftp.service.SftpFileService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

@Slf4j
@RestController
@RequestMapping("/api/sftp")
public class SftpController {
    
    private final SftpFileService sftpFileService;
    
    public SftpController(SftpFileService sftpFileService) {
        this.sftpFileService = sftpFileService;
    }
    
    /**
     * 上传文件
     */
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<Result<Void>> upload(
            @RequestPart("request") UploadRequest request,
            @RequestPart("file") MultipartFile file) {
        
        try (InputStream inputStream = file.getInputStream()) {
            sftpFileService.uploadFile(request.getRemotePath(), inputStream);
            return ResponseEntity.ok(Result.success());
        } catch (Exception e) {
            log.error("上传失败", e);
            return ResponseEntity.status(500).body(Result.failure(e.getMessage()));
        }
    }
    
    /**
     * 下载文件(流式返回)
     */
    @GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<StreamingResponseBody> download(@RequestParam String remotePath) {
        
        try {
            File tempFile = sftpFileService.downloadFileToTemp(remotePath);
            
            StreamingResponseBody responseBody = outputStream -> {
                try (FileInputStream fis = new FileInputStream(tempFile)) {
                    byte[] buffer = new byte[8192];
                    int bytesRead;
                    while ((bytesRead = fis.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                    }
                    outputStream.flush();
                } finally {
                    if (tempFile.exists()) {
                        tempFile.delete();
                        log.debug("临时文件已删除: {}", tempFile.getAbsolutePath());
                    }
                }
            };
            
            String fileName = extractFileName(remotePath);
            
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, 
                            "attachment; filename=\"" + fileName + "\"")
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .body(responseBody);
                    
        } catch (Exception e) {
            log.error("下载失败: {}", remotePath, e);
            return ResponseEntity.status(500).build();
        }
    }
    
    /**
     * 删除文件
     */
    @DeleteMapping("/file")
    public ResponseEntity<Result<Void>> delete(@RequestParam String remotePath) {
        try {
            boolean deleted = sftpFileService.deleteFile(remotePath);
            return ResponseEntity.ok(Result.success(deleted));
        } catch (Exception e) {
            log.error("删除失败: {}", remotePath, e);
            return ResponseEntity.status(500).body(Result.failure(e.getMessage()));
        }
    }
    
    private String extractFileName(String path) {
        int lastSlash = path.lastIndexOf('/');
        return lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
    }
}

A 服务实现

Feign 客户端接口

java 复制代码
package com.example.client;

import feign.Response;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@FeignClient(name = "sftp-service", 
             url = "${sftp.service.url}",
             configuration = SftpFeignConfig.class)
public interface SftpFeignClient {
    
    /**
     * 上传文件到 SFTP
     */
    @PostMapping(value = "/api/sftp/upload", 
                 consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    Result<Void> upload(@RequestPart("request") UploadRequest request, 
                        @RequestPart("file") MultipartFile file);
    
    /**
     * 从 SFTP 下载文件
     * 注意:返回类型必须是 feign.Response,不能是 ResponseEntity<Resource>
     */
    @GetMapping(value = "/api/sftp/download", 
                produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    Response download(@RequestParam("remotePath") String remotePath);
    
    /**
     * 删除 SFTP 文件
     */
    @DeleteMapping("/api/sftp/file")
    Result<Void> delete(@RequestParam("remotePath") String remotePath);
}

Feign 配置类

java 复制代码
package com.example.client;

import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.lang.reflect.Type;

@Configuration
public class SftpFeignConfig {
    
    /**
     * 配置请求选项
     * 连接超时30秒,读取超时30分钟(大文件传输需要足够时间)
     */
    @Bean
    public feign.Request.Options options() {
        return new feign.Request.Options(30_000, 1_800_000);
    }
    
    /**
     * 自定义解码器
     * 对于返回类型为 Response 的接口,不进行解码,直接返回原始响应
     */
    @Bean
    public Decoder feignDecoder() {
        return new StreamDecoder();
    }
    
    /**
     * 编码器
     */
    @Bean
    public Encoder feignEncoder() {
        return new feign.codec.Encoder.Default();
    }
    
    /**
     * 错误解码器
     */
    @Bean
    public ErrorDecoder errorDecoder() {
        return new SftpErrorDecoder();
    }
    
    /**
     * 流式解码器:不对响应体进行预加载
     */
    private static class StreamDecoder implements Decoder {
        @Override
        public Object decode(Response response, Type type) throws IOException {
            // 如果返回类型是 Response,直接返回原始响应
            if (type.equals(Response.class)) {
                return response;
            }
            // 其他类型使用默认解码器
            return new feign.codec.Decoder.Default()
                    .decode(response, type);
        }
    }
    
    /**
     * 错误解码器
     */
    private static class SftpErrorDecoder implements ErrorDecoder {
        @Override
        public Exception decode(String methodKey, Response response) {
            return new RuntimeException(
                String.format("SFTP 服务调用失败: HTTP %d - %s", 
                              response.status(), response.reason())
            );
        }
    }
}

A 服务业务实现

java 复制代码
package com.example.service;

import com.example.client.SftpFeignClient;
import com.example.client.UploadRequest;
import feign.Response;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Slf4j
@Service
public class FileTransferService {
    
    private final SftpFeignClient sftpFeignClient;
    
    public FileTransferService(SftpFeignClient sftpFeignClient) {
        this.sftpFeignClient = sftpFeignClient;
    }
    
    /**
     * 上传本地文件到 SFTP
     */
    public void uploadToSftp(String localFilePath, String remotePath) throws IOException {
        File file = new File(localFilePath);
        
        if (!file.exists()) {
            throw new RuntimeException("本地文件不存在: " + localFilePath);
        }
        
        // 构建 MultipartFile
        MultipartFile multipartFile = new CustomMultipartFile(file);
        
        UploadRequest request = new UploadRequest();
        request.setRemotePath(remotePath);
        
        Result<Void> result = sftpFeignClient.upload(request, multipartFile);
        
        if (result.getCode() != 200) {
            throw new RuntimeException("上传失败: " + result.getMessage());
        }
        
        log.info("文件上传成功: {} -> {}", localFilePath, remotePath);
    }
    
    /**
     * 从 SFTP 下载文件到本地
     */
    public void downloadFromSftp(String remotePath, String localFilePath) throws IOException {
        Response response = sftpFeignClient.download(remotePath);
        
        if (response.status() != 200) {
            throw new RuntimeException("下载失败: HTTP " + response.status());
        }
        
        // 确保目录存在
        Path localPath = Paths.get(localFilePath);
        Files.createDirectories(localPath.getParent());
        
        // 流式写入本地文件
        try (Response.Body body = response.body();
             InputStream inputStream = body.asInputStream();
             FileOutputStream fos = new FileOutputStream(localFilePath);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            
            byte[] buffer = new byte[8192];
            int bytesRead;
            long totalBytes = 0;
            
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
                totalBytes += bytesRead;
                
                if (totalBytes % (10 * 1024 * 1024) == 0) {
                    log.info("下载进度: {} bytes - {}", totalBytes, remotePath);
                }
            }
            
            bos.flush();
            log.info("文件下载完成: {} -> {} ({} bytes)", 
                     remotePath, localFilePath, totalBytes);
        }
    }
    
    /**
     * 从 SFTP 下载文件并直接处理流
     * 适用于需要流式处理的场景
     */
    public void downloadAndProcess(String remotePath, InputStreamProcessor processor) throws IOException {
        Response response = sftpFeignClient.download(remotePath);
        
        if (response.status() != 200) {
            throw new RuntimeException("下载失败: HTTP " + response.status());
        }
        
        try (Response.Body body = response.body();
             InputStream inputStream = body.asInputStream()) {
            processor.process(inputStream);
        }
    }
    
    /**
     * 删除 SFTP 文件
     */
    public void deleteFromSftp(String remotePath) {
        Result<Void> result = sftpFeignClient.delete(remotePath);
        
        if (result.getCode() != 200) {
            throw new RuntimeException("删除失败: " + result.getMessage());
        }
        
        log.info("文件删除成功: {}", remotePath);
    }
    
    /**
     * 输入流处理器接口
     */
    @FunctionalInterface
    public interface InputStreamProcessor {
        void process(InputStream inputStream) throws IOException;
    }
}

关键技术要点

内存优化核心

问题:传统方式会将整个文件加载到内存,导致 OOM

java 复制代码
// 错误示例
byte[] content = file.getBytes(); 
// 整个文件加载到内存
channelSftp.put(new ByteArrayInputStream(content), remotePath);

解决方案:分块流式传输

java 复制代码
// 正确示例
try (InputStream inputStream = file.getInputStream()) {
    // put 方法内部使用缓冲区,不会一次性读取全部内容
    channelSftp.put(inputStream, remotePath);
}

为什么使用 feign.Response

返回类型 内存行为 适用场景
ResponseEntity<Resource> Feign 会将整个响应体加载到内存 小文件(< 10MB)
feign.Response 流式访问,不预加载 大文件(任意大小)

##资源管理最佳实践

java 复制代码
// 使用 try-with-resources 确保资源释放
try (Response response = sftpFeignClient.download(remotePath);
     InputStream inputStream = response.body().asInputStream();
     FileOutputStream fos = new FileOutputStream(localPath)) {
    
    // 处理流
    byte[] buffer = new byte[8192];
    int bytesRead;
    while ((bytesRead = inputStream.read(buffer)) != -1) {
        fos.write(buffer, 0, bytesRead);
    }
}
相关推荐
无限进步_5 小时前
【C++】多重继承中的虚表布局分析:D类对象为何有两个虚表?
开发语言·c++·ide·windows·git·算法·visual studio
_Evan_Yao5 小时前
别让“规范”困住你:前后端交互中的方法选择与认知突围
java·后端·交互·restful
清水白石0085 小时前
向后兼容的工程伦理:Python 开发中“优雅重构”与“责任担当”的平衡之道
开发语言·python·重构
A.A呐5 小时前
【QT第六章】界面优化
开发语言·qt
小夏子_riotous6 小时前
openstack的使用——5. Swift服务的基本使用
linux·运维·开发语言·分布式·云计算·openstack·swift
千码君20166 小时前
kotlin:Jetpack Compose 给APP添加声音(点击音效/背景音乐)
android·开发语言·kotlin·音效·jetpack compose
星乐a6 小时前
String vs StringBuilder vs StringBuffer深度解析
java
萧逸才6 小时前
【learn-claude-code-4j】S14FeiShu - 飞书群聊智能体
java·人工智能·ai·飞书
吴声子夜歌6 小时前
ES6——对象的扩展详解
开发语言·javascript·es6