Springbootはsftpファイルサーバーを統合してファイルのアップロードとダウンロード機能を実現します

目次

1.プロジェクトの構造

2.sftpコア構成の依存関係を追加します

3.sftpConfigオブジェクトを定義します

4. sftpクライアントを定義します。これには、接続の検証、フォルダーの作成、ファイルの判断、ファイルのアップロードとダウンロード、および実際のニーズに応じて使用できるその他の方法が含まれています。

5.sftpファクトリクラスを定義します

6.SftpFactoryをスタートアップクラスに追加します。

7.テストメソッドを作成します

8.ファイルが生成されるかどうかを確認するためのSwaggerインターフェイス呼び出しテスト


モジュールコードを直接アップロードします。

完全なプロジェクトソースコードアドレス(ideal-sftp-1018モジュールを参照):https//github.com/yangshilei/springCloud

1.プロジェクトの構造

次の赤い円は、sftpクライアント操作の主なオブジェクトメソッドです。

2.sftpコア構成の依存関係を追加します

        <!--sftp核心依赖包-->
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.54</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>

3.sftpConfigオブジェクトを定義します

package com.demo.sftp.sftp;

import com.alibaba.fastjson.JSON;
import lombok.Data;

@Data
public class SftpConfig {

    /**
     * ftp连接名
     */
    private String ftpName;
    /**
     * SFTP IP地址
     */
    private String ip;
    /**
     * SFTP 端口
     */
    private Integer port;
    /**
     * SFTP 用户名
     */
    private String userName;
    /**
     * SFTP 密码
     */
    private String password;
    /**
     * 连接失败次数
     */
    private int clientFailNum;

    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }

}

4. sftpクライアントを定義します。これには、接続の検証、フォルダーの作成、ファイルの判断、ファイルのアップロードとダウンロード、および実際のニーズに応じて使用できるその他の方法が含まれています。

package com.demo.sftp.sftp;

import com.demo.sftp.utils.Base64Util;
import com.jcraft.jsch.*;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;

import javax.validation.constraints.NotNull;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

@Slf4j
@Data
public class SftpClient implements AutoCloseable {


    /**
     * 配置信息
     */
    private SftpConfig conf;
    /**
     * Sftp客户端对象
     */
    private ChannelSftp sftp = null;
    /**
     * 会话
     */
    private Session sshSession = null;
    /**
     * 通道
     */
    private Channel channel = null;
    /**
     * 最后一次使用的时间
     * 为便于计算, 使用long型时间戳记录
     */
    private long lastUseDate;

    public SftpClient(SftpConfig conf) {
        this(conf.getFtpName(), conf.getIp(), conf.getPort(), conf.getUserName(), conf.getPassword());
    }

    public SftpClient(String ip, int port, String username, String password) {
        this(null, ip, port, username, password);
    }

    public SftpClient(String sftpName,String ip, int port, String username, String password) {
        conf = new SftpConfig();
        if(StringUtils.isEmpty(sftpName)){
            conf.setFtpName(username + "@" + ip + ":" + port);
        }
        conf.setIp(ip);
        conf.setPort(port);
        conf.setUserName(username);
        conf.setPassword(password);
        this.connect();
    }

    public synchronized ChannelSftp connect(){
        if(!this.checkConf()){
            log.error("sftp连接失败,连接信息不全");
            int clientFailNum = conf.getClientFailNum() + 1;
            conf.setClientFailNum(clientFailNum);
            throw new RuntimeException("sftp连接失败,连接信息不全");
        }
        return connect(conf.getIp(), conf.getPort(), conf.getUserName(), conf.getPassword());
    }

    /**
     * 连接sftp服务器,连接失败就报异常;
     * @param ip
     * @param port
     * @param username
     * @param password
     * @return
     */
    private synchronized ChannelSftp connect(String ip, int port, String username, String password){
        JSch jsch = new JSch();
        Properties sshConfig = new Properties();

        try {
            sshSession = jsch.getSession(username, ip, port);
            sshSession.setPassword(password);

            sshConfig.put("StrictHostKeyChecking", "no");
            sshSession.setConfig(sshConfig);
            sshSession.connect();

            channel = sshSession.openChannel("sftp");
            channel.connect();
            sftp = (ChannelSftp) channel;
            log.info("sftp服务器连接成功!");
        }catch (Exception e){
            log.error("sftp 连接错误: {}@{}:{}; pwd:{}", username, ip, port, password);
            int num = conf.getClientFailNum() + 1;
            conf.setClientFailNum(num);
            throw new RuntimeException("sftp连接错误");
        }

        return sftp;
    }


