SpringBoot集成FTP与SFTP连接池流程

 更新时间:2022年12月06日 11:05:21   作者:leo825...  
在项目开发中,一般文件存储很少再使用SFTP服务,但是也不排除合作伙伴使用SFTP来存储项目中的文件或者通过SFTP来实现文件数据的交互,这篇文章主要介绍了SpringBoot集成FTP与SFTP连接池

简介

FTP 简介

FTP 即 文件传输协议(英语:File Transfer Protocol 的缩写)是一个用于计算机网络上在客户端和服务器之间进行文件传输的应用层协议。完整的 FTP 是由 FTP 服务器 和 FTP 客户端组成的,客户端可以将本地的文件通过 FTP 协议上传到服务器,也可以将服务器的文件下载到本地。它是当今使用的最古老的文件传输协议之一,是一种非常便捷的移动文件的方式。

FTP 连接需要 FTP 服务器和客户端两方在网络上建立通信。建立 FTP 连接时会有两个不同的通信通道。一个被称为命令通道,它的作用是发出和响应指令。另一个为数据通道,用于客户端和服务器端进行数据交互。

使用 FTP 传输文件时,用户需要通过向 FTP 服务器提供凭据来获得文件传输许可。当然某些公共 FTP 服务器可能不需要凭据即可访问其文件,但是无法保证数据传输的安全性,任何未加密公共网络上的数据发送都是非常危险的,所以为了保护传输数据的安全,由 FTP 衍生而出的就是下面的两种协议:FTPS 与 SFTP。

FTPS 简介

FPTSFTPS 隐式 SSLFTPS 显示 SSL 两种模式,两者都是用 SSL 加密。那么两者有什么区别呢?

FTPS 隐式 SSL:该模式通常在端口 990 上运行。在这个模式下全部数据的交换都需要在客户端和服务器之间建立 SSL 会话,并且服务器会拒绝任何不使用 SSL 进行的连接尝试。

FTPS 显式 SSL:显示 SSL 下服务器可以同时支持 FTP 和 FTPS 会话。开始会话前客户端需要先建立与 FTP 服务器的未加密连接,并在发送用户凭证前先发送 AUTH TLS 或 AUTH SSL 命令来请求服务器将命令通道切换到 SSL 加密通道,成功建立通道后再将用户凭证发送到 FTP 服务器,从而保证在会话期间的任何命令都可以通过 SSL 通道自动加密。

总结一下。当启用隐式模式时,FTP 的默认端口就被改为 TCP/990,服务器自动建立安全连接,并且要求客户端也必须支持安全连接模式也就是使用 SSL 进行连接。当启用显式模式时,与 FTP 连接方式和默认端口一样,但是需要以命令 AUTH SSL/TLS 类型的命令进行激活安全连接后才能正常传输数据。

SFTP 简介

最后大家来一起看看 SFTP,SFTP 是 Secure File Transfer Protocol 的缩写,也叫作安全文件传送协议。

如果说 FTPS 是在 FTP 协议上增加了一层 SSL ,那么 SFTP 就是是基于网络协议SSH(安全外壳)的协议与前面所说的 FTP 完全不同。SFTP 不使用单独的命令通道和数据通道,而是数据和命令都会通过单个连接以特殊格式的数据包进行传输。

SFTP 提供了两种验证连接的方法。

  • 与 FTP 一样,连接时只需要验证用户 ID 和密码就可以了。但是,与FTP不同的是,这些凭据是加密的,这是 SFTP 最主要的安全优势。
  • 除密码外,还可以通过 SSH 密钥来验证并通过 SFTP 协议连接。

FTP SFTP FTPS区别

维度FTPFTPSSFTP
安全命令和数据通道中的未加密信息交换。通信是人类可读的。通过隐式SSL或显式SSL在命令和数据通道上进行加密。通信是人类可读的。FTP服务器和客户端之间的所有信息交换均通过SSH协议加密。 SFTP还可以加密会话。由于通讯采用二进制格式, 因此难以理解。
服务器的防火墙端口允许端口21上的入站连接允许端口21和/或990、989上的入站连接允许端口22上的入站连接
客户端的防火墙端口允许到服务器定义的端口21和被动端口范围的出站连接允许到服务器定义的端口21和被动端口范围的出站连接允许到端口22的出站连接

实战代码

公共配置文件

pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.example</groupId>
        <artifactId>springboot-learning-parents</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <groupId>springboot-demo</groupId>
    <artifactId>springboot-ftp</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>springboot-ftp</name>
    <url>https://gitee.com/leo825/springboot-learning-parents.git</url>
    <description>springboot 集成 ftp、ftps、sftp</description>
    <properties>
        <start-class>com.demo.SpringbootFtpApplication</start-class>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <mybatisplus.version>3.5.1</mybatisplus.version>
        <freemaker.version>2.3.31</freemaker.version>
        <mysql.version>8.0.28</mysql.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- ftp 连接工具包 -->
        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>3.8.0</version>
        </dependency>
        <!-- 连接池化技术 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>2.9.0</version>
        </dependency>
        <!-- apache 常用 common工具包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>
        <!-- zip、gz、tar.gz 解压-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-compress</artifactId>
            <version>1.21</version>
        </dependency>
        <!-- sftp 连接依赖-->
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.55</version>
        </dependency>
        <!-- 常用工具包 hutool -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.9</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.yml 配置

#端口,项目上下文
server:
  port: 8080
  servlet:
    context-path: /springboot-ftp

# 日志输出配置
logging:
  level:
    root: INFO
    org:
      springframework:
        security: WARN
        web: ERROR
  file:
    path: ./logs
    name: './logs/springboot-ftp.log'
  pattern:
    file: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n'
    console: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n'

FTP 连接池

整体的代码结构如下

FTP 配置文件 ftp.properties

# ftp 连接参数
# 默认进入的路径
ftp.client.workingDirectory=/home/ftpuser
# 主机地址
ftp.client.host=192.168.56.103
# 主机端口
ftp.client.port=21
# 主机用户名
ftp.client.username=ftpuser
# 主机密码
ftp.client.password=Leo825@123
# 传输编码
ftp.client.encoding=utf-8
# 被动模式:在这种模式下,数据连接是由客户程序发起的
ftp.client.passiveMode=true
# 连接超时时间
ftp.client.clientTimeout=30000
# 线程数
ftp.client.threaNum=1
# 0=ASCII_FILE_TYPE(ASCII格式),1=EBCDIC_FILE_TYPE,2=LOCAL_FILE_TYPE(二进制文件)
ftp.client.transferFileType=2
# 是否重命名
ftp.client.renameUploaded=true
# 重新连接时间
ftp.client.retryTimes=1200
# 缓存大小
ftp.client.bufferSize=8192

# 最大数
ftp.client.maxTotal=50
# 最小空闲
ftp.client.minldle=10
# 最大空闲
ftp.client.maxldle=50
# 最大等待时间
ftp.client.maxWait=30000
# 池对象耗尽之后是否阻塞,maxWait < 0 时一直等待
ftp.client.blockWhenExhausted=true
# 取对象时验证
ftp.client.testOnBorrow=true
# 回收验证
ftp.client.testOnReturn=true
# 创建时验证
ftp.client.testOnCreate=true
# 空闲验证
ftp.client.testWhileldle=false
# 后进先出
ftp.client.lifo=false

