FTP上传下传、SFTP上传下传、进度监控、断点续传、连接池封装JAVA一网打尽(六)【汇总篇】包含前面1~5所有内容且增加更高级的相关知识点

一、摘要(本系列汇总说明)

- 总纲

FTP、SFTP上传下传、进度监控、断点续传、连接池封装一网打尽(一)
FTP、SFTP上传下传、进度监控、断点续传、连接池封装一网打尽(二)
FTP、SFTP上传下传、进度监控、断点续传、连接池封装一网打尽(三)
FTP、SFTP上传下传、进度监控、断点续传、连接池封装一网打尽(四)
FTP、SFTP上传下传、进度监控、断点续传、连接池封装一网打尽(五)【完结篇】
FTP、SFTP上传下传、进度监控、断点续传、连接池封装一网打尽(六)【汇总篇】

- 篇章内容说明

第一篇:基础篇,讲FTP常规上传下载实现、SFTP常规上传下载实现、单元测试类

第二篇:FTP高级篇,讲FTP上传进度监控、断点续传,FTP下载进度监控、断点续传

第三篇:SFTP高级篇,讲SFTP上传进度监控、断点续传,SFTP下载进度监控、断点续传

第四篇:FTP进阶篇,讲FTP池化处理(连接池封装)

第五篇:SFTP进阶篇,讲SFTP池化处理(连接池封装)

第六篇:汇总篇,包含前面1~5篇所有内容,且增加更高级的相关知识点

- 本篇

本文是汇总篇,包含前面1~5篇所有内容,且增加更高级的相关知识点,单元测试类全是干货

1、增加FTP/SFTP账密加密支持,提高安全级别,满足安全需求

2、增加FTP服务(Bean)配置类,便于使用

3、增加对应的单元测试类 SftpClientPoolBeanTest,支持注入SftpClientPool的使用示例

二、环境

- SpringBoot 2.7.18   官方下载地址:SpringBoot 2.7.18

- commons-net-3.10.0.jar   官方下载地址:commons-net-3.10.0.jar

- commons-pool2-2.12.0.jar   官方下载地址:commons-pool2-2.12.0.jar

- jsch-0.1.55.jar  官方下载地址:jsch-0.1.55.jar

- Oracle JDK8u202(Oracle JDK8最后一个非商业版本)   下载地址:Oracle JDK8u202

- FileZilla Client  官方下载地址:FileZilla Client

注意:

     - (特别是MacOS用户)FileZilla有MacOS版本,下载客户端是下Client,不是Server(注意一下名字,不要下错了)。

本章源代码下载:下载地址

三、POM依赖

该系列文章通用,8篇FTP文章的pom文件都一样

请作者不用急着复制,所有源代码作者提供资源下载链接:

【下载】FTP、SFTP上传下传、进度监控、断点续传、连接池封装Java一网打尽-本资源是汇总篇的全部内容下载

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>person.brickman</groupId>
    <artifactId>ftp</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <httpclient.version>4.5.14</httpclient.version>

        <!-- 工具  -->
        <lombok.version>1.18.32</lombok.version>
        <commons-logging.version>1.3.1</commons-logging.version>
        <commons-lang3.version>3.14.0</commons-lang3.version>
        <commons-io.version>2.15.1</commons-io.version>
        <commons-configuration.version>1.10</commons-configuration.version>
        <commons-net.version>3.10.0</commons-net.version>
        <commons-pool2.version>2.12.0</commons-pool2.version>
        <jsch.version>0.1.55</jsch.version>