    /**
     * 校验连接sftp的4个基本信息是否为空
     * 齐全:true;否则:false;
     */
    private boolean checkConf() {
        if (null != conf) {
            return (conf.getIp() != null &&
                    conf.getPort() != 0 &&
                    conf.getUserName() != null &&
                    conf.getPassword() != null);
        }
        return false;
    }


    @Override
    public synchronized void close() {
        if(null != channel){
            channel.disconnect();
        }
        if(null != sftp){
            sftp.disconnect();
        }
        if(null != sshSession){
            sshSession.disconnect();
        }
    }


    // *********************************下面是文件操作的方法******************************************************
    /**
     * 功能说明:打开指定目录
     */
    public synchronized boolean openDir(String directory) {
        checkClient();
        log.info("打开指定目录 sftp cd {}", directory);

        if (StringUtils.isEmpty(directory)) {
            log.error("sftp 打开目录失败: 目录名不能为空!");
            return false;
        }
        try {
            if (!isDirExist(directory)) {
                sftp.mkdir(directory);
            }
            sftp.cd(directory);
            return true;
        } catch (SftpException e) {
            log.error("sftp 打开目录错误: {}", e.getMessage());
            return false;
        }
    }

    /**
     * 检查连接状态, 连接断开则自动重连,重连失败抛出异常
     */
    public synchronized void checkClient() {
        if (!this.isConnected()) {
            this.connect();
        }
    }

    /**
     * 检查连接状态 true: 连接正常 false: 连接断开
     */
    public synchronized boolean isConnected() {
        if (sftp != null && sftp.isConnected()) {
            return true;
        }
        log.info("SFTP {}@{} 连接被断开", conf.getUserName(), conf.getIp());
        return false;
    }

    /**
     * 判断目录是否存在
     */
    public synchronized boolean isDirExist(String directory) {
        boolean isDirExistFlag = false;
        try {
            SftpATTRS sftpATTRS = sftp.lstat(directory);
            isDirExistFlag = true;
            return sftpATTRS.isDir();
        } catch (Exception e) {
            if ("no such file".equals(e.getMessage().toLowerCase())) {
                isDirExistFlag = false;
            } else {
                this.close();
            }
        }
        return isDirExistFlag;
    }

    /**
     * 通过发送pwd命令模拟心跳, 用于维持长连接
     * 先检查连接状态, 连接断开时进行一次重连
     *
     * @return 连接正常或重连成功返回true <br/>
     * 连接错误且重连失败返回false
     */
    public synchronized boolean heartbeat() {
        checkClient();
        return (null != pwd());
    }

    /**
     * 查看当前所处目录
     */
    public synchronized String pwd() {
        String path = null;
        try {
            path = sftp.pwd();
        } catch (SftpException e) {
            log.error("sftp {} 密码错误:{}", conf.getFtpName(), e.getMessage());
        }
        return path;
    }

    /**
     * 重命名文件或者目录 ,移动文件或者目录
     *
     * @param oldpath 旧文件或目录
     * @param newpath 新文件或目录
     */
    public synchronized boolean rename(String oldpath, String newpath) {
        log.info("sftp 移动文件从旧目录 {}到新目录 {}", oldpath, newpath);
        try {
            sftp.rename(oldpath, newpath);
            return true;
        } catch (Exception e) {
            log.error("SFTP移动文件错误: {}", e.getMessage());
            return false;
        }
    }

    /**
     * 上传文件
     * 上传本地文件
     *
     * @param directory 上传的目录
     * @param fileName  要上传的文件名
     */
    public synchronized boolean upload(String directory, String fileName, InputStream in) throws IOException {
        log.info("sftp upload file {} to {}", fileName, directory);

        if (null == in || null == fileName) {
            log.error("上传文件失败, 缺少重要参数!");
            return false;
        }
        boolean status = false;
        try {
            if (this.openDir(directory)) {
                sftp.put(in, fileName);
                status = true;
            }
        } catch (Exception e) {
            log.error("上传文件错误! {}", e.getMessage());
            this.close();
        } finally {
            in.close();
        }
        return status;
    }


