一、摘要(本系列汇总说明)
- 总纲
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