<!--        <sshd.version>2.12.1</sshd.version>-->

        <!-- 2.20.1  2.22.2  3.0.0-M2 3.2.5  -->
        <maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>
        <!-- 3.0.1  2.4  -->
        <maven-source-plugin.version>3.0.1</maven-source-plugin.version>
        <!--忽略本包测试-->
        <maven.test.skip>false</maven.test.skip>
        <skipTests>false</skipTests>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>

        <!--  工具  -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>${commons-net.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>${commons-logging.version}</version>
        </dependency>

        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>${jsch.version}</version>
        </dependency>

        <!-- 测试相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.6</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>${maven-source-plugin.version}</version>
                <configuration>
                    <attach>true</attach>
                </configuration>
                <executions>
                    <execution>
                        <phase>compile</phase>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-deploy-plugin</artifactId>
                <version>2.8.2</version>
                <executions>
                    <execution>
                        <id>deploy</id>
                        <phase>deploy</phase>
                        <goals>
                            <goal>deploy</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration><!-- 跳过失败的单元测试 -->
                    <testFailureIgnore>false</testFailureIgnore>
                    <skipTests>${skipTests}</skipTests>
                    <argLine>${junit.test.params} -Xmx512m -XX:MaxPermSize=256m</argLine>
                </configuration>
            </plugin>

        </plugins>

    </build>

</project>

四、实现

1、公共类

- FtpKeyValue

接口类,读者觉得常量类更顺眼也可以改(阿里原装的)

package person.brickman.ftp.consts;

/**
 * alibaba.datax.ftpreader
 *
 * @author datax
 */
public interface FtpKeyValue {
    /**
     * FTP 常用键定义
     */
    String PROTOCOL = "protocol";
    String HOST = "host";
    String USERNAME = "username";
    String PASSWORD = "password";
    String PORT = "port";
    String TIMEOUT = "timeout";
    String CONNECTPATTERN = "connectPattern";
    String PATH = "path";
    String MAXTRAVERSALLEVEL = "maxTraversalLevel";

    /**
     * 默认值定义
     */
    int DEFAULT_FTP_PORT = 21;
    int DEFAULT_SFTP_PORT = 22;
    int DEFAULT_TIMEOUT = 60000;
    int DEFAULT_MAX_TRAVERSAL_LEVEL = 100;
    String DEFAULT_FTP_CONNECT_PATTERN = "PASV";
    String CONTROL_ENCODING = "utf8";

    String NO_SUCH_FILE = "no such file";
    char C_STAR = '*';
    String STAR = "*";
    char C_QUESTION = '?';
    String QUESTION = "?";
    String SLASH = "/";
    String DOT = ".";
    String DOUBLE_DOT = "..";
}

- AbstractFtpHelper 

抽象类,不管ftp还是sftp都使用此抽象类,而不是直接操作实现类

package person.brickman.ftp;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * alibaba.datax.ftpreader
 *
 * @author datax
 */
public abstract class AbstractFtpHelper {
    public abstract void setFtpClient(Object ftpClient);
    public abstract Object getFtpClient() ;

    /**
     * 与ftp服务器建立连接
     *
     * @param @param host
     * @param @param username
     * @param @param password
     * @param @param port
     * @param @param timeout
     * @param @param connectMode PASV PORT
     * @return void
     * @throws
     */
    public abstract void loginFtpServer(String host, String username, String password, int port, int timeout, String connectMode) throws InterruptedException;

    /**
     * 断开与ftp服务器的连接
     *
     * @param
     * @return void
     * @throws
     */
    public abstract void logoutFtpServer();

    /**
     * 判断指定路径是否是目录
     *
     * @param @param  directoryPath
     * @param @return
     * @return boolean
     * @throws
     */
    public abstract boolean isDirExist(String directoryPath);

    /**
     * 判断指定路径是否是文件
     *
     * @param @param  filePath
     * @param @return
     * @return boolean
     * @throws
     */
    public abstract boolean isFileExist(String filePath);

    /**
     * 判断指定路径是否是软链接
     *
     * @param @param  filePath
     * @param @return
     * @return boolean
     * @throws
     */
    public abstract boolean isSymbolicLink(String filePath);

    /**
     * 递归获取指定路径下符合条件的所有文件绝对路径
     *
     * @param @param  directoryPath
     * @param @param  parentLevel 父目录的递归层数(首次为0)
     * @param @param  maxTraversalLevel 允许的最大递归层数
     * @param @return
     * @return HashSet<String>
     * @throws
     */
    public abstract HashSet<String> getAllFilesInDir(String directoryPath, int parentLevel, int maxTraversalLevel);

    /**
     * 获取指定路径的输入流
     *
     * @param @param  filePath
     * @param @return
     * @return InputStream
     * @throws
     */
    public abstract InputStream getInputStream(String filePath);


    /**
     * 写入指定路径的输出流
     *
     * @param @param  filePath
     * @param @return
     * @return InputStream
     * @throws
     */
    public abstract OutputStream getOutputStream(String filePath);

    /**
     * 写入指定路径的输出流
     *
     * @param @param  filePath
     * @param @param  mode   OVERWRITE = 0; RESUME = 1; APPEND = 2;
     * @param @return
     * @return InputStream
     * @throws
     */
    public abstract OutputStream getOutputStream(String filePath, int mode);

    /**
     * 获取指定路径列表下符合条件的所有文件的绝对路径
     *
     * @param @param  srcPaths 路径列表
     * @param @param  parentLevel 父目录的递归层数(首次为0)
     * @param @param  maxTraversalLevel 允许的最大递归层数
     * @param @return
     * @return HashSet<String>
     * @throws
     */
    public HashSet<String> getAllFilesInDir(List<String> srcPaths, int parentLevel, int maxTraversalLevel) {
        HashSet<String> sourceAllFiles = new HashSet<String>();
        if (!srcPaths.isEmpty()) {
            for (String eachPath : srcPaths) {
                sourceAllFiles.addAll(getAllFilesInDir(eachPath, parentLevel, maxTraversalLevel));
            }
        }
        return sourceAllFiles;
    }

    /**
     * 创建远程目录
     * 不支持递归创建, 比如 mkdir -p
     *
     * @param directoryPath
     */
    public abstract void mkdir(String directoryPath);

    /**
     * 创建远程目录
     * 支持目录递归创建
     *
     * @param directoryPath
     */
    public abstract void mkDirRecursive(String directoryPath);

    /**
     * Q:After I perform a file transfer to the server,
     * printWorkingDirectory() returns null. A:You need to call
     * completePendingCommand() after transferring the file. wiki:
     * http://wiki.apache.org/commons/Net/FrequentlyAskedQuestions
     */
    public abstract void completePendingCommand();

    /**
     * 删除文件
     * warn: 不支持文件夹删除, 比如 rm -rf
     *
     * @param filesToDelete
     */
    public abstract void deleteFiles(Set<String> filesToDelete);

    /**
     * 移动文件
     * warn: 不支持文件夹删除, 比如 rm -rf
     *
     * @param filesToMove
     * @param targetPath
     */
    public abstract void moveFiles(Set<String> filesToMove, String targetPath);
}

 - FileProgressMonitor

- FTP上传下载进度监控实现类

- 此类可以日志中打印上传/下载进度

- 日志打印的精度可通过调整代码中被除数的陪数控制

- 实际应用中如果是web应用可通过session变量实现前台页面进度展示

- 前台进度更新精度实现同程序日志进度打印精度控制逻辑

package person.brickman.ftp;

import com.jcraft.jsch.SftpProgressMonitor;
import lombok.extern.slf4j.Slf4j;
/**
 * @Description:    sftp 上传下载进度监控
 * @Author brickman
 * @CreateDate:     2025/1/2 20:30
 * @Version:        1.0
 */
@Slf4j
public class FileProgressMonitor implements SftpProgressMonitor {
    private long count =  0;  //当前接收的总字节数
    private long max = 0;  /* 最终文件大小  */
    private long percent = -1;//进度

    /**
     * 当每次传输了一个数据块后,调用count方法,count方法的参数为这一次传输的数据块大小
     * 大
     *
     * 这里显示的百分比是整数
     */
    @Override
    public boolean count(long count) {
        this.count += count;
        if (percent >= this.count * 100 / max) {
            return true;
        }
        percent = this.count * 100 / max;
        log.info("Completed {}({}%) out of {}.", this.count, percent, max ); //打印当前进度
        return true;
    }

    /**
     * 大大
     * 当传输结束时,调用end方法
     * 大
     */
    @Override
    public void end() {
        log.info("Transferring done.");
    }

    /**
     * 当文件开始传输时,调用init方法
     * 大
     */
    @Override
    public void init(int op, String src, String dest, long max) {
        log.info("Transferring begin. ");
        this.max = max;
        this.count = 0;
        this.percent = -1;
    }
}

- CustomFTPFileFilter

判断服务器文件是否存在时用,因不能直接判断某个文件是否存在,所以先列目录过滤出文件,再做判断

package person.brickman.ftp;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPFileFilter;

/**
 * @Description: ftp 文件过滤用,
 *  目前判断服务器文件是否存在时用,因不能直接判断某个文件是否存在,所以先列目录过滤出文件,再做判断
 * @Author brickman
 * @CreateDate:     2025/1/2 20:30
 * @Version: 1.0
 */
@Slf4j
public class CustomFTPFileFilter implements FTPFileFilter {

    private String fileName;

    public CustomFTPFileFilter(String fileName){
        this.fileName=fileName;
    }
    @Override
    public boolean accept(FTPFile ftpFile) {
        // 文件名
        String name = ftpFile.getName();
//        log.info("Objects.equals(name,fileName ) :{}",Objects.equals(name,fileName ) );
//        log.info("name:{},  fileName:{}",name, fileName  );
        // 获取指定文件
        return fileName.equalsIgnoreCase( name ) ;
    }
}

- Unstruct**ReaderUtil

package person.brickman.ftp;

import lombok.NoArgsConstructor;
import person.brickman.ftp.consts.FtpKeyValue;

import java.io.File;

/**
 * alibaba.datax.ftpreader
 *
 * @author datax
 */
@NoArgsConstructor
public class UnstructuredStorageReaderUtil {
    /**
     * 获取正则表达式目录的父目录
     *
     * @param @param  regexPath
     * @param @return
     * @return String
     * @throws
     */
    public static String getRegexPathParent(String regexPath) {
        int endMark;
        for (endMark = 0; endMark < regexPath.length(); endMark++) {
            if (FtpKeyValue.C_STAR != regexPath.charAt(endMark) && FtpKeyValue.C_QUESTION != regexPath.charAt(endMark)) {
                continue;
            } else {
                break;
            }
        }
        int lastDirSeparator = regexPath.substring(0, endMark).lastIndexOf(File.separatorChar);
        String parentPath = regexPath.substring(0, lastDirSeparator + 1);

        return parentPath;
    }

    /**
     * 获取含有通配符路径的父目录,目前只支持在最后一级目录使用通配符*或者?.
     * (API jcraft.jsch.ChannelSftp.ls(String path)函数限制)  http://epaul.github.io/jsch-documentation/javadoc/
     *
     * @param @param  regexPath
     * @param @return
     * @return String
     * @throws
     */
    public static String getRegexPathParentPath(String regexPath) {
        int lastDirSeparator = regexPath.lastIndexOf(File.separatorChar);
        String parentPath = "";
        parentPath = regexPath.substring(0, lastDirSeparator + 1);
        if (parentPath.contains(FtpKeyValue.STAR) || parentPath.contains(FtpKeyValue.QUESTION)) {
            throw new RuntimeException(String.format("配置项目path中:[%s]不合法,目前只支持在最后一级目录使用通配符*或者?", regexPath));
        }
        return parentPath;
    }
}

2、FTP实现类 

- StandardFtpHelper

FTP上传下载实现类

package person.brickman.ftp;

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.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import person.brickman.ftp.consts.FtpKeyValue;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Set;

/**
 * alibaba.datax.ftpreader
 *
 * @author datax
 */
@Slf4j
public class StandardFtpHelper extends AbstractFtpHelper {
    FTPClient ftpClient = null;
    HashSet<String> sourceFiles = new HashSet<String>();

    @Override
    public void setFtpClient(Object ftpClient) {
        this. ftpClient=(FTPClient)ftpClient;
    }
    @Override
    public Object getFtpClient() {
        return ftpClient;
    }
    @Override
    public void loginFtpServer(String host, String username, String password, int port, int timeout,
                               String connectMode) {
        ftpClient = new FTPClient();
        try {
            // 连接
            ftpClient.connect(host, port);
            // 登录
            ftpClient.login(username, password);
            // 不需要写死ftp server的OS TYPE,FTPClient getSystemType()方法会自动识别
            /// ftpClient.configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX));
            ftpClient.setConnectTimeout(timeout);
            ftpClient.setDataTimeout(timeout);
            if ("PASV".equals(connectMode)) {
                ftpClient.enterRemotePassiveMode();
                ftpClient.enterLocalPassiveMode();
            } else if ("PORT".equals(connectMode)) {
                ftpClient.enterLocalActiveMode();
                /// ftpClient.enterRemoteActiveMode(host, port);
            }
            int reply = ftpClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(reply)) {
                ftpClient.disconnect();
                String message = String.format("与ftp服务器建立连接失败,请检查用户名和密码是否正确: [%s]",
                        "message:host =" + host + ",username = " + username + ",port =" + port);
                log.error(message);
                throw new RuntimeException(message);
            }
            //设置命令传输编码
            String fileEncoding = System.getProperty("file.encoding");
            ftpClient.setControlEncoding(fileEncoding);
        } catch (UnknownHostException e) {
            String message = String.format("请确认ftp服务器地址是否正确,无法连接到地址为: [%s] 的ftp服务器", host);
            log.error(message);
            throw new RuntimeException(message);
        } catch (IllegalArgumentException e) {
            String message = String.format("请确认连接ftp服务器端口是否正确,错误的端口: [%s] ", port);
            log.error(message);
            throw new RuntimeException(message);
        } catch (Exception e) {
            String message = String.format("与ftp服务器建立连接失败 : [%s]",
                    "message:host =" + host + ",username = " + username + ",port =" + port);
            log.error(message);
            throw new RuntimeException(message);
        }

    }

    @Override
    public void logoutFtpServer() {
        if (ftpClient.isConnected()) {
            try {
                //todo ftpClient.completePendingCommand();//打开流操作之后必须,原因还需要深究
                ftpClient.logout();
            } catch (IOException e) {
                String message = "与ftp服务器断开连接失败";
                log.error(message);
                throw new RuntimeException(message);
            } finally {
                if (ftpClient.isConnected()) {
                    try {
                        ftpClient.disconnect();
                    } catch (IOException e) {
                        String message = "与ftp服务器断开连接失败";
                        log.error(message);
                        throw new RuntimeException(message);
                    }
                }

            }
        }
    }

    @Override
    public boolean isDirExist(String directoryPath) {
        try {
            return ftpClient.changeWorkingDirectory(new String(directoryPath.getBytes(), FTP.DEFAULT_CONTROL_ENCODING));
        } catch 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飞火流星02027

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值