    /**
     * 上传图片
     * 上传base64Str 格式的图片
     *
     * @param base64Str base64编码必填(去掉"data:image/jpeg;base64,"头)
     * @param directory 目录,必填(device=设备图片目录,inspection=巡检图片目录)
     * @param fileName  文件名可以为空(扩展名建议jpg)
     */
    public synchronized String uploadFile(@NotNull String base64Str, String directory, String fileName) {
        if (StringUtils.isBlank(base64Str)) {
            throw new RuntimeException("base64Str不能为空");
        }

        try (InputStream inputStream = Base64Util.baseToInputStream(base64Str)) {

            if (this.openDir(directory)) {
                if (StringUtils.isEmpty(fileName)) {
                    fileName = this.getUUID() + ".jpg";
                }
                // 图片服务器目录:device=设备图片目录,inspection=巡检图片目录,文件名=UUID
                // 目标文件名
                String dst = FileUtil.unite(directory, fileName);
                sftp.put(inputStream, dst, ChannelSftp.OVERWRITE);

            }
        } catch (Exception e) {
            log.error("上传图片错误: {}", e);
            this.close();
        }
        return fileName;
    }

    /**
     * 获取宇宙唯一码.
     *
     * @version Revision 1.0.0
     * @see:
     * @功能说明:
     *
     * @return
     */
    private String getUUID() {
        UUID uuid = UUID.randomUUID();
        String uuidStr = uuid.toString().toUpperCase(Locale.ENGLISH);
        if (StringUtils.isEmpty(uuidStr)) {
            return "";
        }
        return uuidStr;
    }

    /**
     * 下载文件
     *
     * @param directory    下载文件所在路径目录
     * @param downFileName 下载的文件名称
     * @param savePath     保存到本地的路径目录
     */
    public synchronized boolean download(String directory, String downFileName, String savePath) {

        log.info("sftp 下载 {} to {}", FileUtil.unite(directory, downFileName), savePath);
        boolean status = false;
        File file = null;
        FileOutputStream fileOutputStream = null;

        try {
            if (openDir(directory)) {
                if (FileUtil.isExistDir(savePath)) {
                    String saveFile = FileUtil.unite(savePath, downFileName);
                    file = new File(saveFile);
                    fileOutputStream = new FileOutputStream(file);
                    sftp.get(downFileName, fileOutputStream);
                    status = true;
                }
            }
        } catch (Exception e) {
            log.error("sftp 下载 {} 失败: {}", downFileName, e);
            this.close();
        } finally {
            try {
                if (null != fileOutputStream) {
                    fileOutputStream.close();
                }
            } catch (Exception e) {
                log.error("sftp 下载文件, 关闭输出流失败");
                status = false;
            }
        }
        return status;
    }

    /**
     * 下载文件
     *
     * @param directory    下载目录
     * @param downloadFile 下载的文件名称
     * @param localFile    本地文件名称
     * @param saveDir      存在本地的目录
     */
    public synchronized boolean download(String directory, String downloadFile, String localFile, String saveDir)
            throws IOException {

        boolean status = false;
        File file = null;
        FileOutputStream fileOutputStream = null;

        try {
            if (openDir(directory)) {
                File fileDir = new File(saveDir);

                if (isExistsDir(fileDir)) {

                    file = new File(saveDir + localFile);
                    fileOutputStream = new FileOutputStream(file);
                    sftp.get(downloadFile, fileOutputStream);
                    status = true;
                }
            }

        } catch (Exception e) {
            log.error(e.getMessage());
            this.close();
        } finally {
            if (null != fileOutputStream) {
                fileOutputStream.close();
            }
        }

        return status;
    }