FTP 配置实体类

package com.demo.ftp.config;
import lombok.Data;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@EnableConfigurationProperties
@PropertySource(value = {"ftp.properties"})
@ConfigurationProperties(prefix = "ftp.client")
@Configuration
@Data
public class FTPPoolConfig extends GenericObjectPoolConfig {
    // 默认进入的路径
    String workingDirectory;
    // 主机地址
    String host;
    // 主机端口
    int port;
    // 主机用户名
    String username;
    // 主机密码
    String password;
    // 传输编码
    String encoding;
    // 被动模式:在这种模式下,数据连接是由客户程序发起的
    boolean passiveMode;
    // 连接超时时间
    int clientTimeout;
    // 线程数
    int threaNum;
    // 0=ASCII_FILE_TYPE(ASCII格式),1=EBCDIC_FILE_TYPE,2=LOCAL_FILE_TYPE(二进制文件)
    int transferFileType;
    // 是否重命名
    boolean renameUploaded;
    // 重新连接时间
    int retryTimes;
    // 缓存大小
    int bufferSize;
    // 最大数
    int maxTotal;
    // 最小空闲
    int minldle;
    // 最大空闲
    int maxldle;
    // 最大等待时间
    int maxWait;
    // 池对象耗尽之后是否阻塞,maxWait < 0 时一直等待
    boolean blockWhenExhausted;
    // 取对象时验证
    boolean testOnBorrow;
    // 回收验证
    boolean testOnReturn;
    // 创建时验证
    boolean testOnCreate;
    // 空闲验证
    boolean testWhileldle;
    // 后进先出
    boolean lifo;
}

FTP 连接工厂

package com.demo.ftp.factory;
import com.demo.ftp.config.FTPPoolConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
 * FtpClient 工厂声场连接对象
 */
@Component
@Slf4j
public class FTPClientFactory implements PooledObjectFactory<FTPClient> {
    /**
     * 注入 ftp 连接配置
     */
    @Autowired
    FTPPoolConfig config;
    /**
     * 创建连接到池中
     *
     * @return
     * @throws Exception
     */
    @Override
    public PooledObject<FTPClient> makeObject() throws Exception {
        FTPClient ftpClient = new FTPClient();
        ftpClient.setConnectTimeout(config.getClientTimeout());
        ftpClient.connect(config.getHost(), config.getPort());
        int reply = ftpClient.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            ftpClient.disconnect();
            return null;
        }
        boolean success;
        if (StringUtils.isBlank(config.getUsername())) {
            success = ftpClient.login("anonymous", "anonymous");
        } else {
            success = ftpClient.login(config.getUsername(), config.getPassword());
        }
        if (!success) {
            return null;
        }
        ftpClient.setFileType(config.getTransferFileType());
        ftpClient.setBufferSize(1024);
        ftpClient.setControlEncoding(config.getEncoding());
        if (config.isPassiveMode()) {
            ftpClient.enterLocalPassiveMode();
        }
        log.debug("创建ftp连接");
        return new DefaultPooledObject<>(ftpClient);
    }
    /**
     * 链接状态检查
     *
     * @param pool
     * @return
     */
    @Override
    public boolean validateObject(PooledObject<FTPClient> pool) {
        FTPClient ftpClient = pool.getObject();
        try {
            return ftpClient != null && ftpClient.sendNoOp();
        } catch (Exception e) {
            return false;
        }
    }
    /**
     * 销毁连接,当连接池空闲数量达到上限时,调用此方法销毁连接
     *
     * @param pool
     * @throws Exception
     */
    @Override
    public void destroyObject(PooledObject<FTPClient> pool) throws Exception {
        FTPClient ftpClient = pool.getObject();
        if (ftpClient != null) {
            try {
                ftpClient.disconnect();
                log.debug("销毁ftp连接");
            } catch (Exception e) {
                log.error("销毁ftpClient异常,error:", e.getMessage());
            }
        }
    }
    /**
     * 钝化连接,是连接变为可用状态
     *
     * @param p
     * @throws Exception
     */
    @Override
    public void passivateObject(PooledObject<FTPClient> p) throws Exception{
        FTPClient ftpClient = p.getObject();
        try {
            ftpClient.changeWorkingDirectory(config.getWorkingDirectory());
            ftpClient.logout();
            if (ftpClient.isConnected()) {
                ftpClient.disconnect();
            }
        } catch (Exception e) {
            throw new RuntimeException("Could not disconnect from server.", e);
        }
    }
    /**
     * 初始化连接
     *
     * @param pool
     * @throws Exception
     */
    @Override
    public void activateObject(PooledObject<FTPClient> pool) throws Exception {
        FTPClient ftpClient = pool.getObject();
        ftpClient.connect(config.getHost(),config.getPort());
        ftpClient.login(config.getUsername(), config.getPassword());
        ftpClient.setControlEncoding(config.getEncoding());
        ftpClient.changeWorkingDirectory(config.getWorkingDirectory());
        //设置上传文件类型为二进制,否则将无法打开文件
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
    }
    /**
     * 获取 FTP 连接配置
     * @return
     */
    public FTPPoolConfig getConfig(){
        return config;
    }
}

FTP 连接池服务接口

package com.demo.ftp.service;
import com.demo.ftp.config.FTPPoolConfig;
import org.apache.commons.net.ftp.FTPClient;
/**
 * 获取 ftp 客户端对象的接口
 */
public interface FTPPoolService {
    /**
     * 获取ftpClient
     */
    FTPClient borrowObject();
    /**
     * 归还ftpClient
     */
    void returnObject(FTPClient ftpClient);
    /**
     * 获取 ftp 配置信息
     * @return
     */
    FTPPoolConfig getFtpPoolConfig();
}

impl 实现类

package com.demo.ftp.service.imp;
import com.demo.ftp.config.FTPPoolConfig;
import com.demo.ftp.factory.FTPClientFactory;
import com.demo.ftp.service.FTPPoolService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@Slf4j
public class FTPPoolServiceImpl implements FTPPoolService {
    /**
     * ftp 连接池生成
     */
    private GenericObjectPool<FTPClient> pool;
    /**
     * ftp 客户端配置文件
     */
    @Autowired
    private FTPPoolConfig config;
    /**
     * ftp 客户端工厂
     */
    @Autowired
    private FTPClientFactory factory;
    /**
     * 初始化pool
     */
    @PostConstruct
    private void initPool() {
        this.pool = new GenericObjectPool<FTPClient>(this.factory, this.config);
    }
    /**
     * 获取ftpClient
     */
    @Override
    public FTPClient borrowObject() {
        if (this.pool != null) {
            try {
                return this.pool.borrowObject();
            } catch (Exception e) {
                log.error("获取 FTPClient 失败 ", e);
            }
        }
        return null;
    }
    /**
     * 归还 ftpClient
     */
    @Override
    public void returnObject(FTPClient ftpClient) {
        if (this.pool != null && ftpClient != null) {
            this.pool.returnObject(ftpClient);
        }
    }
    @Override
    public FTPPoolConfig getFtpPoolConfig() {
        return config;
    }
}

