Spring Boot 集成 SFTP 文件上传和下载功能


1 摘要

在项目中附件上传和下载是一个常用的功能。可采用的方案有三种,第一种,将附件保存至应用服务器上,这一种应对少量不重要附件可以使用,但对于大量附件或者有重要附件的,不建议使用;第二种,将附件保存至专门的文件服务器上,这一种可应对大量、重要的附件,但是需要自行架设大存储空间的服务器;第三种保存至类似阿里云的 OSS 的服务器上,这一种也可以应对大批量、重要的附件,不过租用第三方服务器也需要付费。本文将介绍第二种,在Spring Boot 项目中通过 SFTP 的方式上传和下载附件。

2 核心 Maven 依赖

./demo-common/pom.xml
        <!-- jsch ssh/sftp client -->
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>${jsch.version}</version>
        </dependency>

其中 ${jsch.version} 的版本为 0.1.55

3 核心代码

3.1 核心 SFTP 上传下载工具类
./demo-common/src/main/java/com/ljq/demo/springboot/common/util/SftpUtil.java
package com.ljq.demo.springboot.common.util;

import com.jcraft.jsch.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.Properties;

/**
 * @Description: sftp 工具类
 * @Author: junqiang.lu
 * @Date: 2020/9/23
 */
public class SftpUtil {

    private SftpUtil() {
    }

    /**
     * 每个目录下最大子文件(夹)数量
     */
    private static final int MAX_CHILD_FILE_NUMBER = 1000;
    private static volatile ChannelSftp sftp = null;
    private static volatile Session sshSession = null;

    /**
     * 附件上传
     *
     * @param sftpConfig sftp 配置信息
     * @param fileName 文件名
     * @param inputStream 文件流
     * @return
     * @throws SftpException
     * @throws JSchException
     */
    public static String upload(SftpConfig sftpConfig, String fileName, InputStream inputStream) throws SftpException, JSchException {
        return upload(sftpConfig,null, fileName, inputStream);
    }

    /**
     * 文件上传
     *
     * @param sftpConfig sftp 配置信息
     * @param relativePath 文件保存的相对路径(最后一级目录)
     * @param fileName 文件名
     * @param inputStream 文件流
     * @return
     * @throws JSchException
     * @throws SftpException
     */
    public static String upload(SftpConfig sftpConfig, String relativePath, String fileName, InputStream inputStream)
            throws JSchException, SftpException {
        init(sftpConfig);
        createFolder(sftpConfig.getPath());
        String filePath = sftpConfig.getPath();
        if (relativePath != null && !relativePath.trim().isEmpty()) {
            filePath = sftpConfig.getPath() + "/" + relativePath;
        }
        filePath = generateValidPath(filePath);
        filePath = filePath + "/" + fileName;
        sftp.put(inputStream, filePath);
        return filePath.substring(sftpConfig.getPath().length() + 1);
    }

    /**
     * 文件下载
     *
     * @param sftpConfig sftp 配置信息
     * @param fileName
     * @return
     * @throws JSchException
     * @throws SftpException
     */
    public static byte[] download(SftpConfig sftpConfig, String fileName) throws JSchException, SftpException,
            IOException {
        init(sftpConfig);
        InputStream inputStream = sftp.get(sftpConfig.getPath() + "/" + fileName);
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int n;
        byte[] data = new byte[1024];
        while ((n = inputStream.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, n);
        }
        buffer.flush();
        return buffer.toByteArray();
    }

    /**
     * 判断是否关闭
     *
     * @return
     */
    public static boolean isClosed() {
        if (Objects.isNull(sshSession) || Objects.isNull(sftp)) {
            return false;
        }
        if (Objects.nonNull(sshSession) && sshSession.isConnected() && Objects.nonNull(sftp) && sftp.isConnected()) {
            return true;
        }
        return false;
    }

    /**
     * 关闭
     */
    public static void close(){
        if (sshSession != null && sshSession.isConnected()) {
            sshSession.disconnect();
        }
        if (sftp != null && sftp.isConnected()) {
            sftp.disconnect();
        }
    }

    /**
     * 初始化
     *
     * @throws JSchException
     */
    private static void init(SftpConfig sftpConfig) throws JSchException {
        if (!isClosed()) {
            synchronized (SftpUtil.class) {
                if (!isClosed()) {
                    JSch jsch = new JSch();
                    jsch.getSession(sftpConfig.getUsername(), sftpConfig.getHost(), sftpConfig.getPort());
                    sshSession = jsch.getSession(sftpConfig.getUsername(), sftpConfig.getHost(), sftpConfig.getPort());
                    sshSession.setPassword(sftpConfig.getPassword());
                    Properties sshConfig = new Properties();
                    sshConfig.put("StrictHostKeyChecking", "no");
                    sshSession.setConfig(sshConfig);
                    sshSession.connect();
                    Channel channel = sshSession.openChannel("sftp");
                    channel.connect();
                    sftp = (ChannelSftp) channel;
                }
            }
        }
    }