    /**
     * 下载文件,下载过程中采用重命名防止被其他程序误处理
     *
     * @param downloadPath  下载目录
     * @param fileName      文件名
     * @param savePath      保存目录
     * @param suffixPattren 下载中文件后缀名
     * @return boolean
     */
    public synchronized boolean downloadAsnFile(String downloadPath, String fileName, String savePath, String suffixPattren) {
        boolean status = true;
        FileOutputStream fileOutputStream = null;
        try {
            File fileDir = new File(savePath);
            if (!fileDir.exists()) {
                fileDir.mkdirs();
            }
            File tempFile = new File(FileUtil.unite(savePath, fileName + suffixPattren));
            fileOutputStream = new FileOutputStream(tempFile);

            sftp.get(fileName, fileOutputStream);
            File file = new File(FileUtil.unite(savePath, fileName));
            tempFile.renameTo(file);
        } catch (Exception e) {
            status = false;
            log.error("下载文件异常,原因:{}", e.getMessage());
        } finally {
            try {
                if (null != fileOutputStream) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                log.error("关闭文件{}出错", fileName);
            }
        }

        return status;
    }

    /**
     * 判断指定文件夹是否存在, 不存在则创建
     *
     * @param file 指定文件夹
     * @author RenZhengGuo 2016年8月13日 下午5:29:37
     */
    private synchronized boolean isExistsDir(File file) {
        boolean mkdir = false;
        // 如果文件夹不存在则创建
        if (!file.exists() && !file.isDirectory()) {
            log.info("目录不存在,创建目录");
            mkdir = file.mkdirs();
        }
        return mkdir;
    }

    /**
     * @param file
     * @author RenZhengGuo 2016年8月13日 下午5:29:37
     */
    public synchronized void cd(String file) {
        // 如果文件夹不存在则创建
        try {
            sftp.cd(file);
        } catch (SftpException e) {
            createDir(file);
//            chmod(Integer.parseInt("777", 8), file);
        }

    }

    /**
     * 创建目录
     *
     * @param file
     * @return
     */
    public synchronized boolean mkdir(String file) {
        if (isDirExist(file)) {
            return true;
        }
        try {
            sftp.mkdir(file);
            return true;
        } catch (SftpException e) {
            log.error("创建文件夹出错!{}", e);
            return false;
        }
    }

    /**
     * 给目录授权
     *
     * @param permsion
     * @param file
     */
    public synchronized void chmod(int permsion, String file) {
        try {
            sftp.chmod(permsion, file);
        } catch (SftpException e) {
            log.info("为文件:{}授权失败!", file);
        }
    }

    /**
     * 创建一个文件目录
     */
    public synchronized void createDir(String createpath) {
        try {
            if (isDirExist(createpath)) {
                this.sftp.cd(createpath);
                return;
            }
            // mkdir 命令不能创建多级目录, 所以要先根据文件分隔符分割成单个目录组
            String[] pathArry = createpath.trim().split(FileUtil.DEF_LINE_SEPARATOR);
            StringBuilder filePath = null;
            // 如果是绝对路径会以"/"开头, 此时分割出的数组首位是空串应替换成"/"
            if (pathArry[0].isEmpty()) {
                filePath = new StringBuilder(FileUtil.DEF_LINE_SEPARATOR);
            } else {
                filePath = new StringBuilder();
            }
            for (String pathNode : pathArry) {
                if (StringUtils.isEmpty(pathNode)) {
                    continue;
                }
                filePath.append(pathNode);
                String path = filePath.toString();

                if (!isDirExist(path)) {
                    sftp.mkdir(path);
                }
                filePath.append(FileUtil.DEF_LINE_SEPARATOR);
            }
            this.sftp.cd(createpath);
        } catch (SftpException e) {
            log.info("创建路径错误:" + createpath);
        }
    }


    /**
     * 判断文件是否存在
     */
    public synchronized boolean fileExist(String directory, String fileName) {
        boolean isDirExistFlag = false;
        try {
            Vector<ChannelSftp.LsEntry> vector = sftp.ls(directory);
            if (vector != null && !vector.isEmpty()) {
                Iterator<ChannelSftp.LsEntry> iterator = vector.iterator();
                while (iterator.hasNext()) {
                    ChannelSftp.LsEntry f = iterator.next();
                    if (f.getAttrs().isDir()) {
                        continue;
                    }
                    if (fileName.equals(f.getFilename())) {
                        return true;
                    }
                }
            }
            return false;
        } catch (Exception e) {
            if ("no such file".equals(e.getMessage().toLowerCase())) {
                isDirExistFlag = false;
            } else {
                this.close();
            }
        }
        return isDirExistFlag;
    }

