最近有小伙伴反馈询问 如何通过实时监听远程FTP文件夹的变化并下载到本地指定目录
针对此疑问,出一期解决方案,我在冲浪时也找到了一些比较好的案例,但是追求完美的我,怎能屈服于别人的博客,对此我研究了两天解决方案,
最终得出结论:FTP协议本身不支持实时监听文件变化。可以通过定时轮询的方式来检查目录下的文件列表
1.连接到FTP服务器
2. 监听指定目录
3.检测文件变化
4.下载变化的文件
OK废话不多说,上代码ftp的搭建我就不说了
<!-- 用于连接监听FTP文件夹-->
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.9.0</version>
</dependency>
<!-- lombok插件-日志输出 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
#远程ftp相关配置
ftp:
# IP
server: 192.168.1.112
# 端口号
port: 21
# ftp用户名
user: ftpmonitor
# ftp密码
pwd: SolveProblem
# 用户主目录
dir: /home/vsftpd/ftpmonitor
# 下载到本地目录
localDir: /usr/local/tmp/netty/
# 轮询监听时间
sleep: 60000
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 读取yml配置文件中FTP相关配置
*
* @author wusiwee
*/
@Component
@ConfigurationProperties(prefix = "ftp")
public class FtpConfig {
/** ip */
@Getter
private static String server;
/** 端口 */
@Getter
private static Integer port;
/** 用户 */
@Getter
private static String user;
/** 密码 */
@Getter
private static String pwd;
/** 服务器路径 */
@Getter
private static String dir;
@Getter
private static String localDir;
/** 休眠时间 */
@Getter
private static Long sleep;
public void setServer(String server) {
FtpConfig.server = server;
}
public void setPort(Integer port) {
FtpConfig.port = port;
}
public void setUser(String user) {
FtpConfig.user = user;
}
public void setPwd(String pwd) {
FtpConfig.pwd = pwd;
}
public void setDir(String dir) {
FtpConfig.dir = dir;
}
public void setSleep(Long sleep) {
FtpConfig.sleep = sleep;
}
public void setLocalDir(String localDir) {
FtpConfig.localDir = localDir;
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
/**
* 轮询任务注入
* @author wusiwee
*/
@Configuration
public class AppConfig {
@Bean
public TaskScheduler taskScheduler() {
return new ConcurrentTaskScheduler();
}
}
先解释一下为什么要创建Service服务:可以作通用的FTP的login,loginOut处理
import org.apache.commons.net.ftp.FTPClient;
import java.io.IOException;
/**
* Ftp服务
* @author wusiwee
*/
public interface FtpService {
/**
* ftp登陆
* @return boolean 是否登陆成功
* @throws IOException 异常
* */
FTPClient login() throws IOException;
/**
* ftp登出
*
* @param ftpClient 应用
*/
void loginOut(FTPClient ftpClient);
/**
* 实时监听处理远程目录文件
*
**/
void handleFile();
}
import lombok.RequiredArgsConstructor;
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.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.*;
import java.util.concurrent.*;
/**
* @author wusiwee
* 监听远程FTP服务器目录变化
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class FtpServiceImplTemp implements FtpService {
/**
* 轮询任务处理器
*/
private final TaskScheduler taskScheduler;
/**
* 视频文件service,自行更换为自己的业务
*/
private final VideoFileService videoFileService;
/**
* 全局控制开关
*/
private volatile boolean isRunning = false;
/**
* 自定义线程池,用于处理异步下载
*/
private final ExecutorService downloadExecutor = new ThreadPoolExecutor(
1, 1, 30, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
/**
* 启动时自定触发此方法,轮询触发任务,1分钟检查一次
* taskScheduler 具有异步执行效果,所以启动时不影响主程序运行
*/
@PostConstruct
public void init() {
taskScheduler.scheduleWithFixedDelay(this::handleFile, FtpConfig.getSleep());
}
/**
* ftp登陆
* @return boolean 是否登陆成功
* */
@Override
public FTPClient login() throws IOException {
FTPClient ftpClient = new FTPClient();
ftpClient.connect(FtpConfig.getServer(), FtpConfig.getPort());
ftpClient.login(FtpConfig.getUser(), FtpConfig.getPwd());
//设置连接超时时间为60s
ftpClient.setConnectTimeout(60000);
//设置数据超时时间为60s
ftpClient.setDataTimeout(60000);
// 设置为被动模式
ftpClient.enterLocalPassiveMode();
// 设置文件类型为二进制,防止文件损坏
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
return ftpClient;
}
/**
* ftp登出
*/
@Override
public void loginOut(FTPClient ftpClient) {
if (ftpClient.isConnected()) {
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
// 处理断开连接时的异常
log.info("断开连接出现异常:{}",e.getMessage(),e);
}
}
}
/**
* 处理文件(启动则触发)
**/
@Override
public void handleFile() {
log.info("执行ftp下载任务..");
if (isRunning) {
log.info("上一个任务未结束..");
return;
}
isRunning = true;
FTPClient ftpClient = null;
try {
ftpClient = login();
checkDirectory(ftpClient, FtpConfig.getDir());
loginOut(ftpClient);
isRunning = false;
} catch (Exception e){
isRunning = false;
// 处理异常
if (ftpClient != null) {
loginOut(ftpClient);
}
log.info("轮询监听FTP文件出现异常:{}",e.getMessage(),e);
}
}
/**
* 递归检查文件
* @param ftpClient ftp服务
* @param directory 读取的文件
* @throws IOException IO异常
*/
private void checkDirectory(FTPClient ftpClient, String directory) throws IOException {
FTPFile[] files = ftpClient.listFiles(directory);
for (FTPFile file : files) {
// 远程目录
String filePath = directory + "/" + file.getName();
if (file.isDirectory()) {
// 递归检查子目录
checkDirectory(ftpClient, filePath);
} else {
// 检查文件是否更新,文件变更时间(Linux)
long modifiedTime = file.getTimestamp().getTimeInMillis();
// 此处可以使用Redis和数据库 二选一 进行处理,我选择数据库,根据处理后的唯一标识,文件名,服务器上的操作时间 进行校验判断
String code = extractString(filePath);
// 校验文件是否在数据库存在
boolean b = videoFileService.checkVideoExit(code, file.getName().substring(0,file.getName().lastIndexOf(".")), modifiedTime);
if (b) {
downloadFile(file, filePath, code, modifiedTime);
}else {
log.info("数据已记录..");
}
}
}
}
/**
* 将文件下载到本地
*
* @param file 文件
* @param filePath 文件路径
* @param code 设备编号
*/
private void downloadFile(FTPFile file, String filePath, String code, long modifiedTime) {
downloadExecutor.submit(() -> {
FTPClient ftpClient = null;
String localPath = FtpConfig.getLocalDir() + file.getName();
try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(localPath))) {
// 每个下载任务创建自己的FTP连接
ftpClient = login();
boolean success = ftpClient.retrieveFile(filePath, outputStream);
if (success) {
log.info("远程文件下载成功,文件名{}: ", file.getName());
} else {
log.info("文件下载失败: " + file.getName());
}
} catch (IOException e) {
log.error("文件:{} 下载异常:{}", filePath, e.getMessage());
} finally {
if (ftpClient != null) {
loginOut(ftpClient);
}
}
});
}
/**
* 处理远程文件夹附带的设备编号
*
* @param path 路径(附带设备编号code)
* @return 结果
*/
private static String extractString(String path) {
String[] parts = path.split("/");
return parts.length > 5 ? parts[5] : "";
}
}
会自动触发 init()函数 进行轮询监听
人,最可悲的是自大的同时 还不努力变成自大的自己