♉前言

在工作过程中遇到了文件上传和文件线上解压等场景,于是决定使用Jsch的SFTP,最开始使用的是一个单纯的SFTP工具类,但是在使用过程发现其是不支持多线程的,于是将其改造成了多线程版本,使用一段时间后,发现文件上传等接口的时间有点长,于是使用XRebel进行排查问题,发现在创建Sftp和shell连接耗费的时间有点长,于是决定将其适配连接池

♉代码

以下代码使用Springboot框架作为代码环境,代码中有一些自定义异常类我没放上来,如果有需要可以自定义,或者将使用了的地方替换成RunTimeException

🐕Maven配置

pom.xml

		<!-- lombok -->
		<dependency>
				<groupId>org.projectlombok</groupId>
				<artifactId>lombok</artifactId>
				<optional>true</optional>
		</dependency>
		 <!-- Jsch -->
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.55</version>
        </dependency>
        <!--超级nb的一个工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.6.4</version>
        </dependency>
		<!--自定义连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.1</version>
        </dependency>

🐺SpringBoot配置文件配置

application.yml

配置文件中连接池相关配置

upload-sftp:
  host: 192.168.11.112
  port: 22
  username: root
  password: 123456
  system: linux
  path: /home/upload
  sftp-pool:
    max-total: 20
    max-idle: 20
    min-idle: 10
  shell-pool:
    max-total: 10
    max-idle: 10
    min-idle: 5

🦁配置相关类

FileChannelConfig

import com.pzx.demo.common.sftp.core.factory.SftpFactory;
import com.pzx.demo.common.sftp.core.factory.ShellFactory;
import com.pzx.demo.common.sftp.core.helper.SftpHelper;
import com.pzx.demo.common.sftp.core.helper.ShellHelper;
import com.pzx.demo.common.sftp.core.pool.SftpPool;
import com.pzx.demo.common.sftp.core.pool.ShellPool;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author JunPzx
 * @version v1.0.0
 * @date 2021/8/3 15:10
 */
@Configuration
@EnableConfigurationProperties(FileChannelProperties.class)
public class FileChannelConfig {
    @Bean
    public SftpFactory sftpFactory(FileChannelProperties properties) {
        return new SftpFactory(properties);
    }