    /**
     * 删除文件
     * 不能使用全路径删除, 先CD到文件所在目录, 删除完毕后在CD回原目录
     *
     * @param directory  要删除文件所在目录
     * @param deleteFile 要删除的文件
     */
    public synchronized boolean delete(String directory, String deleteFile) {

        checkClient();
        log.info("sftp del {}/{}", directory, deleteFile);
        String pwd = pwd();
        boolean status = false;
        try {

            sftp.cd(directory);
            sftp.rm(deleteFile);
            status = true;
            sftp.cd(pwd);

        } catch (Exception e) {

            log.error("sftp 删除文件错误: {}", e);
            this.close();
        }
        return status;
    }

    /**
     * 删除文件
     * 删除当前目录下的文件
     *
     * @param deleteFile 要删除的文件
     */
    public synchronized boolean delete(String deleteFile) {

        checkClient();
        log.info("sftp del {}", deleteFile);
        boolean status = false;
        try {
            sftp.rm(deleteFile);
            status = true;

        } catch (Exception e) {

            log.error("sftp 删除文件错误: {}", e);
            this.close();
        }
        return status;
    }

    /**
     * 列出指定目录下的文件
     * 失败时返回空list
     *
     * @param directory 要列出的目录
     * @return 文件名列表
     */
    public synchronized List<String> listFiles(String directory) {

        checkClient();
        log.info("sftp ls {}", directory);

        List<String> ftpFileNameList = new ArrayList<>();

        if (!StringUtils.isEmpty(directory)) {
            try {
                Vector<ChannelSftp.LsEntry> sftpFile = sftp.ls(directory);
                for (ChannelSftp.LsEntry item : sftpFile) {
                    ftpFileNameList.add(item.getFilename());
                }
            } catch (SftpException e) {
                log.error("sftp 获取文件列表错误! {}", e.getMessage());
                this.close();
            }
        }
        return ftpFileNameList;
    }

    /**
     * 改变目录用户组
     *
     * @param gid
     * @param path
     * @return boolean
     */
    public synchronized boolean chgrp(Integer gid, String path) {
        try {
            sftp.chgrp(gid, path);
            return true;
        } catch (SftpException e) {
            log.info("改变用户组失败:{}", e.getMessage());
            return false;
        }

    }

    /**
     * 打开指定文件名的文件, 返回InputStream
     * 失败返回 null
     *
     * @param filePath 要打开的文件名
     * @return io流
     */
    public synchronized InputStream openFile(String filePath) {
        log.info("sftp open file {}", filePath);
        InputStream inputStream = null;
        try {
            inputStream = sftp.get(filePath);
            return inputStream;
        } catch (SftpException e) {
            log.error("打开文件错误: {}", e);
            this.close();
            return null;
        }
    }



}

5.sftpファクトリクラスを定義します

package com.demo.sftp.sftp;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/**
 * Sftp连接创建工厂
 */
@Slf4j
public class SftpFactory {

    private Map<String, SftpList> factory;

    private final int DEF_MAP_NUM = 16;
    /**
     * 默认连接数
     * 初始化连接池的时候默认创建的连接数
     */
    private int defClientNum = 1;
    /**
     * 连接池自动增长的维护时间间隔
     */
    private int growDistanceTime = 10;
    /**
     * 连接池自动清理空闲连接的维护时间间隔
     */
    private int strictionDistanceTime = 2;



    public SftpFactory() {
        factory = new HashMap<>(DEF_MAP_NUM);
    }

    public SftpFactory(int defClientNum) {
        if (defClientNum > 0){
            this.defClientNum = defClientNum;
        }
        factory = new HashMap<>(DEF_MAP_NUM);
    }

    /**
     * 通过工厂创建SFTP连接<br/>
     * 如果连接池里已经有同样的连接则直接从连接池里获取<br/>
     * 如果连接池里没有, 则创建新的
     *
     * @param config SFTP连接配置
     * @return SFTP连接对象
     */
    public SftpClient createSftp(SftpConfig config){
        return createSftp(config.getIp(),config.getPort(),config.getUserName(),config.getPassword());
    }

    public SftpClient createSftp(String ip, int port, String username, String password){
        String key = username + "@" + ip + ":" + port;
        SftpConfig conf = new SftpConfig();
        conf.setIp(ip);
        conf.setPort(port);
        conf.setUserName(username);
        conf.setPassword(password);
        conf.setFtpName(key);

        SftpList sftpList = factory.get(key);
        if (null == sftpList){
            sftpList = new SftpList(conf, defClientNum);
            factory.put(key, sftpList);
        }
        else if (sftpList.isEmpty()){
            sftpList.add(conf);
        }
        return sftpList.next();
    }