FTP 工具类

package com.demo.util;
import cn.hutool.core.util.CharsetUtil;
import com.demo.ftp.service.FTPPoolService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
 * FTP工具类
 *
 * @Scope设置Bean的作用域 作用域类型            使用范围	            作用域描述
 * singleton	    所有Spring应用	    默认值,IoC容器只存在单例
 * prototype	    所有Spring应用	    每次从IoC容器中取出都是一个bean,都创建一个新的bean
 * session	        Spring web应用	    Http会话
 * application	    Spring web应用	    web工程生命周期
 * request	        Spring web应用	    web工程单次请求
 * globalSession	Spring web应用	    在一个全局的 httpSession中,一个bean对应一个实例。实践中基本不使用
 */
@Component
@Slf4j
public class FTPUtil {
    /**
     * ftp 连接池
     */
    @Autowired
    FTPPoolService ftpPoolService;
    /**
     * 从FTP服务器上下载文件,支持断点续传,下载百分比汇报
     *
     * @param remote 远程文件路径及名称
     * @param local  本地文件完整绝对路径
     * @return 下载的状态
     * @throws IOException
     */
    public DownloadStatus download(String remote, String local) throws IOException {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        // 设置被动模式,由于Linux安全性考虑,端口没有全部放开,所有被动模式不能用
        ftpClient.enterLocalPassiveMode();
        // 设置以二进制方式传输
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
        DownloadStatus result;
        try {
            // 检查远程文件是否存在
            FTPFile[] files = ftpClient.listFiles(new String(remote.getBytes(CharsetUtil.UTF_8), CharsetUtil.ISO_8859_1));
            if (files.length != 1) {
                log.info("远程文件不存在");
                return DownloadStatus.RemoteFileNotExist;
            }
            long lRemoteSize = files[0].getSize();
            File f = new File(local);
            // 本地存在文件,进行断点下载
            if (f.exists()) {
                long localSize = f.length();
                // 判断本地文件大小是否大于远程文件大小
                if (localSize >= lRemoteSize) {
                    log.info("本地文件大于远程文件,下载中止");
                    return DownloadStatus.LocalFileBiggerThanRemoteFile;
                }
                // 进行断点续传,并记录状态
                FileOutputStream out = new FileOutputStream(f, true);
                ftpClient.setRestartOffset(localSize);
                InputStream in = ftpClient.retrieveFileStream(new String(remote.getBytes(CharsetUtil.UTF_8), CharsetUtil.ISO_8859_1));
                byte[] bytes = new byte[1024];
                long step = lRemoteSize / 100;
                step = step == 0 ? 1 : step;// 文件过小,step可能为0
                long process = localSize / step;
                int c;
                while ((c = in.read(bytes)) != -1) {
                    out.write(bytes, 0, c);
                    localSize += c;
                    long nowProcess = localSize / step;
                    if (nowProcess > process) {
                        process = nowProcess;
                        if (process % 10 == 0) {
                            log.info("下载进度:" + process);
                        }
                    }
                }
                in.close();
                out.close();
                boolean isDo = ftpClient.completePendingCommand();
                if (isDo) {
                    result = DownloadStatus.DownloadFromBreakSuccess;
                } else {
                    result = DownloadStatus.DownloadFromBreakFailed;
                }
            } else {
                OutputStream out = new FileOutputStream(f);
                InputStream in = ftpClient.retrieveFileStream(new String(remote.getBytes(CharsetUtil.UTF_8), CharsetUtil.ISO_8859_1));
                byte[] bytes = new byte[1024];
                long step = lRemoteSize / 100;
                step = step == 0 ? 1 : step;// 文件过小,step可能为0
                long process = 0;
                long localSize = 0L;
                int c;
                while ((c = in.read(bytes)) != -1) {
                    out.write(bytes, 0, c);
                    localSize += c;
                    long nowProcess = localSize / step;
                    if (nowProcess > process) {
                        process = nowProcess;
                        if (process % 10 == 0) {
                            log.info("下载进度:" + process);
                        }
                    }
                }
                in.close();
                out.close();
                boolean upNewStatus = ftpClient.completePendingCommand();
                if (upNewStatus) {
                    result = DownloadStatus.DownloadNewSuccess;
                } else {
                    result = DownloadStatus.DownloadNewFailed;
                }
            }
        } catch (Exception e) {
            log.error("download error", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return DownloadStatus.DownloadNewFailed;
    }
    /**
     * 切换远程目录
     *
     * @param path
     * @return
     * @throws IOException
     */
    public boolean changeDirectory(String path) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        boolean result = false;
        try {
            result = ftpClient.changeWorkingDirectory(path);
        } catch (Exception e) {
            log.error("changeDirectory error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return result;
    }
    /**
     * 创建远程目录
     *
     * @param pathName
     * @return
     * @throws IOException
     */
    public boolean createDirectory(String pathName) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        boolean result = false;
        try {
            result = ftpClient.makeDirectory(pathName);
        } catch (Exception e) {
            log.error("createDirectory error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return result;
    }
    /**
     * 删除远程目录
     *
     * @param path
     * @return
     * @throws IOException
     */
    public boolean removeDirectory(String path) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        boolean result = false;
        try {
            result = ftpClient.removeDirectory(path);
        } catch (Exception e) {
            log.error("removeDirectory error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return result;
    }
    /**
     * 删除远程目录,可递归删除所有
     *
     * @param path
     * @param isAll
     * @return
     * @throws IOException
     */
    public boolean removeDirectory(String path, boolean isAll) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        boolean result = false;
        try {
            if (!isAll) {
                return removeDirectory(path);
            }
            FTPFile[] ftpFileArr = ftpClient.listFiles(path);
            if (ftpFileArr == null || ftpFileArr.length == 0) {
                return removeDirectory(path);
            }
            //
            for (FTPFile ftpFile : ftpFileArr) {
                String name = ftpFile.getName();
                if (ftpFile.isDirectory()) {
                    log.info("* [sD]Delete subPath [" + path + "/" + name + "]");
                    if (!ftpFile.getName().equals(".") && (!ftpFile.getName().equals(".."))) {
                        removeDirectory(path + "/" + name, true);
                    }
                } else if (ftpFile.isFile()) {
                    log.info("* [sF]Delete file [" + path + "/" + name + "]");
                    deleteFile(path + "/" + name);
                } else if (ftpFile.isSymbolicLink()) {
                } else if (ftpFile.isUnknown()) {
                }
            }
            result = ftpClient.removeDirectory(path);
        } catch (Exception e) {
            log.error("removeDirectory error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return result;
    }
    /**
     * 查看目录是否存在
     *
     * @param path
     * @return
     * @throws IOException
     */
    public boolean isDirectoryExists(String path) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        boolean flag = false;
        try {
            FTPFile[] ftpFileArr = ftpClient.listFiles(path);
            for (FTPFile ftpFile : ftpFileArr) {
                if (ftpFile.isDirectory() && ftpFile.getName().equalsIgnoreCase(path)) {
                    flag = true;
                    break;
                }
            }
        } catch (Exception e) {
            log.error("isDirectoryExists error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return flag;
    }
    /**
     * 得到某个目录下的文件名列表
     *
     * @param path
     * @return
     * @throws IOException
     */
    public List<String> getFileList(String path) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        List<String> retList = new ArrayList<String>();
        try {
            FTPFile[] ftpFiles = ftpClient.listFiles(path);
            if (ftpFiles == null || ftpFiles.length == 0) {
                return retList;
            }
            for (FTPFile ftpFile : ftpFiles) {
                if (ftpFile.isFile()) {
                    retList.add(ftpFile.getName());
                }
            }
        } catch (Exception e) {
            log.error("getFileList error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return retList;
    }
    /**
     * 删除文件
     *
     * @param pathName
     * @return
     * @throws IOException
     */
    public boolean deleteFile(String pathName) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        boolean result = false;
        try {
            result = ftpClient.deleteFile(pathName);
        } catch (Exception e) {
            log.error("deleteFile error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return result;
    }
    /**
     * 上传文件到FTP服务器,支持断点续传
     *
     * @param local  本地文件名称,绝对路径
     * @param remote 远程文件路径,按照Linux上的路径指定方式,支持多级目录嵌套,支持递归创建不存在的目录结构
     * @return 上传结果
     * @throws IOException
     */
    public UploadStatus upload(String local, String remote) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        UploadStatus result = UploadStatus.UploadNewFileFailed;
        try {
            ftpClient.changeWorkingDirectory(ftpPoolService.getFtpPoolConfig().getWorkingDirectory());
            // 设置PassiveMode传输
            ftpClient.enterLocalPassiveMode();
            // 设置以二进制流的方式传输
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            ftpClient.setControlEncoding(CharsetUtil.UTF_8);
            // 对远程目录的处理
            String remoteFileName = remote;
            if (remote.contains("/")) {
                remoteFileName = remote.substring(remote.lastIndexOf("/") + 1);
                // 创建服务器远程目录结构,创建失败直接返回
                if (createDirecroty(remote, ftpClient) == UploadStatus.CreateDirectoryFail) {
                    return UploadStatus.CreateDirectoryFail;
                }
            }
            // 检查远程是否存在文件
            FTPFile[] files = ftpClient.listFiles(new String(remoteFileName.getBytes(CharsetUtil.UTF_8), CharsetUtil.ISO_8859_1));
            if (files.length == 1) {
                long remoteSize = files[0].getSize();
                File f = new File(local);
                long localSize = f.length();
                if (remoteSize == localSize) { // 文件存在
                    return UploadStatus.FileExits;
                } else if (remoteSize > localSize) {
                    return UploadStatus.RemoteFileBiggerThanLocalFile;
                }
                // 尝试移动文件内读取指针,实现断点续传
                result = uploadFile(remoteFileName, f, ftpClient, remoteSize);
                // 如果断点续传没有成功,则删除服务器上文件,重新上传
                if (result == UploadStatus.UploadFromBreakFailed) {
                    if (!ftpClient.deleteFile(remoteFileName)) {
                        return UploadStatus.DeleteRemoteFaild;
                    }
                    result = uploadFile(remoteFileName, f, ftpClient, 0);
                }
            } else {
                result = uploadFile(remoteFileName, new File(local), ftpClient, 0);
            }
        } catch (Exception e) {
            log.error("upload error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return result;
    }
    /**
     * 递归创建远程服务器目录
     *
     * @param remote    远程服务器文件绝对路径
     * @param ftpClient FTPClient对象
     * @return 目录创建是否成功
     * @throws IOException
     */
    public UploadStatus createDirecroty(String remote, FTPClient ftpClient) throws IOException {
        UploadStatus status = UploadStatus.CreateDirectorySuccess;
        String directory = remote.substring(0, remote.lastIndexOf("/") + 1);
        if (!directory.equalsIgnoreCase("/") && !ftpClient.changeWorkingDirectory(new String(directory.getBytes(CharsetUtil.UTF_8), CharsetUtil.ISO_8859_1))) {
            // 如果远程目录不存在,则递归创建远程服务器目录
            int start = 0;
            int end = 0;
            if (directory.startsWith("/")) {
                start = 1;
            } else {
                start = 0;
            }
            end = directory.indexOf("/", start);
            while (true) {
                String subDirectory = new String(remote.substring(start, end).getBytes(CharsetUtil.UTF_8), CharsetUtil.ISO_8859_1);
                if (!ftpClient.changeWorkingDirectory(subDirectory)) {
                    if (ftpClient.makeDirectory(subDirectory)) {
                        ftpClient.changeWorkingDirectory(subDirectory);
                    } else {
                        log.info("创建目录失败");
                        return UploadStatus.CreateDirectoryFail;
                    }
                }
                start = end + 1;
                end = directory.indexOf("/", start);
                // 检查所有目录是否创建完毕
                if (end <= start) {
                    break;
                }
            }
        }
        return status;
    }
    /**
     * 上传文件到服务器,新上传和断点续传
     *
     * @param remoteFile 远程文件名,在上传之前已经将服务器工作目录做了改变,一定要注意这里的 remoteFile 已经别被编码 ISO-8859-1
     * @param localFile  本地文件File句柄,绝对路径
     * @param ftpClient  FTPClient引用
     * @return
     * @throws IOException
     */
    public UploadStatus uploadFile(String remoteFile, File localFile, FTPClient ftpClient, long remoteSize) {
        if (null == ftpClient) {
            ftpClient = ftpPoolService.borrowObject();
            ;
        }
        if (null == ftpClient) {
            return null;
        }
        UploadStatus status = UploadStatus.UploadNewFileFailed;
        try (RandomAccessFile raf = new RandomAccessFile(localFile, "r");
             // 一定要注意这里的 remoteFile 已经别被编码 ISO-8859-1
             OutputStream out = ftpClient.appendFileStream(remoteFile);) {
            // 显示进度的上传
            log.info("localFile.length():" + localFile.length());
            long step = localFile.length() / 100;
            step = step == 0 ? 1 : step;// 文件过小,step可能为0
            long process = 0;
            long localreadbytes = 0L;
            // 断点续传
            if (remoteSize > 0) {
                ftpClient.setRestartOffset(remoteSize);
                process = remoteSize / step;
                raf.seek(remoteSize);
                localreadbytes = remoteSize;
            }
            byte[] bytes = new byte[1024];
            int c;
            while ((c = raf.read(bytes)) != -1) {
                out.write(bytes, 0, c);
                localreadbytes += c;
                if (localreadbytes / step != process) {
                    process = localreadbytes / step;
                    if (process % 10 == 0) {
                        log.info("上传进度:" + process);
                    }
                }
            }
            out.flush();
            raf.close();
            out.close();
            // FTPUtil的upload方法在执行ftpClient.completePendingCommand()之前应该先关闭OutputStream,否则主线程会在这里卡死执行不下去。
            // 原因是completePendingCommand()会一直在等FTP Server返回226 Transfer complete,但是FTP Server只有在接受到OutputStream执行close方法时,才会返回。
            boolean result = ftpClient.completePendingCommand();
            if (remoteSize > 0) {
                status = result ? UploadStatus.UploadFromBreakSuccess : UploadStatus.UploadFromBreakFailed;
            } else {
                status = result ? UploadStatus.UploadNewFileSuccess : UploadStatus.UploadNewFileFailed;
            }
        } catch (Exception e) {
            log.error("uploadFile error ", e);
        }
        return status;
    }
    /**
     * 下载资源文件
     *
     * @param sourceFileName
     * @return
     * @throws IOException
     */
    public InputStream downFile(String sourceFileName) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        InputStream result = null;
        try {
            result = ftpClient.retrieveFileStream(sourceFileName);
        } catch (Exception e) {
            log.error("deleteFile error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
        return result;
    }
    /**
     * 查看用户目录
     *
     * @param path
     * @throws IOException
     */
    public void listFilesDir(String path) {
        FTPClient ftpClient = ftpPoolService.borrowObject();
        try {
            String ftpPath = path;
            ftpClient.changeWorkingDirectory(ftpPath);
            FTPFile[] files = ftpClient.listFiles();
            for (FTPFile ff : files) {
                if (!ff.isDirectory()) {
                } else {
                    if (!ff.getName().startsWith(".")) {
                        ftpPath = ff.getName() + "/";
                        ftpClient.changeWorkingDirectory(ftpPath);
                        listFilesDir(ftpPath);
                        ftpClient.changeWorkingDirectory(path);
                    }
                }
            }
        } catch (Exception e) {
            log.error("deleteFile error ", e);
        } finally {
            ftpPoolService.returnObject(ftpClient);
        }
    }
    /**
     * 上传状态枚举
     */
    public enum UploadStatus {
        CreateDirectoryFail, // 远程服务器相应目录创建失败
        CreateDirectorySuccess, // 远程服务器闯将目录成功
        UploadNewFileSuccess, // 上传新文件成功
        UploadNewFileFailed, // 上传新文件失败
        FileExits, // 文件已经存在
        RemoteFileBiggerThanLocalFile, // 远程文件大于本地文件
        UploadFromBreakSuccess, // 断点续传成功
        UploadFromBreakFailed, // 断点续传失败
        DeleteRemoteFaild; // 删除远程文件失败
    }
    /**
     * 下载状态枚举
     */
    public enum DownloadStatus {
        RemoteFileNotExist, // 远程文件不存在
        DownloadNewSuccess, // 下载文件成功
        DownloadNewFailed, // 下载文件失败
        LocalFileBiggerThanRemoteFile, // 本地文件大于远程文件
        DownloadFromBreakSuccess, // 断点续传成功
        DownloadFromBreakFailed; // 断点续传失败
    }
}

SFTP 连接池

整体代码结构:

SFTP 配置文件 sftp.properties

# ftp 连接参数
# 默认进入的路径
sftp.client.workingDirectory=/home/hadoop/sftpdata
# 主机地址
sftp.client.host=192.168.56.103
# 主机端口
sftp.client.port=22
# 主机用户名
sftp.client.username=hadoop
# 主机密码
sftp.client.password=123@Hado
# sftp 私钥
sftp.client.privateKey=
# 传输编码
sftp.client.encoding=utf-8
# 连接超时时间
sftp.client.clientTimeout=30000
# 缓存大小
sftp.client.bufferSize=8192

# 最大数
sftp.client.maxTotal=50
# 最小空闲
sftp.client.minldle=10
# 最大空闲
sftp.client.maxldle=50
# 最大等待时间
sftp.client.maxWait=30000
# 池对象耗尽之后是否阻塞,maxWait < 0 时一直等待
sftp.client.blockWhenExhausted=true
# 取对象时验证
sftp.client.testOnBorrow=true
# 回收验证
sftp.client.testOnReturn=true
# 创建时验证
sftp.client.testOnCreate=true
# 空闲验证
sftp.client.testWhileldle=false
# 后进先出
sftp.client.lifo=false

SFTP 配置实体类

package com.demo.sftp.config;
import lombok.Data;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@EnableConfigurationProperties
@PropertySource(value = {"sftp.properties"})
@ConfigurationProperties(prefix = "sftp.client")
@Configuration
@Data
public class SFTPPoolConfig extends GenericObjectPoolConfig {
    // 默认进入的路径
    String workingDirectory;
    // 主机地址
    String host;
    // 主机端口
    int port;
    // 主机用户名
    String username;
    // 主机密码
    String password;
    // 主机密码
    String privateKey;
    // 传输编码
    String encoding;
    // 连接超时时间
    int clientTimeout;
    // 重新连接时间
    int retryTimes;
    // 缓存大小
    int bufferSize;
    // 最大数
    int maxTotal;
    // 最小空闲
    int minldle;
    // 最大空闲
    int maxldle;
    // 最大等待时间
    int maxWait;
    // 池对象耗尽之后是否阻塞,maxWait < 0 时一直等待
    boolean blockWhenExhausted;
    // 取对象时验证
    boolean testOnBorrow;
    // 回收验证
    boolean testOnReturn;
    // 创建时验证
    boolean testOnCreate;
    // 空闲验证
    boolean testWhileldle;
    // 后进先出
    boolean lifo;
}

SFTP 连接工厂

package com.demo.sftp.factory;
import com.demo.sftp.config.SFTPPoolConfig;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Properties;
/**
 * SFTP 工厂声场连接对象
 */
@EqualsAndHashCode(callSuper = true)
@Component
@Slf4j
public class ChannelSftpFactory extends BasePooledObjectFactory<ChannelSftp>{
    /**
     * 注入 sftp 连接配置
     */
    @Autowired
    SFTPPoolConfig config;
    /**
     * 新建对象
     */
    @Override
    public ChannelSftp create() {
        ChannelSftp channel = null;
        try {
            // 用户名密码不能为空
            if (StringUtils.isBlank(config.getUsername()) || StringUtils.isBlank(config.getPassword())) {
                log.error("username or password is needed !!!");
                return null;
            }
            JSch jsch = new JSch();
            // 设置私钥
            if (StringUtils.isNotBlank(config.getPrivateKey())) {
                jsch.addIdentity(config.getPrivateKey());
            }
            // jsch的session需要补充设置sshConfig.put("PreferredAuthentications", "publickey,keyboard-interactive,password")来跳过Kerberos认证,同样的HutoolSFTPUtil工具类里面也有这个问题
            Session sshSession = jsch.getSession(config.getUsername(), config.getHost(), config.getPort());
            sshSession.setPassword(config.getPassword());
            Properties sshConfig = new Properties();
            // “StrictHostKeyChecking”如果设置成“yes”,ssh就不会自动把计算机的密匙加入“$HOME/.ssh/known_hosts”文件,并且一旦计算机的密匙发生了变化,就拒绝连接。
            sshConfig.put("StrictHostKeyChecking", "no");
            sshSession.setConfig(sshConfig);
            sshSession.connect();
            channel = (ChannelSftp) sshSession.openChannel("sftp");
            channel.connect();
        } catch (Exception e) {
            log.error("连接 sftp 失败,请检查配置", e);
        }
        return channel;
    }
    /**
     * 创建一个连接
     *
     * @param channelSftp
     * @return
     */
    @Override
    public PooledObject<ChannelSftp> wrap(ChannelSftp channelSftp) {
        return new DefaultPooledObject<>(channelSftp);
    }
    /**
     * 销毁一个连接
     *
     * @param p
     */
    @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 (Exception e) {
            log.error("channelSftp 不可用 ", e);
            return false;
        }
        return true;
    }
    /**
     * 获取 FTP 连接配置
     * @return
     */
    public SFTPPoolConfig getConfig(){
        return config;
    }
}

SFTP 连接池服务接口

package com.demo.sftp.service;
import com.demo.sftp.config.SFTPPoolConfig;
import com.jcraft.jsch.ChannelSftp;
/**
 * 获取 sftp 客户端对象的接口
 */
public interface SFTPPoolService {
    /**
     * 获取 sftp
     */
    ChannelSftp borrowObject() ;

    /**
     * 归还 sftp
     */
    void returnObject(ChannelSftp channelSftp);
    /**
     * 获取 ftp 配置信息
     * @return
     */
    SFTPPoolConfig getFtpPoolConfig();
}

impl 实现类:

package com.demo.sftp.service.imp;
import com.demo.sftp.config.SFTPPoolConfig;
import com.demo.sftp.factory.ChannelSftpFactory;
import com.demo.sftp.service.SFTPPoolService;
import com.jcraft.jsch.ChannelSftp;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@Slf4j
public class SFTPPoolServiceImpl implements SFTPPoolService {
    /**
     * ftp 连接池生成
     */
    private GenericObjectPool<ChannelSftp> pool;
    /**
     * ftp 客户端配置文件
     */
    @Autowired
    private SFTPPoolConfig config;
    /**
     * ftp 客户端工厂
     */
    @Autowired
    private ChannelSftpFactory factory;
    /**
     * 初始化pool
     */
    @PostConstruct
    private void initPool() {
        this.pool = new GenericObjectPool<ChannelSftp>(this.factory, this.config);
    }
    /**
     * 获取sftp
     */
    @Override
    public ChannelSftp borrowObject() {
        if (this.pool != null) {
            try {
                return this.pool.borrowObject();
            } catch (Exception e) {
                log.error("获取 ChannelSftp 失败", e);
                e.printStackTrace();
            }
        }
        return null;
    }
    /**
     * 归还 sftp
     */
    @Override
    public void returnObject(ChannelSftp channelSftp) {
        if (this.pool != null && channelSftp != null) {
            this.pool.returnObject(channelSftp);
        }
    }
    @Override
    public SFTPPoolConfig getFtpPoolConfig() {
        return config;
    }
}

SFTP 工具类

package com.demo.util;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.ArrayUtil;
import com.demo.sftp.service.SFTPPoolService;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.SftpException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.stream.Collectors;
/**
 * Sftp工具类
 */
@Component
@Slf4j
public class SFTPUtil {
    /**
     * sftp连接池服务
     */
    @Autowired
    SFTPPoolService sFtpPoolService;
    /**
     * 文件分隔符,linux下就是 /
     */
    private String NODE_SEPARATOR = "/";
    /**
     * 权限数字
     */
    private String permission = "755";
    /**
     * 每个目录下最大子文件(夹)数量
     */
    private int MAX_CHILD_FILE_NUMBER = 1000;
    /**
     * 本地文件上传到远程 目录
     *
     * @param relativePath 远程文件最后一级目录
     * @param fileName 远程文件名
     * @param localFileFullPath 本地文件全路径
     * @return
     */
    public String uploadLocalToRemote(String relativePath, String fileName, String localFileFullPath) {
        String remoteFullPath = "";
        File localFile = new File(localFileFullPath);
        if (localFile.exists() && localFile.isFile()) {
            try (InputStream inputStream = new FileInputStream(localFile)) {
                if (!relativePath.endsWith(NODE_SEPARATOR)) {
                    relativePath = relativePath.concat(NODE_SEPARATOR);
                }
                remoteFullPath = upload(relativePath, fileName, inputStream);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return remoteFullPath;
    }
    /**
     * 附件上传
     *
     * @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 = sFtpPoolService.borrowObject();
        String filePath = sFtpPoolService.getFtpPoolConfig().getWorkingDirectory();
        try {
            if (relativePath != null && !relativePath.trim().isEmpty()) {
                filePath = filePath + relativePath;
            }
            if (!dirIsExist(filePath)) {
                filePath = generateValidPath(filePath, sftp);
            }
            filePath = filePath.concat(fileName);
            sftp.put(inputStream, filePath);
            sftp.chmod(Integer.parseInt(permission, 8), filePath);
            return filePath;
        } catch (SftpException e) {
            log.error("SFTP上传文件出错", e);
        } finally {
            sFtpPoolService.returnObject(sftp);
        }
        return filePath;
    }
    /**
     * 文件下载
     *
     * @param fileUrl 文件路径
     * @return 文件字节数组
     */
    public byte[] download(String fileUrl) {
        ChannelSftp sftp = sFtpPoolService.borrowObject();
        try {
            InputStream inputStream = sftp.get(fileUrl);
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int n;
            byte[] data = new byte[sFtpPoolService.getFtpPoolConfig().getBufferSize()];
            while ((n = inputStream.read(data, 0, data.length)) != -1) {
                buffer.write(data, 0, n);
            }
            buffer.flush();
            return buffer.toByteArray();
        } catch (IOException | SftpException e) {
            log.error("SFTP下载文件出错", e);
        } finally {
            sFtpPoolService.returnObject(sftp);
        }
        return new byte[0];
    }
    /**
     * 创建目录(只能创建一级目录,如果需要创建多级目录,需要调用mkdirs方法)
     *
     * @param path 目录路径
     */
    public void createFolder(String path) {
        ChannelSftp sftp = sFtpPoolService.borrowObject();
        try {
            sftp.mkdir(path);
        } catch (SftpException e) {
            log.error("SFTP创建文件夹出错", e);
        } finally {
            sFtpPoolService.returnObject(sftp);
        }
    }
    /**
     * 如果目录不存在,则创建多级目录
     *
     * @param path
     * @return
     */
    public boolean createFolders(String path){
        ChannelSftp sftp = sFtpPoolService.borrowObject();
        boolean result = false;
        try {
            if (validatePathValid(path, sftp)) {
                result = true;
            } else {
                String newPath = path + String.valueOf(System.currentTimeMillis()).substring(9);
                mkdirs(sftp, newPath.split("/"), "", newPath.split("/").length, 0);
                result = true;
            }
        } catch (Exception e) {
            log.error("SFTP创建文件夹出错", e);
        } finally {
            sFtpPoolService.returnObject(sftp);
        }
        return result;
    }
    /**
     * 文件读取
     *
     * @param fileUrl 文件路径
     * @return 文件字节数组
     */
    public String read(String fileUrl) {
        ChannelSftp sftp = sFtpPoolService.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) {
            log.error("SFTP读取文件出错", e);
        } finally {
            sFtpPoolService.returnObject(sftp);
        }
        return "";
    }
    /**
     * 判断目录是否存在
     *
     * @param url 文件夹目录
     * @return ture:存在;false:不存在
     */
    public boolean dirIsExist(String url) {
        ChannelSftp sftp = sFtpPoolService.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) {
            log.error("SFTP读取文件夹出错", e);
        } finally {
            sFtpPoolService.returnObject(sftp);
        }
        return false;
    }
    /**
     * 删除文件 或 删除文件夹
     * 注: 如果是文件夹, 不论该文件夹中有无内容,都能删除, 因此:此方法慎用
     *
     * @param remoteDirOrRemoteFile 要删除的文件  或 文件夹
     */
    public void delete(String remoteDirOrRemoteFile) {
        ChannelSftp sftp = sFtpPoolService.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) {
            log.error("SFTP删除文件或者文件夹出错", e);
        } finally {
            sFtpPoolService.returnObject(sftp);
        }
    }
    /**
     * 删除相关文件 并 采集所有 需要被删除的 文件夹
     * <p>
     * 注: 如果是文件夹, 不论该文件夹中有无内容,都能删除, 因此:此方法慎用
     *
     * @param toBeDeletedEmptyDirContainer 所有待删除的空文件夹集合
     * @param targetFileOrDirContainer     本次, 要删除的文件的集合   或   本次, 要删除的文件所在文件夹的集合
     */
    public 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指向的是文件的话,返回空的集合
     * 注: 只会包含子级文件夹,不包含孙子级文件夹(如果有孙子级文件夹的话)
     */
    public List<String> fileDeleteExecutor(String remoteDirOrRemoteFile) {
        ChannelSftp sftp = sFtpPoolService.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) {
            log.error("SFTP删除文件或者文件夹出错", e);
        } finally {
            sFtpPoolService.returnObject(sftp);
        }
        return new ArrayList<>(0);
    }
    /**
     * 从给定路径中截取文件名
     *
     * @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 是有无效
     */
    public boolean invalidFileName(String fileName) {
        return ".".equals(fileName) || "..".equals(fileName);
    }
    /**
     * 判断SFTP上的path是否为文件夹
     * 注:如果该路径不存在,那么会返回false
     *
     * @param path SFTP上的路径
     * @return 判断结果
     */
    public boolean isDirectory(String path) {
        ChannelSftp sftp = sFtpPoolService.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 {
            sFtpPoolService.returnObject(sftp);
        }
    }
    /**
     * 获取某个文件夹下的所有文件名称
     *
     * @param path      文件夹路径
     * @param fileTypes 文件类型,如果为null或者长度为0,则获取所有文件名称,如果已指定,则获取指定类型的文件类型
     * @return 文件名称
     */
    public List<String> queryFileName(String path, String... fileTypes) {
        ChannelSftp sftp = sFtpPoolService.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) {
            log.error("SFTP获取某个文件夹下的所有文件名称出错", e);
        } finally {
            sFtpPoolService.returnObject(sftp);
        }
        return new ArrayList<>(0);
    }
    /**
     * 创建多级文件目录
     *
     * @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(permission, 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 = sFtpPoolService.borrowObject();
        try {
            sftp.cd(path);
            return sftp.ls(path).size();
        } finally {
            sFtpPoolService.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;
        }
    }
}

SFTP 工具类

(整合 Hutool ,好用)

package com.demo.util;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.extra.ssh.JschUtil;
import cn.hutool.extra.ssh.Sftp;
import com.demo.sftp.config.SFTPPoolConfig;
import com.jcraft.jsch.Session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
@Component
@Slf4j
public class HutoolSFTPUtil {
    @Autowired
    SFTPPoolConfig sftpPoolConfig;
    /**
     * 创建目录
     *
     * @param newDir
     */
    public boolean mkdir(String newDir) {
        // jsch的session需要补充设置sshConfig.put("PreferredAuthentications", "publickey,keyboard-interactive,password")来跳过Kerberos认证,同样的HutoolSFTPUtil工具类里面也有这个问题
        Session session = JschUtil.createSession(sftpPoolConfig.getHost(), sftpPoolConfig.getPort(), sftpPoolConfig.getUsername(), sftpPoolConfig.getPassword());
        Sftp sftp = JschUtil.createSftp(session);
        boolean result = false;
        try {
            result = sftp.mkdir(newDir);
        } catch (Exception e) {
            log.error("mkdir error ", e);
        } finally {
            JschUtil.close(session);
        }
        return result;
    }
    /**
     * 删除目录
     *
     * @param delDir
     */
    public boolean delDir(String delDir) {
        Session session = JschUtil.createSession(sftpPoolConfig.getHost(), sftpPoolConfig.getPort(), sftpPoolConfig.getUsername(), sftpPoolConfig.getPassword());
        Sftp sftp = JschUtil.createSftp(session);
        boolean result = false;
        try {
            result = sftp.delDir(delDir);
        } catch (Exception e) {
            log.error("mkdir error ", e);
        } finally {
            JschUtil.close(session);
        }
        return result;
    }
    /**
     * 递归创建目录
     *
     * @param newDir
     * @return
     */
    public boolean mkdirs(String newDir) {
        Session session = JschUtil.createSession(sftpPoolConfig.getHost(), sftpPoolConfig.getPort(), sftpPoolConfig.getUsername(), sftpPoolConfig.getPassword());
        Sftp sftp = JschUtil.createSftp(session);
        boolean result = false;
        try {
            sftp.mkDirs(newDir);
            result = true;
        } catch (Exception e) {
            log.error("mkdir error ", e);
        } finally {
            JschUtil.close(session);
        }
        return result;
    }
    /**
     * 上传文件
     *
     * @param destPath
     * @param srcFileFullPath
     * @return
     */
    public boolean upload(String destPath, String srcFileFullPath) {
        Session session = JschUtil.createSession(sftpPoolConfig.getHost(), sftpPoolConfig.getPort(), sftpPoolConfig.getUsername(), sftpPoolConfig.getPassword());
        Sftp sftp = JschUtil.createSftp(session);
        boolean result = false;
        try {
            File file = new File(srcFileFullPath);
            result = sftp.upload(destPath, file);
        } catch (Exception e) {
            log.error("mkdir error ", e);
        } finally {
            JschUtil.close(session);
        }
        return result;
    }
    /**
     * 下载文件
     *
     * @param src
     * @param destFileFullPath
     * @return
     */
    public boolean download(String src, String destFileFullPath) {
        Session session = JschUtil.createSession(sftpPoolConfig.getHost(), sftpPoolConfig.getPort(), sftpPoolConfig.getUsername(), sftpPoolConfig.getPassword());
        Sftp sftp = JschUtil.createSftp(session);
        boolean result = false;
        try {
            File destFile = new File(destFileFullPath);
            if (!destFile.exists()) {
                destFile.createNewFile();
            }
            sftp.download(src, destFile);
            result = true;
        } catch (Exception e) {
            log.error("mkdir error ", e);
        } finally {
            JschUtil.close(session);
        }
        return result;
    }
    /**
     * 远程执行 shell 命令
     *
     * @param command
     * @return
     * @throws Exception
     */
    public String exec(String command) {
        Session session = JschUtil.createSession(sftpPoolConfig.getHost(), sftpPoolConfig.getPort(), sftpPoolConfig.getUsername(), sftpPoolConfig.getPassword());
        String result = "";
        try {
            log.info("执行命令:{}", command);
            result = JschUtil.exec(session, command, CharsetUtil.CHARSET_UTF_8);
            log.info("执行结果:{}", result);
        } catch (Exception e) {
            log.error("exec error ", e);
        } finally {
            JschUtil.close(session);
        }
        return result;
    }
}

测试代码

package com.demo.controller;
import com.demo.util.FTPUtil;
import com.demo.util.HutoolSFTPUtil;
import com.demo.util.SFTPUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * controller 测试
 *
 */
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
    /**
     * 构造方法注入
     */
    @Autowired
    FTPUtil ftpUtil;
    @Autowired
    SFTPUtil sftpUtil;
    @Autowired
    HutoolSFTPUtil hutoolSFTPUtil;
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/ftpCreate")
    public String ftpCreate() {
        boolean result =  false;
        try {
            result = ftpUtil.createDirectory("test03");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(result);
    }
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/upload")
    public String upload() {
        try {
            String remotePath = "/test01/test-20220718 -测试.txt";
            ftpUtil.createDirectory(remotePath);
            String localPath = "D:\\logs\\log\\test-20220718 -测试.txt";
            FTPUtil.UploadStatus uploadStatus = ftpUtil.upload(localPath,remotePath);
            log.info(String.valueOf(uploadStatus));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "success";
    }
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/sftpCreate")
    public String sftpCreate() {
        boolean result =  false;
        try {
            result = sftpUtil.createFolders("/home/hadoop/sftpdata/test01");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(result);
    }
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/sftpUpload")
    public String sftpUpload() {
        String result =  "";
        try {
            String remotePath = "/test01";
            String remoteFileName = "test-20220718 -测试.txt";
            String localFileFullPath = "D:\\logs\\log\\test-20220718 -测试.txt";
            result = sftpUtil.uploadLocalToRemote(remotePath,remoteFileName,localFileFullPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/hutoolMakeDir")
    public String hutoolMakeDir() {
        boolean result = false;
        try {
            result = hutoolSFTPUtil.mkdir("/home/hadoop/sftpdata/hutool");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(result);
    }
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/hutoolMakeDirs")
    public String hutoolMakeDirs() {
        boolean result = false;
        try {
            result = hutoolSFTPUtil.mkdirs("/home/hadoop/sftpdata/hutool/test01/test02");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(result);
    }
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/hutoolUpload")
    public String hutoolUpload() {
        boolean result = false;
        try {
            String remoteFile = "/home/hadoop/sftpdata/hutool/test01/test02/test01.txt";
            String localFile = "D:\\logs\\log\\test-20220718 -测试.txt";
            String remotePath = remoteFile.substring(0,remoteFile.lastIndexOf("/"));
            if(hutoolSFTPUtil.mkdirs(remotePath)){
                result = hutoolSFTPUtil.upload(remoteFile,localFile);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(result);
    }
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/hutoolDownload")
    public String hutoolDownload() {
        boolean result = false;
        try {
            String remoteFile = "/home/hadoop/sftpdata/hutool/01.txt";
            String localFile = "D:\\logs\\log\\test-20220718-02.txt";
            result = hutoolSFTPUtil.download(remoteFile,localFile);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(result);
    }
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/hutoolDelDir")
    public String hutoolDelDir() {
        boolean result = false;
        try {
            result = hutoolSFTPUtil.delDir("/home/hadoop/sftpdata/hutool");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(result);
    }
    /**
     * 保存数据
     *
     * @return
     */
    @GetMapping("/exec")
    public String exec() {
        String result = "";
        try {
            result = hutoolSFTPUtil.exec("ls /home/hadoop");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return String.valueOf(result);
    }
}

总结

1、FTP 服务器需要自己搭建,网上很多很简单

2、SFTP 方式访问,只要能用 SSH 连接到服务器就行

3、Hutool 真的是很强大的工具包,建议以后深入了解,不要重复造轮子

4、源代码地址:https://gitee.com/leo825/springboot-learning-parents.git

到此这篇关于SpringBoot集成FTP与SFTP连接池流程的文章就介绍到这了,更多相关SpringBoot集成连接池内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • mybatisplus开启sql打印的三种方式汇总

    mybatisplus开启sql打印的三种方式汇总

    这篇文章主要介绍了mybatisplus开启sql打印的三种方式,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2024-01-01
  • 详解OpenCV For Java环境搭建与功能演示

    详解OpenCV For Java环境搭建与功能演示

    这篇文章主要介绍了x详解OpenCV For Java环境搭建与功能演示,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-04-04
  • 详解Spring中singleton bean如何同时服务多个请求

    详解Spring中singleton bean如何同时服务多个请求

    这篇文章主要介绍了详解Spring中singleton bean如何同时服务多个请求
    2023-02-02
  • SpringBoot利用filter实现xss防御功能

    SpringBoot利用filter实现xss防御功能

    Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击,攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行,利用这些恶意脚本,攻击者可获取用户的敏感信息,本文给大家介绍了SpringBoot利用filter实现xss防御功能,需要的朋友可以参考下
    2024-09-09
  • SpringBoot实现多数据源的切换实践

    SpringBoot实现多数据源的切换实践

    这篇主要介绍了SpringBoot实现多数据源的切换,本文基于AOP来实现数据源的切换,文中通过示例代码介绍的非常详细,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • Spring boot 打jar包分离lib的正确配置方式

    Spring boot 打jar包分离lib的正确配置方式

    spring boot打jar包分离lib后,配置文件的方式,在网上可以搜到很多答案,但是都不够完善,今天小编给大家带来了Spring boot 打jar包分离lib的正确配置方式,感兴趣的朋友一起看看吧
    2018-02-02
  • SpringBoot整合Mybatis无法扫描xml文件的解决

    SpringBoot整合Mybatis无法扫描xml文件的解决

    这篇文章主要介绍了SpringBoot整合Mybatis无法扫描xml文件的解决操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 小白必看toString(),String.valueOf,(String)强转

    小白必看toString(),String.valueOf,(String)强转

    在Java中,往往需要把一个类型的变量转换成String 类型,本文主要介绍了toString(),String.valueOf,(String)强转,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-06-06
  • java中List数组用逗号分隔开转成字符串2种方法

    java中List数组用逗号分隔开转成字符串2种方法

    在我们日常开发中,在前后端交互的时候会遇到多个id或其他字段存放到一个字段中,这时我们会遇到一个List(集合)---->String(单个字段),这篇文章主要给大家介绍了关于java中List数组用逗号分隔开转成字符串的2种方法,需要的朋友可以参考下
    2023-10-10
  • 使用Mybatis时SqlSessionFactory对象总是报空指针

    使用Mybatis时SqlSessionFactory对象总是报空指针

    本文主要介绍了使用Mybatis时SqlSessionFactory对象总是报空指针,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-09-09

最新评论