    @Bean
    public SftpPool sftpPool(SftpFactory sftpFactory) {
        final SftpPool sftpPool = new SftpPool(sftpFactory);
        try {
            // 初始化连接池
            sftpPool.init();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sftpPool;
    }

    @Bean
    public SftpHelper sftpHelper(SftpPool sftpPool) {
        return new SftpHelper(sftpPool);
    }

    @Bean
    public ShellFactory shellFactory(FileChannelProperties properties) {
        return new ShellFactory(properties);
    }

    @Bean
    public ShellPool shellPool(ShellFactory shellFactory) {
        final ShellPool shellPool = new ShellPool(shellFactory);
        try {
            // 初始化连接池
            shellPool.init();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return shellPool;
    }

    @Bean
    public ShellHelper shellHelper(ShellPool shellPool) {
        return new ShellHelper(shellPool);
    }

FileChannelProperties

import cn.hutool.core.util.StrUtil;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelShell;
import lombok.Data;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author JunPzx
 * @version v1.0.0
 * @date 2021/8/4 14:11
 */
@Data
@ConfigurationProperties(prefix = "upload-sftp")
public class FileChannelProperties {
    private static final String LINUX = "linux";
    private static final String WINDOWS = "windows";
    /**
     * 文件保存根路径
     */
    private static String path;
    /**
     * 使用的系统,默认使用linux
     */
    private static String system = LINUX;

    /**
     * 主机
     */
    private static String host;

    /**
     * 端口
     */
    private static int port = 22;

    /**
     * 账号
     */
    private static String username = "root";

    /**
     * 密码
     */
    private static String password = "root";

    private SftpPool sftpPool = new SftpPool();

    private ShellPool shellPool = new ShellPool();

    public static String getPath() {
        if (StrUtil.isBlank(system) || LINUX.equalsIgnoreCase(system)) {
            return StrUtil.isNotBlank(path) ? path : "/";
        }
        return StrUtil.isNotBlank(path) ? path : "C:/";
    }

    public void setPath(String path) {
        FileChannelProperties.path = path;
    }

    public static String getHost() {
        return host;
    }

    public void setHost(String host) {
        FileChannelProperties.host = host;
    }

    public static int getPort() {
        return port;
    }

    public void setPort(int port) {
        FileChannelProperties.port = port;
    }

    public static String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        FileChannelProperties.username = username;
    }

    public static String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        FileChannelProperties.password = password;
    }

    public void setSystem(String system) {
        FileChannelProperties.system = system;
    }

    public static class SftpPool extends GenericObjectPoolConfig<ChannelSftp> {

        /**
         * 最大连接数
         */
        private int maxTotal = DEFAULT_MAX_TOTAL;

        /**
         * 最大空闲数
         */
        private int maxIdle = DEFAULT_MAX_IDLE;

        /**
         * 最小空闲数
         */
        private int minIdle = DEFAULT_MIN_IDLE;

        public SftpPool() {
            super();
        }

        @Override
        public int getMaxTotal() {
            return maxTotal;
        }

        @Override
        public void setMaxTotal(int maxTotal) {
            this.maxTotal = maxTotal;
        }

        @Override
        public int getMaxIdle() {
            return maxIdle;
        }

        @Override
        public void setMaxIdle(int maxIdle) {
            this.maxIdle = maxIdle;
        }

        @Override
        public int getMinIdle() {
            return minIdle;
        }

        @Override
        public void setMinIdle(int minIdle) {
            this.minIdle = minIdle;
        }
    }

    public static class ShellPool extends GenericObjectPoolConfig<ChannelShell> {
        /**
         * 最大连接数
         */
        private int maxTotal = DEFAULT_MAX_TOTAL;

        /**
         * 最大空闲数
         */
        private int maxIdle = DEFAULT_MAX_IDLE;

        /**
         * 最小空闲数
         */
        private int minIdle = DEFAULT_MIN_IDLE;

        public ShellPool() {
            super();
        }

        @Override
        public int getMaxTotal() {
            return maxTotal;
        }

        @Override
        public void setMaxTotal(int maxTotal) {
            this.maxTotal = maxTotal;
        }

        @Override
        public int getMaxIdle() {
            return maxIdle;
        }

        @Override
        public void setMaxIdle(int maxIdle) {
            this.maxIdle = maxIdle;
        }

        @Override
        public int getMinIdle() {
            return minIdle;
        }

        @Override
        public void setMinIdle(int minIdle) {
            this.minIdle = minIdle;
        }

    }
}

🐅工厂相关类

SftpFactory

import com.jcraft.jsch.*;
import com.pzx.demo.common.sftp.core.config.FileChannelProperties;
import com.pzx.demo.core.exception.ApplicationException;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;

import java.util.Properties;

/**
 * Sftp工厂
 *
 * @author JunPzx
 * @version v1.0.0
 * @date 2021/8/4 14:15
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class SftpFactory extends BasePooledObjectFactory<ChannelSftp> {

    private FileChannelProperties properties;

    public SftpFactory(FileChannelProperties fileChannelProperties) {
        this.properties = fileChannelProperties;
    }

    @Override
    public ChannelSftp create() throws Exception {
        try {
            JSch jsch = new JSch();
            Session sshSession = jsch.getSession(FileChannelProperties.getUsername(), FileChannelProperties.getHost(), FileChannelProperties.getPort());
            sshSession.setPassword(FileChannelProperties.getPassword());
            Properties sshConfig = new Properties();
            sshConfig.put("StrictHostKeyChecking", "no");
            sshSession.setConfig(sshConfig);
            sshSession.connect();
            ChannelSftp channel = (ChannelSftp) sshSession.openChannel("sftp");
            channel.connect();
            return channel;
        } catch (JSchException e) {
            throw new ApplicationException("连接SFTP失败");
        }
    }

    @Override
    public PooledObject<ChannelSftp> wrap(ChannelSftp channelSftp) {
        return new DefaultPooledObject<>(channelSftp);
    }

    @Override
    public void destroyObject(PooledObject<ChannelSftp> p) {
        ChannelSftp channelSftp = p.getObject();
        channelSftp.disconnect();
    }

    @Override
    public boolean validateObject(final PooledObject<ChannelSftp> p) {
        final ChannelSftp channelSftp = p.getObject();
        try {
            if (channelSftp.isClosed()) {
                return false;
            }
            channelSftp.cd("/");
        } catch (SftpException e) {
            return false;
        }
        return true;
    }
}

ShellFactory

import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.pzx.demo.common.sftp.core.config.FileChannelProperties;
import com.pzx.demo.core.exception.ApplicationException;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;

import java.io.*;
import java.util.Properties;

/**
 * 命令执行器工厂
 *
 * @author JunPzx
 * @version v1.0.0
 * @date 2021/8/4 14:15
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class ShellFactory extends BasePooledObjectFactory<ChannelShell> {

    private FileChannelProperties properties;

    public ShellFactory(FileChannelProperties fileChannelProperties) {
        this.properties = fileChannelProperties;
    }

    @Override
    public ChannelShell create() throws Exception {
        try {
            JSch jsch = new JSch();
            Session sshSession = jsch.getSession(FileChannelProperties.getUsername(), FileChannelProperties.getHost(), FileChannelProperties.getPort());
            sshSession.setPassword(FileChannelProperties.getPassword());
            Properties sshConfig = new Properties();
            sshConfig.put("StrictHostKeyChecking", "no");
            sshSession.setConfig(sshConfig);
            sshSession.connect();
            ChannelShell shell = (ChannelShell) sshSession.openChannel("shell");
            shell.connect();
            return shell;
        } catch (JSchException e) {
            throw new ApplicationException("连接Shell失败");
        }
    }

    @Override
    public PooledObject<ChannelShell> wrap(ChannelShell channelShell) {
        return new DefaultPooledObject<>(channelShell);
    }

    @Override
    public void destroyObject(PooledObject<ChannelShell> p) {
        ChannelShell channelShell = p.getObject();
        channelShell.disconnect();
    }

    @Override
    public boolean validateObject(final PooledObject<ChannelShell> p) {
        final ChannelShell channelShell = p.getObject();
        try {
            if (channelShell.isClosed()) {
                return false;
            }
            final OutputStream outputStream = channelShell.getOutputStream();
            final InputStream inputStream = channelShell.getInputStream();
            PrintWriter printWriter = new PrintWriter(outputStream);
            String commandFlag = ("echo $?");
            printWriter.println("cd /");
            printWriter.println(commandFlag);
            printWriter.flush();
            BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
            String msg;
            boolean flag = false;
            int count = 0;
            while ((msg = in.readLine()) != null) {
                if (!flag) {
                    flag = msg.contains(commandFlag);
                } else {
                    return "0".equals(msg) || "1".equals(msg);
                }
				// 这个地方代码的必要性不是很强,只要配置了空闲连接检测,这段代码可以删掉,但是尽量还是留着,防止意外
                if (!flag) {
                    count++;
                    if (count > 20) {
                        return false;
                    }
                }
            }
            return true;
        } catch (IOException e) {
            return false;
        }
    }
}

🐆相关使用工具类

SftpHelper

import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.ArrayUtil;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.SftpException;
import com.pzx.demo.common.sftp.core.config.FileChannelProperties;
import com.pzx.demo.common.sftp.core.pool.SftpPool;
import com.pzx.demo.core.exception.ApplicationSftpException;
import lombok.extern.log4j.Log4j2;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.stream.Collectors;

/**
 * Sftp工具类
 *
 * @author 2021/8/3
 * @author Junpzx
 */
@Log4j2
public class SftpHelper {