    private class SftpList{

        private List<SftpClient> sftpClients;

        private SftpClient tempClient;

        private SftpConfig conf;

        /**
         * 最大连接数
         */
        private final int MAX_CLIENT_NUM = 10;

        /**
         * 最小连接数
         */
        private final int MIN_CLIENT_NUM = 1;

        /**
         * 最大错误次数,
         */
        private final int MAX_ERROR_NUM = 3;

        private int index = 0;



        private SftpList(SftpConfig config) {
            this(config, defClientNum);
        }

        /**
         * 创建一个新的连接池
         * @param config sftp 连接配置
         * @param clientNum 指定连接数
         */
        private SftpList(SftpConfig config, int clientNum){

            if (clientNum < MIN_CLIENT_NUM){

                clientNum = MIN_CLIENT_NUM;
            }else if (clientNum > MAX_CLIENT_NUM){

                clientNum = MIN_CLIENT_NUM;
            }
            this.sftpClients = new ArrayList<>(clientNum);
            this.conf = config;
            init();
        }

        /**
         * 在已有连接池增加一个 sftp 连接,
         * 增加后的总连接数不能大于最大连接数
         *
         * @param config 连接配置
         */
        public void add(SftpConfig config){
            if (conf == null || conf.getFtpName() == null){
                conf = config;
            }
            add();
        }

        private void add(){
            if (size() < MAX_CLIENT_NUM){
                SftpClient sftp = new SftpClient(conf);
                sftpClients.add(sftp);
            }
        }

        /**
         * 初始化连接池
         * 如果连接池是空的则自动按默认连接数增加连接
         */
        public void init(){
            int size = sftpClients.size();

            if (size <= 0){
                size = defClientNum;
            }

            for (int i = 0; i < size; i++) {
                SftpClient sftp = new SftpClient(conf);
                sftp.setLastUseDate(System.currentTimeMillis());
                sftpClients.add(sftp);
            }
            this.heartbeat();
        }


        /**
         * 取出一个sftp连接
         * @return sftp连接
         */
        private synchronized SftpClient next(){
            if ((++index) >= size()){
                index = 0;
            }
            SftpClient sftp = sftpClients.get(index);
            if (sftp == null || !sftp.isConnected()){
                log.warn("SFTP---{}({}) 空连接尝试重连: {}", index, size(), conf.getFtpName());
                sftp = new SftpClient(conf);
                sftpClients.set(index, sftp);
            }
            sftp.setLastUseDate(System.currentTimeMillis());
            log.info("获取 SFTP-{}({}): {}", index, size(), conf.getFtpName());
            return sftp;
        }


        /**
         * 获取一条早期使用的 sftp 连接
         * @return sftp 连接
         */
        private SftpClient early(){
            int i = index + 2;
            if (i == sftpClients.size()){
                i = 0;
            }
            return sftpClients.get(i);
        }

        private void updateConf(SftpConfig config){
            if (null != config){
                conf.setIp(config.getIp());
                conf.setPort(config.getPort());
                conf.setUserName(config.getUserName());
                conf.setPassword(config.getPassword());
            }
            for (SftpClient ftp : sftpClients){
                ftp.setConf(conf);
            }
            log.info("更新 SFTP 连接: {}", conf.getFtpName());
        }

        /**
         * 删除最后一个连接
         * 为避免关闭使用中的连接, 先将最后一个连接放到缓存中, 等下一次检查连接时再删除
         */
        private void removeLast(){
            int lastIndex = sftpClients.size() - 1;
            if (this.tempClient != null){
                this.tempClient.close();
            }
            tempClient = sftpClients.get(lastIndex);
            sftpClients.remove(lastIndex);
            log.info("删除最后一条 SFTP 连接: {}", conf.getFtpName());
        }

        /**
         * 删除一个指定的连接, 删除的连接会直接关闭
         * @param index 需要删除的连接下标
         */
        private void removeSftp(int index){
            if (sftpClients.size() > index){
                SftpClient ftp = sftpClients.get(index);
                if (ftp != null){
                    ftp.close();
                }
                sftpClients.remove(index);
                log.info("删除 SFTP-{}({}) 连接: {}", index, size(), conf.getFtpName());
            }
        }