    /**
     * 创建目录
     *
     * @param path 目录路径
     * @return
     */
    private static boolean createFolder(String path) {
        try {
            sftp.mkdir(path);
        } catch (SftpException e) {
            return false;
        }
        return true;
    }

    /**
     * 统计目录下文件(夹)数量
     *
     * @param path
     * @return
     */
    private static int countFiles(String path) throws SftpException {
        sftp.cd(path);
        return sftp.ls(path).size();
    }

    /**
     * 校验路径是否可用
     *
     * @param path
     * @return
     */
    private static boolean validatePathValid(String path) {
        int countFiles = 0;
        try {
            countFiles = countFiles(path);
        } catch (SftpException e) {
            createFolder(path);
        }
        if (countFiles <= MAX_CHILD_FILE_NUMBER) {
            return true;
        }
        return false;
    }

    /**
     * 生成有效路径
     *
     * @param path
     * @return
     */
    private static String generateValidPath(String path) {
        if (validatePathValid(path)) {
            return path;
        } else {
            String newPath = path + String.valueOf(System.currentTimeMillis()).substring(9);
            createFolder(newPath);
            return newPath;
        }
    }

    /**
     * sftp 配置信息
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class SftpConfig {
        /**
         * 主机地址
         */
        private String host;
        /**
         * sftp 连接端口
         */
        private int port;
        /**
         * 用户名
         */
        private String username;
        /**
         * 密码
         */
        private String password;
        /**
         * 文件保存根路径
         */
        private String path;

    }

    public static SftpConfig sftpConfig() {
        return new SftpUtil().new SftpConfig();
    }

    public static SftpConfig sftpConfig(String host, int port, String username, String password, String path) {
        return new SftpUtil().new SftpConfig(host, port, username, password, path);
    }



}

3.2 SFTP 附件上传配置

SFTP 配置类

./demo-base-web/src/main/java/com/ljq/demo/springboot/baseweb/config/SftpUploadConfig.java
package com.ljq.demo.springboot.baseweb.config;

import lombok.Getter;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
 * @Description: sftp 附件上传配置
 * @Author: junqiang.lu
 * @Date: 2020/9/27
 */
@ToString
@Getter
@Configuration
public class SftpUploadConfig {

    /**
     * 主机地址
     */
    @Value("${uploadSftp.host}")
    private String host;
    /**
     * sftp 连接端口
     */
    @Value("${uploadSftp.port}")
    private int port;
    /**
     * 用户名
     */
    @Value("${uploadSftp.username}")
    private String username;
    /**
     * 密码
     */
    @Value("${uploadSftp.password}")
    private String password;
    /**
     * 文件保存路径
     */
    @Value("${uploadSftp.path}")
    private String path;



}

SFTP 配置信息

./demo-web/src/main/resources/application-dev.yml
./demo-web/src/main/resources/application-test.yml
./demo-web/src/main/resources/application-pro.yml
## sftp 附件上传配置
uploadSftp:
  host: 172.16.140.10
  port: 22
  username: root
  password: root
  path: /home/ljq/upload

3.3 SFTP 附件上传与下载示例

Service 层

Service 接口:

./demo-service/src/main/java/com/ljq/demo/springboot/service/CommonService.java
package com.ljq.demo.springboot.service;

import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpException;
import com.ljq.demo.springboot.baseweb.api.ApiResult;
import com.ljq.demo.springboot.baseweb.exception.ParamsCheckException;
import com.ljq.demo.springboot.vo.DownloadBean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
 * @Description: 公共业务
 * @Author: junqiang.lu
 * @Date: 2018/12/24
 */
public interface CommonService {

    /**
     * 通过 sftp 上传附件至固定服务器
     *
     * @param file 文件
     * @param dir 文件夹名称
     * @return
     * @throws ParamsCheckException
     */
    ApiResult uploadSftp(MultipartFile file, String dir) throws ParamsCheckException;

    /**
     *
     * 通过 sftp 下载文件
     *
     * @param filePath 文件路径
     * @return
     * @throws JSchException
     * @throws SftpException
     * @throws IOException
     */
    ResponseEntity<byte[]> downloadSftp(String filePath) throws JSchException, SftpException, IOException;


}

Service 实现类:

./demo-service/src/main/java/com/ljq/demo/springboot/service/impl/CommonServiceImpl.java
package com.ljq.demo.springboot.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.StrUtil;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpException;
import com.ljq.demo.springboot.baseweb.api.ApiResult;
import com.ljq.demo.springboot.baseweb.api.ResponseCode;
import com.ljq.demo.springboot.baseweb.config.OSSConfig;
import com.ljq.demo.springboot.baseweb.config.PDFExportConfig;
import com.ljq.demo.springboot.baseweb.config.SftpUploadConfig;
import com.ljq.demo.springboot.baseweb.exception.ParamsCheckException;
import com.ljq.demo.springboot.baseweb.util.OSSBootUtil;
import com.ljq.demo.springboot.baseweb.util.OSSSingleUtil;
import com.ljq.demo.springboot.baseweb.util.PDFUtil;
import com.ljq.demo.springboot.baseweb.util.ResourceFileUtil;
import com.ljq.demo.springboot.common.util.SftpUtil;
import com.ljq.demo.springboot.service.CommonService;
import com.ljq.demo.springboot.vo.DownloadBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @Description: 公共业务具体实现类
 * @Author: junqiang.lu
 * @Date: 2018/12/24
 */