    /**
     * 每个目录下最大子文件(夹)数量
     */
    private static final int MAX_CHILD_FILE_NUMBER = 1000;
    private static final String NODE_SEPARATOR = "/";
    private final static String PATH = FileChannelProperties.getPath();
    private SftpPool pool;

    public SftpHelper(SftpPool pool) {
        this.pool = pool;
    }

    /**
     * 附件上传
     *
     * @param fileName    文件名
     * @param inputStream 文件流
     * @return 上传后的文件完整路径
     */
    public String upload(String fileName, InputStream inputStream) {
        return upload(null, fileName, inputStream);
    }

    /**
     * 文件上传
     *
     * @param relativePath 文件保存的相对路径(最后一级目录)
     * @param fileName     文件名
     * @param inputStream  文件流
     * @return 上传后的文件完整路径
     */
    public String upload(String relativePath, String fileName, InputStream inputStream) {
        ChannelSftp sftp = pool.borrowObject();
        try {
            String filePath = PATH;
            if (relativePath != null && !relativePath.trim().isEmpty()) {
                filePath = PATH + relativePath;
            }
            if (!dirIsExist(filePath)) {
                filePath = generateValidPath(filePath, sftp);
            }
            filePath = filePath.concat(fileName);
            sftp.put(inputStream, filePath);
            sftp.chmod(Integer.parseInt("777", 8), filePath);
            return filePath;
        } catch (SftpException e) {
            throw new ApplicationSftpException("SFTP上传文件出错", e);
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 文件下载
     *
     * @param fileUrl 文件路径
     * @return 文件字节数组
     */
    public byte[] download(String fileUrl) {
        ChannelSftp sftp = pool.borrowObject();
        try {
            InputStream inputStream = sftp.get(fileUrl);
            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();
        } catch (IOException | SftpException e) {
            throw new ApplicationSftpException("SFTP下载文件出错", e);
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 创建目录(只能创建一级目录,如果需要创建多级目录,需要调用mkdirs方法)
     *
     * @param path 目录路径
     */
    private void createFolder(String path) {
        ChannelSftp sftp = pool.borrowObject();
        try {
            sftp.mkdir(path);
        } catch (SftpException e) {
            throw new ApplicationSftpException("SFTP创建文件夹出错", e);
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 文件读取
     *
     * @param fileUrl 文件路径
     * @return 文件字节数组
     */
    public String read(String fileUrl) {
        ChannelSftp sftp = pool.borrowObject();
        try {
            InputStream inputStream = sftp.get(fileUrl);
            BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
            String str, resultStr = "";
            while ((str = in.readLine()) != null) {
                resultStr = resultStr.concat(str);
            }
            return resultStr;
        } catch (SftpException | IOException e) {
            throw new ApplicationSftpException("SFTP读取文件出错", e);
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 判断目录是否存在
     *
     * @param url 文件夹目录
     * @return ture:存在;false:不存在
     */
    public boolean dirIsExist(String url) {
        ChannelSftp sftp = pool.borrowObject();
        try {
            if (isDirectory(url)) {
                sftp.cd(url);
                String pwd = sftp.pwd();
                return pwd.equals(url) || pwd.concat("/").equals(url);
            }
            return false;
        } catch (SftpException e) {
            throw new ApplicationSftpException("SFTP读取文件夹出错", e);
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 删除文件 或 删除文件夹
     * 注: 如果是文件夹, 不论该文件夹中有无内容,都能删除, 因此:此方法慎用
     *
     * @param remoteDirOrRemoteFile 要删除的文件  或 文件夹
     */
    public void delete(String remoteDirOrRemoteFile) {
        ChannelSftp sftp = pool.borrowObject();
        try {
            List<String> targetFileOrDirContainer = new ArrayList<>(8);
            targetFileOrDirContainer.add(remoteDirOrRemoteFile);
            List<String> toBeDeletedEmptyDirContainer = new ArrayList<>(8);
            if (isDirectory(remoteDirOrRemoteFile)) {
                toBeDeletedEmptyDirContainer.add(remoteDirOrRemoteFile);
            }
            collectToBeDeletedEmptyDir(toBeDeletedEmptyDirContainer, targetFileOrDirContainer);
            if (!toBeDeletedEmptyDirContainer.isEmpty()) {
                String targetDir;
                for (int i = toBeDeletedEmptyDirContainer.size() - 1; i >= 0; i--) {
                    targetDir = toBeDeletedEmptyDirContainer.get(i);
                    sftp.rmdir(targetDir);
                }
            }
        } catch (SftpException e) {
            throw new ApplicationSftpException("SFTP删除文件或者文件夹出错", e);
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 删除相关文件 并 采集所有 需要被删除的 文件夹
     * <p>
     * 注: 如果是文件夹, 不论该文件夹中有无内容,都能删除, 因此:此方法慎用
     *
     * @param toBeDeletedEmptyDirContainer 所有待删除的空文件夹集合
     * @param targetFileOrDirContainer     本次, 要删除的文件的集合   或   本次, 要删除的文件所在文件夹的集合
     */
    private void collectToBeDeletedEmptyDir(List<String> toBeDeletedEmptyDirContainer,
                                            List<String> targetFileOrDirContainer) {
        List<String> todoCallDirContainer = new ArrayList<>(8);
        List<String> subfolderList;
        for (String remoteDirOrRemoteFile : targetFileOrDirContainer) {
            subfolderList = fileDeleteExecutor(remoteDirOrRemoteFile);
            toBeDeletedEmptyDirContainer.addAll(subfolderList);
            todoCallDirContainer.addAll(subfolderList);
        }
        if (!todoCallDirContainer.isEmpty()) {
            collectToBeDeletedEmptyDir(toBeDeletedEmptyDirContainer, todoCallDirContainer);
        }
    }

    /**
     * 删除remoteDirOrRemoteFile指向的文件 或 删除remoteDirOrRemoteFile指向的文件夹下的所有子级文件
     * 注: 如果是文件夹, 只会删除该文件夹下的子级文件;不会删除该文件夹下的孙子级文件(如果有孙子级文件的话)
     *
     * @param remoteDirOrRemoteFile 要删除的文件 或 要 文件夹   【绝对路径】
     * @return remoteDirOrRemoteFile指向的文件夹 下的 文件夹集合
     * 注: 如果remoteDirOrRemoteFile指向的是文件的话,返回空的集合
     * 注: 只会包含子级文件夹,不包含孙子级文件夹(如果有孙子级文件夹的话)
     */
    private List<String> fileDeleteExecutor(String remoteDirOrRemoteFile) {
        ChannelSftp sftp = pool.borrowObject();
        try {
            List<String> subfolderList = new ArrayList<>(8);
            // 如果是文件,直接删除
            if (!isDirectory(remoteDirOrRemoteFile)) {
                sftp.rm(remoteDirOrRemoteFile);
                return subfolderList;
            }
            // 保证 remoteDirOrRemoteFile 以 “/” 开头,以 “/” 结尾
            remoteDirOrRemoteFile = handlePath(remoteDirOrRemoteFile, true, true);
            Vector<?> vector = sftp.ls(remoteDirOrRemoteFile);
            String fileName;
            String sftpAbsoluteFilename;
            // 列出文件名
            for (Object item : vector) {
                ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) item;
                fileName = entry.getFilename();
                if (invalidFileName(fileName)) {
                    continue;
                }
                sftpAbsoluteFilename = remoteDirOrRemoteFile + fileName;
                // 如果是文件,直接删除
                if (!isDirectory(sftpAbsoluteFilename)) {
                    sftp.rm(sftpAbsoluteFilename);
                    continue;
                }
                subfolderList.add(sftpAbsoluteFilename);
            }
            return subfolderList;
        } catch (SftpException e) {
            throw new ApplicationSftpException("SFTP删除文件或者文件夹出错", e);
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 从给定路径中截取文件名
     *
     * @param path 路径,  如: /files/abc/info.yml
     * @return 文件名, 如: info.yml
     */
    private String getFilenameFromPath(String path) {
        return path.substring(path.lastIndexOf(NODE_SEPARATOR) + 1);
    }

    /**
     * 路径处理器
     * <p>
     * 根据参数控制处理类型,如:
     * 当: originPath 为【var/apps】时,
     * 当: handleHead 为 true, 处理结果为【/var/apps】
     * 当: handleTail 为 true, 处理结果为【var/apps/】
     * 当: handleHead 和 handleTail 均为 true, 处理结果为【/var/apps/】
     *
     * @param originPath 要处理的路径
     * @param handleHead 处理 起始处
     * @param handleTail 处理 结尾处
     * @return 处理后的路径
     */
    private String handlePath(String originPath, boolean handleHead, boolean handleTail) {
        if (originPath == null || "".equals(originPath.trim())) {
            return NODE_SEPARATOR;
        }
        if (handleHead && !originPath.startsWith(NODE_SEPARATOR)) {
            originPath = NODE_SEPARATOR.concat(originPath);
        }
        if (handleTail && !originPath.endsWith(NODE_SEPARATOR)) {
            originPath = originPath.concat(NODE_SEPARATOR);
        }
        return originPath;
    }

    /**
     * 判断是否为无效的文件名
     * 注:文件名(夹)名为【.】或【..】时,是无效的
     *
     * @param fileName 文件名
     * @return 是有无效
     */
    private boolean invalidFileName(String fileName) {
        return ".".equals(fileName) || "..".equals(fileName);
    }

    /**
     * 判断SFTP上的path是否为文件夹
     * 注:如果该路径不存在,那么会返回false
     *
     * @param path SFTP上的路径
     * @return 判断结果
     */
    private boolean isDirectory(String path) {
        ChannelSftp sftp = pool.borrowObject();
        // 合法的错误id
        // int legalErrorId = 4;
        try {
            sftp.cd(path);
            return true;
        } catch (SftpException e) {
            // 如果 path不存在,那么报错信息为【No such file】,错误id为【2】
            // 如果 path存在,但是不能cd进去,那么报错信息形如【Can't change directory: /files/sqljdbc4-3.0.jar】,错误id为【4】
            return false;
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 创建多级文件目录
     *
     * @param dirs     每个目录的名称数组
     * @param tempPath 临时路径,传入""空字符串,主要为了递归调用方便
     * @param length   数组长度
     * @param index    当前索引,为了递归调用
     */
    private void mkdirs(ChannelSftp sftp, String[] dirs, String tempPath, int length, int index) {
        // 以"/a/b/c/d"为例按"/"分隔后,第0位是"";顾下标从1开始
        index++;
        if (index < length) {
            // 目录不存在,则创建文件夹
            tempPath += "/" + dirs[index];
        }
        try {
            sftp.cd(tempPath);
            if (index < length) {
                mkdirs(sftp, dirs, tempPath, length, index);
            }
        } catch (SftpException ex) {
            try {
                sftp.mkdir(tempPath);
                sftp.chmod(Integer.parseInt("777", 8), tempPath);
                sftp.cd(tempPath);
            } catch (SftpException e) {
                return;
            }
            mkdirs(sftp, dirs, tempPath, length, index);
        }
    }

    /**
     * 统计目录下文件(夹)数量
     *
     * @param path 目录路径
     * @return 文件数量
     */
    private int countFiles(String path) throws SftpException {
        ChannelSftp sftp = pool.borrowObject();
        try {
            sftp.cd(path);
            return sftp.ls(path).size();
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 获取某个文件夹下的所有文件名称
     *
     * @param path      文件夹路径
     * @param fileTypes 文件类型,如果为null或者长度为0,则获取所有文件名称,如果已指定,则获取指定类型的文件类型
     * @return 文件名称
     */
    public List<String> queryFileName(String path, String... fileTypes) {
        ChannelSftp sftp = pool.borrowObject();
        try {
            Vector<ChannelSftp.LsEntry> ls = sftp.ls(path);
            return ls.stream().map(ChannelSftp.LsEntry::getFilename).filter(
                    name -> {
                        if (ArrayUtil.isNotEmpty(fileTypes)) {
                            return FileNameUtil.isType(name, fileTypes);
                        }
                        return true;
                    }).collect(Collectors.toList());
        } catch (SftpException e) {
            throw new ApplicationSftpException("SFTP获取某个文件夹下的所有文件名称出错", e);
        } finally {
            pool.returnObject(sftp);
        }
    }

    /**
     * 校验路径是否可用
     *
     * @param path 路径
     * @return 是否可用
     */
    private boolean validatePathValid(String path, ChannelSftp sftp) {
        int countFiles = 0;
        try {
            countFiles = countFiles(path);
        } catch (SftpException e) {
            mkdirs(sftp, path.split("/"), "", path.split("/").length, 0);
        }
        return countFiles <= MAX_CHILD_FILE_NUMBER;
    }

    /**
     * 生成有效路径
     *
     * @param path 参数路径
     * @return 解析后的有效路径
     */
    private String generateValidPath(String path, ChannelSftp sftp) {
        if (validatePathValid(path, sftp)) {
            return path;
        } else {
            String newPath = path + String.valueOf(System.currentTimeMillis()).substring(9);
            mkdirs(sftp, newPath.split("/"), "", newPath.split("/").length, 0);
            return newPath;
        }
    }
}

ShellHelper

import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.StrUtil;
import com.jcraft.jsch.ChannelShell;
import com.pzx.demo.common.sftp.core.pool.ShellPool;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.HashMap;
import java.util.Map;

/**
 * @author JunPzx
 * @version v1.0.0
 * @date 2021/8/5 14:13
 */
@Log4j2
public class ShellHelper {

    private ShellPool pool;

    public ShellHelper(ShellPool shellPool) {
        this.pool = shellPool;
    }


    /**
     * 远程解压缩指定目录下的指定名字的文件
     *
     * @param path:指定解压文件的目录
     * @param fileName:需要解压的文件名字
     * @param targetPath         :解压完成后的存放路径
     */
    public boolean remoteZipToFile(String path, String fileName, String targetPath) {
        ChannelShell channelShell = pool.borrowObject();
        try {
            final OutputStream outputStream = channelShell.getOutputStream();
            final InputStream inputStream = channelShell.getInputStream();
            PrintWriter printWriter = new PrintWriter(outputStream);
            String fileSuffix = FileNameUtil.getSuffix(fileName);
            String command;
            switch (fileSuffix) {
                case "gz":
                    command = "tar -zxvf " + path + fileName;
                    break;
                case "zip":
                    command = "unzip -O cp936 " + path + fileName;
                    break;
                default:
                    return false;
            }
            if (StrUtil.isNotBlank(targetPath)) {
                command += ("gz".equals(fileSuffix) ? " -C" : " -d") + " /" + targetPath + "/";
            }
            printWriter.println(command);
            String commandFlag = "echo $?";
            printWriter.println(commandFlag);
            printWriter.flush();
            BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
            String msg;
            boolean flag = false;
            while ((msg = in.readLine()) != null && !flag) {
                System.out.println(msg);
                flag = msg.contains(commandFlag);
            }
            return true;
        } catch (IOException e) {
            log.error("解压文件出错", e);
            return false;
        } finally {
            pool.returnObject(channelShell);
        }
    }


    /**
     * 解压文件,获取文件中的每个文件和文件夹名称路径存到Map中
     *
     * @param zipFile 需要解压的Zip文件
     * @return {@link HashMap} Key: 路径 Value:类型(Directory或者File)
     */
    public Map<String, String> unZip(MultipartFile zipFile) {
        Map<String, String> returnMap = new HashMap<>(8);
        try (ZipArchiveInputStream inputStream = getZipFile(zipFile)) {
            ZipArchiveEntry entry;
            while ((entry = inputStream.getNextZipEntry()) != null) {
                if (entry.isDirectory()) {
                    returnMap.put(entry.getName(), "Directory");
                } else {
                    returnMap.put(entry.getName(), "File");
                }
            }
        } catch (Exception ignored) {
        }
        return returnMap;
    }

    private ZipArchiveInputStream getZipFile(File zipFile) throws Exception {
        return new ZipArchiveInputStream(new BufferedInputStream(new FileInputStream(zipFile)), "GBK", true);
    }

    private ZipArchiveInputStream getZipFile(MultipartFile zipFile) throws Exception {
        return new ZipArchiveInputStream(new BufferedInputStream(zipFile.getInputStream()), "GBK", true);
    }
}

🦌连接池相关类

SftpPool

import com.jcraft.jsch.ChannelSftp;
import com.pzx.demo.common.sftp.core.factory.SftpFactory;
import com.pzx.demo.core.exception.ApplicationSftpException;
import lombok.Data;
import org.apache.commons.pool2.impl.GenericObjectPool;

/**
 * @author JunPzx
 * @version v1.0.0
 * @date 2021/8/4 14:20
 */
@Data
public class SftpPool {

    private GenericObjectPool<ChannelSftp> pool;

    public SftpPool(SftpFactory factory) {
        this.pool = new GenericObjectPool<>(factory, factory.getProperties().getSftpPool());
        pool.setTestOnBorrow(true);
        pool.setTimeBetweenEvictionRunsMillis(1000 * 60 * 30);
        pool.setTestWhileIdle(true);
    }

    /**
     * 获取一个sftp连接对象
     *
     * @return sftp连接对象
     */
    public ChannelSftp borrowObject() {
        try {
            return pool.borrowObject();
        } catch (Exception e) {
            throw new ApplicationSftpException("获取ftp连接失败", e);
        }
    }

    /**
     * 归还一个sftp连接对象
     *
     * @param channelSftp sftp连接对象
     */
    public void returnObject(ChannelSftp channelSftp) {
        if (channelSftp != null) {
            pool.returnObject(channelSftp);
        }
    }

    /**
     * 初始化连接池
     *
     * @throws Exception e
     */
    public void init() throws Exception {
        for (int i = 0; i < pool.getMaxIdle(); i++) {
            pool.addObject();
        }
    }
}

ShellPool

import com.jcraft.jsch.ChannelShell;
import com.pzx.demo.common.sftp.core.factory.ShellFactory;
import com.pzx.demo.core.exception.ApplicationSftpException;
import lombok.Data;
import org.apache.commons.pool2.impl.GenericObjectPool;

/**
 * @author JunPzx
 * @version v1.0.0
 * @date 2021/8/4 14:20
 */
@Data
public class ShellPool {

    private GenericObjectPool<ChannelShell> pool;

    public ShellPool(ShellFactory factory) {
        this.pool = new GenericObjectPool<>(factory, factory.getProperties().getShellPool());
        pool.setTestOnBorrow(true);
        pool.setTimeBetweenEvictionRunsMillis(1000 * 60 * 30);
        pool.setTestWhileIdle(true);
    }

    /**
     * 获取一个执行器连接对象
     *
     * @return 执行器连接对象
     */
    public ChannelShell borrowObject() {
        try {
            return pool.borrowObject();
        } catch (Exception e) {
            throw new ApplicationSftpException("获取shell连接失败", e);
        }
    }

    /**
     * 归还一个执行器连接对象
     *
     * @param channelShell 执行器连接对象
     */
    public void returnObject(ChannelShell channelShell) {
        if (channelShell != null) {
            pool.returnObject(channelShell);
        }
    }

    /**
     * 初始化连接池
     *
     * @throws Exception e
     */
    public void init() throws Exception {
        for (int i = 0; i < pool.getMaxIdle(); i++) {
            pool.addObject();
        }
    }
}

♉写在最后

以上就是相关代码,如果对连接池有疑问可以查看利用commons-pool2自定义对象池,Respect