        private void removeAll(){
            for (SftpClient ftp : sftpClients){
                ftp.close();
            }
            sftpClients.clear();
            log.info("清除 SFTP 连接池: {}", conf.getFtpName());
        }


        private final long DELAY = 10L;

        private final long LONG_DISTANCE = TimeUnit.MINUTES.toMillis(growDistanceTime);

        private final long SHORT_DISTANCE = TimeUnit.MINUTES.toMillis(strictionDistanceTime);

        /**
         * 定时器, 定时检查连接状态;<be/>
         * 超过15分钟没有调用SFTP连接, 则关闭最后一个连接;<be/>
         * 总连接数等于最小连接数(MIN_CLIENT_NUM)时不再关闭连接;<be/>
         *
         * 所有SFTP连接的最后一次调用时间间隔小于2分钟时, 说明使用比较频繁;<be/>
         * SFTP连接调用频繁时, 自动增加一个新连接;<be/>
         * 总连接数等于最大连接数(MAX_CLIENT_NUM)时, 不再增加连接;<be/>
         */
        public void heartbeat(){
            ThreadFactory threadFactory = new BasicThreadFactory.Builder()
                    .namingPattern("sftp-heartbeat-%d").daemon(false).build();
            ScheduledExecutorService executorService =
                    new ScheduledThreadPoolExecutor(1, threadFactory);

            executorService.scheduleWithFixedDelay(()->{
                // 关闭临时连接
                if (tempClient != null){
                    tempClient.close();
                    tempClient = null;
                }
                // sftp 连接心跳
                for (SftpClient f : sftpClients){
                    try{
                        f.heartbeat();
                    }catch(Exception e){
                        log.error("sftp心跳错误");
                        removeAll();
                    }
                }
                // 总连接数大于最小连接数时, 判断最后一次使用的连接距当前的使用间隔
                // 大于10分钟未使用的移到临时连接, 等待下一次自动关闭
                if (sftpClients.size() > MIN_CLIENT_NUM){
                    long nowDate = System.currentTimeMillis();
                    long da = nowDate - early().getLastUseDate();
                    if (da > LONG_DISTANCE){
                        removeLast();
                        log.info("清理空闲连接: {}", conf.getFtpName());
                    }
                }
                // 总连接数小于最大连接数时, 轮询所有连接
                // 如果使用间隔小于2分钟, 自动增加一个连接
                if (sftpClients.size() < MAX_CLIENT_NUM && checkClient()){
                    add();
                }

            }, DELAY, DELAY, TimeUnit.MINUTES);

        }

        private boolean checkClient(){
            long nowDate = System.currentTimeMillis();
            for (SftpClient ftp: sftpClients){
                long da = nowDate - ftp.getLastUseDate();
                if (da > SHORT_DISTANCE){
                    return false;
                }
            }
            log.info("增加连接: {}", conf.getFtpName());
            return true;
        }

        public boolean isEmpty(){
            return sftpClients.isEmpty();
        }

        public int size(){
            return sftpClients.size();
        }

    }

}

6.SftpFactoryをスタートアップクラスに追加します。

package com.demo.sftp;

import com.demo.sftp.sftp.SftpFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@SpringBootApplication
@EnableSwagger2
@EnableAsync // 开启spring异步注解
public class SftpApplication {

    public static void main(String[] args) {
        SpringApplication.run(SftpApplication.class,args);
    }

    @Bean
    public SftpFactory sftpFactory(){
        return new SftpFactory();
    }

}

7.テストメソッドを作成します

怠惰すぎるため、このメソッドのsftp接続情報は直接書き込まれます。実際のプロジェクトでは、動的取得用に構成ファイルまたはデータベースでsftp接続情報を構成して、sftp接続情報が変更されたときに実行することをお勧めします。 、sftpクライアントに接続するためのコードの変更を回避できます。

package com.demo.sftp.controller;

import com.demo.sftp.dto.Result;
import com.demo.sftp.sftp.FileUtil;
import com.demo.sftp.sftp.SftpClient;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@Slf4j
@Api("sftp学习接口")
@RestController
@RequestMapping(value = "/sftp")
public class TestController {