@Slf4j
@Service("commonService")
public class CommonServiceImpl implements CommonService {

    @Autowired
    private PDFExportConfig pdfExportConfig;
    @Autowired
    private OSSConfig ossConfig;
    @Autowired
    private SftpUploadConfig uploadConfig;

    /**
     * 通过 sftp 上传附件至固定服务器
     *
     * @param file 文件
     * @param dir 文件夹名称
     * @return
     * @throws ParamsCheckException
     */
    @Override
    public ApiResult uploadSftp(MultipartFile file, String dir) throws ParamsCheckException {
        if (Objects.isNull(file) || file.isEmpty()) {
            throw new ParamsCheckException(ResponseCode.MISS_REQUEST_PART_ERROR);
        }
        log.debug("原始文件名:{},文件大小: {} Mb", file.getOriginalFilename(), (file.getSize()/1048576));
        // 储存文件
        SftpUtil.SftpConfig sftpConfig = SftpUtil.sftpConfig();
        BeanUtil.copyProperties(uploadConfig, sftpConfig, CopyOptions.create().ignoreNullValue().ignoreError());
        String filePath;
        if (StrUtil.isNotBlank(dir)) {
            dir = dir.replaceAll("[^a-zA-Z0-9]", "");
        }
        try {
            filePath = SftpUtil.upload(sftpConfig,dir, System.currentTimeMillis() +
                    file.getOriginalFilename(), file.getInputStream());
        } catch (Exception e) {
            log.warn("文件上传失败,{}", e.getMessage());
            throw new ParamsCheckException(ResponseCode.FAIL);
        }
        return ApiResult.success(filePath);
    }

    /**
     * 通过 sftp 下载文件
     *
     * @param filePath 文件路径
     * @return
     * @throws JSchException
     * @throws SftpException
     * @throws IOException
     */
    @Override
    public ResponseEntity<byte[]> downloadSftp(String filePath) throws JSchException, SftpException, IOException {
        SftpUtil.SftpConfig sftpConfig = SftpUtil.sftpConfig();
        BeanUtil.copyProperties(uploadConfig, sftpConfig, CopyOptions.create().ignoreNullValue().ignoreError());
        byte[] bytes = SftpUtil.download(sftpConfig, filePath);
        HttpHeaders headers = new HttpHeaders();
        String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        headers.setContentDispositionFormData("attachment", new String(fileName.getBytes("UTF-8"),"ISO-8859-1"));
        return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
    }
}

Controller 层:

./demo-web/src/main/java/com/ljq/demo/springboot/web/controller/CommonController.java
package com.ljq.demo.springboot.web.controller;

import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpException;
import com.ljq.demo.springboot.baseweb.api.ApiResult;
import com.ljq.demo.springboot.baseweb.exception.ParamsCheckException;
import com.ljq.demo.springboot.common.annotation.ParamsCheck;
import com.ljq.demo.springboot.service.CommonService;
import com.ljq.demo.springboot.vo.DownloadBean;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
 * @Description: 公共模块控制中心
 * @Author: junqiang.lu
 * @Date: 2018/12/24
 */
@RestController
@RequestMapping("api/demo/common")
public class CommonController {

    private static final Logger logger = LoggerFactory.getLogger(CommonController.class);

    @Autowired
    private CommonService commonService;

    /**
     * 通过 sftp 上传附件至固定服务器
     *
     * @param file 文件
     * @param dir 文件夹名称
     * @return
     * @throws ParamsCheckException
     */
    @PostMapping(value = "/upload/sftp", produces = {MediaType.APPLICATION_JSON_VALUE})
    @ApiOperation(value = "文件上传", notes = "文件上传")
    public ResponseEntity<ApiResult> uploadSftp(@RequestParam("file") MultipartFile file,
                                            String dir) throws ParamsCheckException {
        ApiResult apiResult = commonService.uploadSftp(file, dir);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return new ResponseEntity<>(apiResult, headers, HttpStatus.OK);
    }

    /**
     * 通过 sftp 下载文件
     *
     * @param filePath 文件路径
     * @return
     * @throws JSchException
     * @throws SftpException
     * @throws IOException
     */
    @ParamsCheck(ignore = true)
    @GetMapping(value = "/download/sftp", produces = {MediaType.MULTIPART_FORM_DATA_VALUE})
    public ResponseEntity<byte[]> downloadSftp(String filePath) throws JSchException, SftpException, IOException {
        ResponseEntity<byte[]> responseEntity = commonService.downloadSftp(filePath);
        return responseEntity;
    }

}

4 测试

上传:

上传

下载:
下载

5 推荐参考资料

JAVA SFTP文件上传、下载及批量下载

Java InputStream to Byte Array and ByteBuffer

Java – Write to File

6 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值