    @ApiOperation(value = "测试接口",notes = "测试接口")
    @PostMapping("/test")
    Result test(){
        log.info("请求正常");
        this.uploadFile();
        return Result.ok("success");
    }

    /**
     * 模拟上传文件到sftp服务器:
     * 代码生成数据,写入文件并上传到sftp服务器;
     */
    private void uploadFile(){
        // 因为该模块代码对外公开,所以此处使用代码的符合表示实际的连接信息。
        String ip = "ip";
        String port = "port";
        String userName = "username";
        String passWord = "passWord";
        SftpClient sftp = new SftpClient(null, ip, Integer.parseInt(port), userName, passWord);

        log.info("sftp客户端创建成功");
        String content = this.getContent();
        ByteArrayInputStream bakInputStream = new ByteArrayInputStream(content.getBytes());
        ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes());

        log.info("开始上传文件");
        String bakPath = "/upload/bakPath";
        String storePath = "/upload/storePath";
        String fileName = "YSL_" + 8888 + "_test" + System.currentTimeMillis();
        String tempFileName = "~" + fileName;
        boolean b = this.uploadFile(sftp, bakPath, storePath,
                fileName, tempFileName, bakInputStream, inputStream);
        log.info("文件上传结束");

        if(!b){
            log.info("文件上传失败了");
            return;
        }
        sftp.close();
    }

    /**
     * 文件内容封装
     */
    private String getContent(){
        StringBuilder outStr = new StringBuilder("STA|1"+"\r\n");
        outStr.append("数据编号,企业ID,企业名称,操作类型,账单类型,产品类型,产品唯一识别编码," +
                "账目项1,账目项1产品单价,账目项1产品个数,账目项2,账目项2产品单价,账目项2产品个数"+"\r\n");

        outStr.append(1+",")
                .append(1112233+",")// 数据编号
                .append(31803+",") // 企业ID
                .append(3+",") // 操作类型
                .append(2 + ",") // 账单类型
                .append(40+",") // 产品类型编号
                .append("SV3333333"+",") // 产品唯一识别编码
                .append("zmx111222"+",") // 账目项1
                .append(233+",") // 账目项1产品单价
                .append(1+",") // 账目项1产品个数
                .append("zmx111222"+",") // 账目项2
                .append(33+",") // 账目项2产品单价
                .append(1+",") // 账目项2产品个数
                .append("\r\n")
                .append("end");
        return outStr.toString();
    }
    /**
     * 上传文件
     */
    public static boolean uploadFile(SftpClient sftp, String bakPath, String storePath,
                                     String fileName, String tempFileName,
                                     InputStream bakInput, InputStream input) {
        try {
            //上传至备份目录
            log.info("开始上传文件fileName==={}",fileName);
            sftp.upload(bakPath, fileName, bakInput); // 文件上传到备份文件夹
            //上传临时文件至下发目录
            boolean b = sftp.upload(storePath, tempFileName, input);
            // 临时文件上传成功,修改临时文件名为正式名
            if (b) {
                sftp.rename(FileUtil.unite(storePath, tempFileName), FileUtil.unite(storePath, fileName));
            }
            return true;
        } catch (IOException e) {
            log.error("上传文件{}出错", fileName);
            log.error(e.getLocalizedMessage());
            return false;
        }
    }
}

 

8.ファイルが生成されるかどうかを確認するためのSwaggerインターフェイス呼び出しテスト

対応するsftpディレクトリの下にディレクトリが実際に正常に作成されているかどうかを確認します。使用するアカウントに現在のディレクトリにディレクトリを作成する権限がない場合、プログラムを使用してファイルをアップロードする前に、対応するファイルディレクトリを手動で作成する必要があります。

フォルダ内のファイルも正常にアップロードされます

これまでのところ、sftpアップロードファイルのデモンストレーションは終了しています。実際の本番環境では、異なるアカウントに対応するファイルサーバーが多数存在する可能性があります。この情報をシステム情報データベーステーブルに書き込んで、動的な形式で構成情報を取得することをお勧めします。結局のところ、多くのsftpがあります。構成ファイル内のサーバー情報の構成は、依然として非常に冗長であるように見えます。

 

 

 

 

おすすめ

転載: blog.csdn.net/qq_37488998/article/details/111601132